diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..d93bbb19 --- /dev/null +++ b/.gitignore @@ -0,0 +1,18 @@ +# mac +.DS_Store + +# git folder +.git + +# npm +node_modules +npm-debug.log + +# package +package-lock.json + +# cache +.cache + +# personal config +config.js \ No newline at end of file diff --git a/.prettierrc.js b/.prettierrc.js new file mode 100644 index 00000000..c8c6a549 --- /dev/null +++ b/.prettierrc.js @@ -0,0 +1,7 @@ +module.exports = { + semi: true, + trailingComma: 'all', + singleQuote: true, + printWidth: 120, + tabWidth: 4, +}; diff --git a/LICENSE b/LICENSE new file mode 100644 index 00000000..8410c200 --- /dev/null +++ b/LICENSE @@ -0,0 +1,661 @@ +GNU AFFERO GENERAL PUBLIC LICENSE + Version 3, 19 November 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU Affero General Public License is a free, copyleft license for +software and other kinds of works, specifically designed to ensure +cooperation with the community in the case of network server software. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +our General Public Licenses are intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + Developers that use our General Public Licenses protect your rights +with two steps: (1) assert copyright on the software, and (2) offer +you this License which gives you legal permission to copy, distribute +and/or modify the software. + + A secondary benefit of defending all users' freedom is that +improvements made in alternate versions of the program, if they +receive widespread use, become available for other developers to +incorporate. Many developers of free software are heartened and +encouraged by the resulting cooperation. However, in the case of +software used on network servers, this result may fail to come about. +The GNU General Public License permits making a modified version and +letting the public access it on a server without ever releasing its +source code to the public. + + The GNU Affero General Public License is designed specifically to +ensure that, in such cases, the modified source code becomes available +to the community. It requires the operator of a network server to +provide the source code of the modified version running there to the +users of that server. Therefore, public use of a modified version, on +a publicly accessible server, gives the public access to the source +code of the modified version. + + An older license, called the Affero General Public License and +published by Affero, was designed to accomplish similar goals. This is +a different license, not a version of the Affero GPL, but Affero has +released a new version of the Affero GPL which permits relicensing under +this license. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU Affero General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Remote Network Interaction; Use with the GNU General Public License. + + Notwithstanding any other provision of this License, if you modify the +Program, your modified version must prominently offer all users +interacting with it remotely through a computer network (if your version +supports such interaction) an opportunity to receive the Corresponding +Source of your version by providing access to the Corresponding Source +from a network server at no charge, through some standard or customary +means of facilitating copying of software. This Corresponding Source +shall include the Corresponding Source for any work covered by version 3 +of the GNU General Public License that is incorporated pursuant to the +following paragraph. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the work with which it is combined will remain governed by version +3 of the GNU General Public License. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU Affero General Public License from time to time. Such new versions +will be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU Affero General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU Affero General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU Affero General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published + by the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If your software can interact with users remotely through a computer +network, you should also make sure that it provides a way for users to +get its source. For example, if your program is a web application, its +interface could display a "Source" link that leads users to an archive +of the code. There are many ways you could offer source, and different +solutions will be better for different programs; see section 13 for the +specific requirements. + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU AGPL, see +. \ No newline at end of file diff --git a/README.md b/README.md index 626eda61..c3ee8920 100644 --- a/README.md +++ b/README.md @@ -1 +1,86 @@ -# mirotalksfu \ No newline at end of file +# mirotalksfu + +`Open Source WebRTC video calls, messaging and screen sharing` + +![License: AGPLv3](https://img.shields.io/badge/License-AGPLv3-blue.svg) +[![Donate](https://img.shields.io/badge/Donate-PayPal-brightgreen.svg)](https://paypal.me/MiroslavPejic?locale.x=it_IT) +[![Repo Link](https://img.shields.io/badge/Repo-Link-black.svg)](https://github.com/miroslavpejic85/mirotalk) +[![Code style: prettier](https://img.shields.io/badge/Code_style-Prettier-ff69b4.svg?)](https://github.com/prettier/prettier) + +Powered by `WebRTC` using [SFU](https://mediasoup.org) integrated server. + +![mirotalksfu](public/images/mirotalksfu.png) + +## Features + +- Is `100% Free` - `Open Source` and `Self Hosted` +- `No download`, `plug-in` or `login` required, entirely browser based +- `Unlimited` number of `conference rooms` and `users`, `without` call `time limitation` +- Desktop and Mobile compatible +- Optimized Room URL Sharing (share it to your participants, wait them to join) +- Webcam Streaming up to 4K quality (Front - Rear for mobile) +- Echo cancellation and noise suppression that makes your audio crystal clear +- Screen Sharing to present documents, slides, and more ... +- Chat with Emoji Picker to show you feeling and possibility to Save the conversations +- Select Microphone - Speaker and Video source +- Recording your Screen, Audio or Video +- Full Screen Mode on mouse click on the Video element +- Supports [REST API](api/README.md) (Application Programming Interface) + +## Quick Start + +- You will need to have `Node.js` installed, this project has been tested with Node version [12.X](https://nodejs.org/en/blog/release/v12.22.1/) and [14.X](https://nodejs.org/en/blog/release/v14.17.5/) `not` with `16.X`. + +```bash +# clone this repo +git clone https://github.com/miroslavpejic85/mirotalksfu.git + +# mirotalk dir +cd mirotalksfu + +# copy src/config.template.js src/config.js +cp src/config.template.js src/config.js + +# install dependencies +npm install + +# start the server +npm start +``` + +- Open https://localhost:3010 in browser + +--- + +## API + +The `response` will give you a `entrypoint / Room URL` for `your meeting`. + +```bash +curl -X POST "http://localhost:3010/api/v1/meeting" -H "authorization: mirotalksfu_default_secret" -H "Content-Type: application/json" +``` + +--- + +## Notes + +Run the project on a `Linux or Mac` system as the `mediasoup` installation could have issues on `Windows`. If you have a Windows system, consider to installing [WSL](https://docs.microsoft.com/en-us/windows/wsl/install-win10) to be able to run it. + +--- + +## Credits + +- [Davide Pacilio](https://cruip.com/demos/solid/) (html template) +- [Dirk Vanbeveren](https://github.com/Dirvann) (sfu logic) +- [Mediasoup](https://mediasoup.org) (sfu server) + +--- + +## Contributing + +- Contributions are welcome and greatly appreciated! +- Just run before `npm run lint` + +## License + +[![AGPLv3](public/images/AGPLv3.png)](LICENSE) diff --git a/api/README.md b/api/README.md new file mode 100644 index 00000000..d5b890fc --- /dev/null +++ b/api/README.md @@ -0,0 +1,40 @@ +![restAPI](restAPI.png) + +## Create a meeting + +Create a meeting with a `HTTP request` containing the `API_KEY` sent to MiroTalk’s server. The response contains a `meeting` URL that can be `embedded` in your client within an `iframe`. + +```bash +# js +node meeting.js +# php +php meeting.php +# python +python meeting.py +# bash +./meeting.sh +``` + +## Embed a meeting + +Embedding a meeting into a `service` or `app` requires using an `iframe` with the `src` attribute specified as the `meeting` from `HTTP response`. + +```html + +``` + +## Fast Integration + +Develop your `website` or `application`, and bring `video meetings` in with a simple `iframe`. + +```html + +``` diff --git a/api/meeting.js b/api/meeting.js new file mode 100644 index 00000000..0699341e --- /dev/null +++ b/api/meeting.js @@ -0,0 +1,22 @@ +'use strict'; + +const fetch = require('node-fetch'); + +const API_KEY = 'mirotalksfu_default_secret'; +const MIROTALK_URL = 'http://localhost:3010/api/v1/meeting'; + +function getResponse() { + return fetch(MIROTALK_URL, { + method: 'POST', + headers: { + authorization: API_KEY, + 'Content-Type': 'application/json', + }, + }); +} + +getResponse().then(async (res) => { + console.log('Status code:', res.status); + const data = await res.json(); + console.log('meeting:', data.meeting); +}); diff --git a/api/meeting.php b/api/meeting.php new file mode 100644 index 00000000..56fff7a8 --- /dev/null +++ b/api/meeting.php @@ -0,0 +1,24 @@ +{'meeting'}, "\n"; diff --git a/api/meeting.py b/api/meeting.py new file mode 100644 index 00000000..a926816f --- /dev/null +++ b/api/meeting.py @@ -0,0 +1,19 @@ +import requests +import json + +API_KEY = "mirotalksfu_default_secret" +MIROTALK_URL = "http://localhost:3010/api/v1/meeting" + +headers = { + "authorization": API_KEY, + "Content-Type": "application/json", +} + +response = requests.post( + MIROTALK_URL, + headers=headers +) + +print("Status code:", response.status_code) +data = json.loads(response.text) +print("meeting:", data["meeting"]) diff --git a/api/meeting.sh b/api/meeting.sh new file mode 100755 index 00000000..96ebe65c --- /dev/null +++ b/api/meeting.sh @@ -0,0 +1,9 @@ +#!/bin/bash + +API_KEY="mirotalksfu_default_secret" +MIROTALK_URL="http://localhost:3010/api/v1/meeting" + +curl $MIROTALK_URL \ + --header "authorization: $API_KEY" \ + --header "Content-Type: application/json" \ + --request POST \ No newline at end of file diff --git a/api/restAPI.png b/api/restAPI.png new file mode 100644 index 00000000..9c3f219f Binary files /dev/null and b/api/restAPI.png differ diff --git a/package.json b/package.json new file mode 100644 index 00000000..9ae271b9 --- /dev/null +++ b/package.json @@ -0,0 +1,29 @@ +{ + "name": "mirotalksfu", + "version": "1.0.0", + "description": "WebRTC SFU browser-based video calls", + "main": "Server.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1", + "start": "node src/Server.js", + "compile": "npx browserify public/modules/MediasoupClientCompile.js -o public/modules/MediasoupClient.js", + "lint": "npx prettier --write ." + }, + "author": "Miroslav Pejic", + "license": "AGPLv3", + "dependencies": { + "compression": "^1.7.4", + "cors": "^2.8.5", + "express": "^4.17.1", + "httpolyglot": "^0.1.2", + "mediasoup": "^3.8.3", + "mediasoup-client": "^3.6.37", + "ngrok": "^4.0.1", + "socket.io": "^4.1.3", + "uuid": "8.3.2" + }, + "devDependencies": { + "node-fetch": "^2.6.1", + "prettier": "2.3.2" + } +} diff --git a/public/Room.html b/public/Room.html new file mode 100644 index 00000000..39033386 --- /dev/null +++ b/public/Room.html @@ -0,0 +1,228 @@ + + + + + + MiroTalk SFU - Room Video Calls, Messaging and Screen Sharing. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+
+ +
+
+
+
+
Chat
+
+ + + +
+
+
+
+
+
+
+
Participant
+
00:00:00
+
+
Public message example
+
+
+
+
+
+
+
You
+
00:00:00
+
+
Public message example
+
+
+
+
+ + + +
+ +
+
+ + diff --git a/public/css/Room.css b/public/css/Room.css new file mode 100644 index 00000000..d5ff3554 --- /dev/null +++ b/public/css/Room.css @@ -0,0 +1,575 @@ +/*-------------------------------------------------------------- +# Keyframes +--------------------------------------------------------------*/ + +@-webkit-keyframes fadeIn { + from { + opacity: 0; + } + to { + opacity: 1; + } +} + +@-moz-keyframes fadeIn { + from { + opacity: 0; + } + to { + opacity: 1; + } +} + +@keyframes fadeIn { + from { + opacity: 0; + } + to { + opacity: 1; + } +} + +:root { + --msger-top: 50%; + --msger-left: 50%; + --msger-height: 680px; + --msger-width: 420px; + --msger-bg: #16171b; + --left-msg-bg: #222328; + --right-msg-bg: #0a0b0c; + --box-shadow: 0px 8px 16px 0px rgba(0, 0, 0, 0.2); +} + +* { + outline: none; + font-family: 'Verdana'; +} + +body { + background-image: url('../images/background.jpg'); + background-repeat: no-repeat; + background-attachment: fixed; + background-size: cover; +} + +/*-------------------------------------------------------------- +# Control buttons +--------------------------------------------------------------*/ + +#control { + z-index: 1; + position: fixed; + padding: 10px; + top: 0; + width: 100%; + background: black; +} + +#control button { + border-radius: 5px; +} +#control p { + font-size: small; + cursor: default; +} + +/*-------------------------------------------------------------- +# Room QR +--------------------------------------------------------------*/ + +#qrRoomContainer { + display: flex; + justify-content: center; + align-items: center; +} + +/*-------------------------------------------------------------- +# Video grid +--------------------------------------------------------------*/ + +.containers { + display: grid; + grid-template-columns: repeat(2, 1fr); + column-gap: 10px; + row-gap: 10px; + border-radius: 5px; +} + +@media only screen and (max-width: 720px) { + .containers { + display: grid; + grid-template-columns: 1fr; + column-gap: 10px; + row-gap: 10px; + border-radius: 5px; + } +} + +.vid { + flex: 0 1 auto; + height: 360px; + border-radius: 10px; + cursor: pointer; +} + +video { + width: 100%; + height: 100%; + object-fit: cover; + box-shadow: var(--box-shadow); +} + +video:hover { + opacity: 0.9; +} + +video:fullscreen { + object-fit: contain; /* cover; */ + opacity: 1; +} + +.mirror { + -webkit-transform: rotateY(180deg); + -moz-transform: rotateY(180deg); + transform: rotateY(180deg); +} + +#videoMedia { + margin-top: 50px; +} + +#localMedia, +#remoteVideos { + margin: 10px; + cursor: default; +} + +.pn { + position: absolute; + display: flex; + align-items: center; + padding: 10px; + margin: 10px; + width: auto; + height: 30px; + border-radius: 5px; + margin-top: 325px; + color: white; + background: rgba(0, 0, 0, 0.7); +} + +.d, +.d video { + position: relative; +} + +.d p { + position: absolute; + padding: 10px; + margin: 10px; + width: auto; + height: 30px; + border-radius: 5px; + top: 315px; + color: white; + background: rgba(0, 0, 0, 0.7); + display: flex; + align-items: center; +} + +/*-------------------------------------------------------------- +# Dropdown menù +--------------------------------------------------------------*/ + +.dropdown { + position: relative; + display: inline-block; +} + +.dropdown-content { + z-index: 2; + display: none; + position: absolute; + margin: auto; + padding: 10px; + min-width: 200px; + background-color: rgba(0, 0, 0, 0.7); + color: white; + overflow: auto; + border-radius: 5px; + box-shadow: var(--box-shadow); +} + +.dropdown-content select { + width: auto; + font-size: small; +} + +/*-------------------------------------------------------------- +# Recording +--------------------------------------------------------------*/ + +.recording { + display: flex; +} + +.recording button, +.recording p { + padding: 5px; + margin: 1px; + font-size: 0.8em; + border-radius: 5px; +} + +/*-------------------------------------------------------------- +# Chat Room +--------------------------------------------------------------*/ + +.chat-room { + z-index: 3; + display: none; + position: fixed; + height: var(--msger-height); + width: var(--msger-width); + background: var(--msger-bg); + border-radius: 5px; + border-radius: 10px; + box-shadow: var(--box-shadow); + overflow: hidden; +} + +.msger { + display: flex; + flex-flow: column wrap; + justify-content: space-between; + top: var(--msger-top); + left: var(--msger-left); + height: var(--msger-height); + width: var(--msger-width); + background: var(--msger-bg); +} + +/*-------------------------------------------------------------- +# Chat room header +--------------------------------------------------------------*/ + +.chat-header { + display: flex; + justify-content: space-between; + padding: 10px; + background: rgb(0, 0, 0); + color: #666; + cursor: move; +} + +.chat-header-options button { + border: none; + font-size: 1.2rem; + transition: all 0.3s ease-in-out; + background: rgb(0, 0, 0); + color: #fff; + border-radius: 5px; + transition: background 0.23s; +} + +/*-------------------------------------------------------------- +# Chat room output area +--------------------------------------------------------------*/ + +.chat-msger { + flex: 1; + overflow-y: auto; + padding: 10px; + background: var(--msger-bg); +} + +.chat-msger::-webkit-scrollbar { + width: 5px; +} + +.chat-msger::-webkit-scrollbar-track { + background: transparent; +} + +.chat-msger::-webkit-scrollbar-thumb { + background: black; + /*aqua;*/ +} + +.msg { + display: flex; + align-items: flex-end; + margin-bottom: 10px; +} + +.msg:last-of-type { + margin: 0; +} + +/*-------------------------------------------------------------- +# Chat room left side +--------------------------------------------------------------*/ + +.left-msg .msg-bubble { + background: var(--left-msg-bg); + border-bottom-left-radius: 0; + color: #fff; +} + +.left-msg .private-msg-bubble { + background: var(--private-msg-bg); + border-bottom-left-radius: 0; + color: #fff; +} + +/*-------------------------------------------------------------- +# Chat room right side +--------------------------------------------------------------*/ + +.right-msg { + flex-direction: row-reverse; +} + +.right-msg .msg-bubble { + background: var(--right-msg-bg); + border-bottom-right-radius: 0; + color: #fff; +} + +.right-msg .private-msg-bubble { + background: var(--private-msg-bg); + border-bottom-right-radius: 0; + color: #fff; +} + +.right-msg .msg-img { + margin: 0 0 0 10px; +} + +/*-------------------------------------------------------------- +# Chat room common +--------------------------------------------------------------*/ + +.msg-img { + width: 50px; + height: 50px; + margin-right: 10px; + background-repeat: no-repeat; + background-position: center; + background-size: cover; +} + +.msg-bubble { + max-width: 200px; + padding: 15px; + border-radius: 15px; +} + +.msg-info { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 10px; +} + +.msg-info-name { + margin-right: 10px; + font-weight: bold; +} + +.msg-info-time { + font-size: 0.85em; +} + +.msg-text { + overflow-wrap: break-word; + word-wrap: break-word; + hyphens: auto; +} + +#chat-msg-a { + color: white; +} + +/*-------------------------------------------------------------- +# Chat room input area +--------------------------------------------------------------*/ + +.chat-msger-inputarea { + display: flex; + padding: 10px; + background: #222328; +} + +.chat-msger-input { + flex: 1; + padding: 10px; + border: none; + border-top-left-radius: 10px; + background: rgb(0, 0, 0); + color: white; +} + +.chat-msger-inputarea button { + width: 32px; +} + +/*-------------------------------------------------------------- +# Chat room emoji picker +--------------------------------------------------------------*/ + +emoji-picker { + top: 0px; + position: absolute; + width: 100%; + height: 100%; + --background: #16171b; + --num-columns: 9; + --emoji-size: 1.5rem; + overflow: hidden; +} + +/*-------------------------------------------------------------- +# swal2 +--------------------------------------------------------------*/ + +.swal2-validation-message, +.swal2-title, +.swal2-content, +.swal2-input { + text-align: center; + color: white !important; + background-color: transparent !important; +} + +/*-------------------------------------------------------------- +# About +--------------------------------------------------------------*/ + +#about { + cursor: default; +} + +#about b { + color: rgb(0, 180, 50); +} + +#about img { + cursor: pointer; + border-radius: 10px; +} + +#about a { + color: white; + text-decoration: none; +} + +#about a:hover { + color: rgb(0, 180, 50); + transition: all 0.3s ease-in-out; +} + +#about button { + border: none; + width: 170px; + height: 40px; + font-size: 1.2rem; + background: linear-gradient(100deg, #376df9 0, #4b4547 75%, #222222 100%); + box-shadow: 0 0 6px 0 rgb(251 255 0 / 82%); + color: #ffffff; + transition: background 0.23s; + cursor: pointer; +} + +#about button:hover { + font-weight: bold; +} + +/*-------------------------------------------------------------- +# Common +--------------------------------------------------------------*/ + +.hidden { + display: none; +} + +.show { + display: block; +} + +.center { + top: 50%; + left: 50%; + transform: translate(-50%, -50%); +} + +.fadein { + -webkit-animation: fadeIn ease-in 1; + -moz-animation: fadeIn ease-in 1; + animation: fadeIn ease-in 1; + -webkit-animation-fill-mode: forwards; + -moz-animation-fill-mode: forwards; + animation-fill-mode: forwards; + -webkit-animation-duration: 1s; + -moz-animation-duration: 1s; + animation-duration: 1s; +} + +p, +button { + background: black; + color: white; + border: none; +} + +button:hover { + color: rgb(0, 180, 50); +} + +/*-------------------------------------------------------------- +# Pulse class effect +--------------------------------------------------------------*/ + +.pulsate { + animation: pulsate 3s ease-out; + animation-iteration-count: infinite; + -webkit-animation: pulsate 3s ease-out; + -webkit-animation-iteration-count: infinite; + opacity: 0.5; +} + +@-webkit-keyframes pulsate { + 0% { + opacity: 0.5; + } + 50% { + opacity: 1; + } + 100% { + opacity: 0.5; + } +} + +@keyframes pulsate { + 0% { + opacity: 0.5; + } + 50% { + opacity: 1; + } + 100% { + opacity: 0.5; + } +} + +@-webkit-keyframes pulsate { + 0% { + opacity: 0.5; + } + 50% { + opacity: 1; + } + 100% { + opacity: 0.5; + } +} diff --git a/public/css/landing.css b/public/css/landing.css new file mode 100644 index 00000000..2b418c09 --- /dev/null +++ b/public/css/landing.css @@ -0,0 +1,1793 @@ +html { + line-height: 1.15; + -ms-text-size-adjust: 100%; + -webkit-text-size-adjust: 100%; +} +body { + margin: 0; +} +article, +aside, +footer, +header, +nav, +section { + display: block; +} +h1 { + font-size: 2em; + margin: 0.67em 0; +} +figcaption, +figure, +main { + display: block; +} +figure { + margin: 1em 40px; +} +hr { + box-sizing: content-box; + height: 0; + overflow: visible; +} +pre { + font-family: monospace, monospace; + font-size: 1em; +} +a { + background-color: transparent; + -webkit-text-decoration-skip: objects; +} +abbr[title] { + border-bottom: none; + text-decoration: underline; + -webkit-text-decoration: underline dotted; + text-decoration: underline dotted; +} +b, +strong { + font-weight: inherit; +} +b, +strong { + font-weight: bolder; +} +code, +kbd, +samp { + font-family: monospace, monospace; + font-size: 1em; +} +dfn { + font-style: italic; +} +mark { + background-color: #ff0; + color: #000; +} +small { + font-size: 80%; +} +sub, +sup { + font-size: 75%; + line-height: 0; + position: relative; + vertical-align: baseline; +} +sub { + bottom: -0.25em; +} +sup { + top: -0.5em; +} +audio, +video { + display: inline-block; +} +audio:not([controls]) { + display: none; + height: 0; +} +img { + border-style: none; +} +svg:not(:root) { + overflow: hidden; +} +button, +input, +optgroup, +select, +textarea { + font-family: sans-serif; + font-size: 100%; + line-height: 1.15; + margin: 0; +} +button, +input { + overflow: visible; +} +button, +select { + text-transform: none; +} +button, +html [type='button'], +[type='reset'], +[type='submit'] { + -webkit-appearance: button; +} +button::-moz-focus-inner, +[type='button']::-moz-focus-inner, +[type='reset']::-moz-focus-inner, +[type='submit']::-moz-focus-inner { + border-style: none; + padding: 0; +} +button:-moz-focusring, +[type='button']:-moz-focusring, +[type='reset']:-moz-focusring, +[type='submit']:-moz-focusring { + outline: 1px dotted ButtonText; +} +fieldset { + padding: 0.35em 0.75em 0.625em; +} +legend { + box-sizing: border-box; + color: inherit; + display: table; + max-width: 100%; + padding: 0; + white-space: normal; +} +progress { + display: inline-block; + vertical-align: baseline; +} +textarea { + overflow: auto; +} +[type='checkbox'], +[type='radio'] { + box-sizing: border-box; + padding: 0; +} +[type='number']::-webkit-inner-spin-button, +[type='number']::-webkit-outer-spin-button { + height: auto; +} +[type='search'] { + -webkit-appearance: textfield; + outline-offset: -2px; +} +[type='search']::-webkit-search-cancel-button, +[type='search']::-webkit-search-decoration { + -webkit-appearance: none; +} +::-webkit-file-upload-button { + -webkit-appearance: button; + font: inherit; +} +details, +menu { + display: block; +} +summary { + display: list-item; +} +canvas { + display: inline-block; +} +template { + display: none; +} +[hidden] { + display: none; +} +html { + box-sizing: border-box; +} +*, +*:before, +*:after { + box-sizing: inherit; +} +body { + background: #1d2026; + -moz-osx-font-smoothing: grayscale; + -webkit-font-smoothing: antialiased; +} +hr { + border: 0; + display: block; + height: 1px; + background: #242830; + margin-top: 24px; + margin-bottom: 24px; +} +ul, +ol { + margin-top: 0; + margin-bottom: 24px; + padding-left: 24px; +} +ul { + list-style: disc; +} +ol { + list-style: decimal; +} +li > ul, +li > ol { + margin-bottom: 0; +} +dl { + margin-top: 0; + margin-bottom: 24px; +} +dt { + font-weight: 600; +} +dd { + margin-left: 24px; + margin-bottom: 24px; +} +img { + height: auto; + max-width: 100%; + vertical-align: middle; +} +figure { + margin: 24px 0; +} +figcaption { + font-size: 16px; + line-height: 24px; + padding: 8px 0; +} +img, +svg { + display: block; +} +table { + border-collapse: collapse; + margin-bottom: 24px; + width: 100%; +} +tr { + border-bottom: 1px solid #242830; +} +th { + text-align: left; +} +th, +td { + padding: 10px 16px; +} +th:first-child, +td:first-child { + padding-left: 0; +} +th:last-child, +td:last-child { + padding-right: 0; +} +html { + font-size: 20px; + line-height: 30px; +} +body { + color: #8a94a7; + font-size: 1rem; +} +body, +button, +input, +select, +textarea { + font-family: 'IBM Plex Sans', sans-serif; +} +a { + color: #8a94a7; + text-decoration: underline; +} +a:hover, +a:active { + outline: 0; + text-decoration: none; +} +h1, +h2, +h3, +h4, +h5, +h6, +.h1, +.h2, +.h3, +.h4, +.h5, +.h6 { + clear: both; + color: #fff; + font-weight: 600; +} +h1, +.h1 { + font-size: 38px; + line-height: 48px; + letter-spacing: 0px; +} +@media (min-width: 641px) { + h1, + .h1 { + font-size: 44px; + line-height: 54px; + letter-spacing: 0px; + } +} +h2, +.h2 { + font-size: 32px; + line-height: 42px; + letter-spacing: 0px; +} +@media (min-width: 641px) { + h2, + .h2 { + font-size: 38px; + line-height: 48px; + letter-spacing: 0px; + } +} +h3, +.h3, +blockquote { + font-size: 24px; + line-height: 34px; + letter-spacing: 0px; +} +@media (min-width: 641px) { + h3, + .h3, + blockquote { + font-size: 32px; + line-height: 42px; + letter-spacing: 0px; + } +} +h4, +h5, +h6, +.h4, +.h5, +.h6 { + font-size: 20px; + line-height: 30px; + letter-spacing: -0.1px; +} +@media (min-width: 641px) { + h4, + h5, + h6, + .h4, + .h5, + .h6 { + font-size: 24px; + line-height: 34px; + letter-spacing: 0px; + } +} +@media (max-width: 640px) { + .h1-mobile { + font-size: 38px; + line-height: 48px; + letter-spacing: 0px; + } + .h2-mobile { + font-size: 32px; + line-height: 42px; + letter-spacing: 0px; + } + .h3-mobile { + font-size: 24px; + line-height: 34px; + letter-spacing: 0px; + } + .h4-mobile, + .h5-mobile, + .h6-mobile { + font-size: 20px; + line-height: 30px; + letter-spacing: -0.1px; + } +} +.text-light h1, +.text-light h2, +.text-light h3, +.text-light h4, +.text-light h5, +.text-light h6, +.text-light .h1, +.text-light .h2, +.text-light .h3, +.text-light .h4, +.text-light .h5, +.text-light .h6 { + color: !important; +} +.text-sm { + font-size: 18px; + line-height: 28px; + letter-spacing: -0.1px; +} +.text-xs { + font-size: 16px; + line-height: 24px; + letter-spacing: -0.1px; +} +h1, +h2, +.h1, +.h2 { + margin-top: 48px; + margin-bottom: 16px; +} +h3, +.h3 { + margin-top: 36px; + margin-bottom: 12px; +} +h4, +h5, +h6, +.h4, +.h5, +.h6 { + margin-top: 24px; + margin-bottom: 4px; +} +p { + margin-top: 0; + margin-bottom: 24px; +} +dfn, +cite, +em, +i { + font-style: italic; +} +blockquote { + color: #3b404c; + font-style: italic; + margin-top: 24px; + margin-bottom: 24px; + margin-left: 24px; +} +blockquote::before { + content: '\201C'; +} +blockquote::after { + content: '\201D'; +} +blockquote p { + display: inline; +} +address { + color: #8a94a7; + border-width: 1px 0; + border-style: solid; + border-color: #242830; + padding: 24px 0; + margin: 0 0 24px; +} +pre, +pre h1, +pre h2, +pre h3, +pre h4, +pre h5, +pre h6, +pre .h1, +pre .h2, +pre .h3, +pre .h4, +pre .h5, +pre .h6 { + font-family: 'Courier 10 Pitch', Courier, monospace; +} +pre, +code, +kbd, +tt, +var { + background: #1d2026; +} +pre { + font-size: 16px; + line-height: 24px; + margin-bottom: 1.6em; + max-width: 100%; + overflow: auto; + padding: 24px; + margin-top: 24px; + margin-bottom: 24px; +} +code, +kbd, +tt, +var { + font-family: Monaco, Consolas, 'Andale Mono', 'DejaVu Sans Mono', monospace; + font-size: 16px; + padding: 2px 4px; +} +abbr, +acronym { + cursor: help; +} +mark, +ins { + text-decoration: none; +} +small { + font-size: 18px; + line-height: 28px; + letter-spacing: -0.1px; +} +b, +strong { + font-weight: 600; +} +button, +input, +select, +textarea, +label { + font-size: 20px; + line-height: 30px; +} +.container, +.container-sm { + width: 100%; + margin: 0 auto; + padding-left: 16px; + padding-right: 16px; +} +@media (min-width: 481px) { + .container, + .container-sm { + padding-left: 24px; + padding-right: 24px; + } +} +.container { + max-width: 1128px; +} +.container-sm { + max-width: 848px; +} +.container .container-sm { + max-width: 800px; + padding-left: 0; + padding-right: 0; +} +.screen-reader-text { + clip: rect(1px, 1px, 1px, 1px); + position: absolute !important; + height: 1px; + width: 1px; + overflow: hidden; + word-wrap: normal !important; +} +.screen-reader-text:focus { + border-radius: 2px; + box-shadow: 0 0 2px 2px rgba(0, 0, 0, 0.6); + clip: auto !important; + display: block; + font-size: 14px; + letter-spacing: 0px; + font-weight: 600; + line-height: 16px; + text-decoration: none; + text-transform: uppercase; + background-color: #1d2026; + color: #0270d7 !important; + border: none; + height: auto; + left: 8px; + padding: 16px 32px; + top: 8px; + width: auto; + z-index: 100000; +} +.list-reset { + list-style: none; + padding: 0; +} +.text-left { + text-align: left; +} +.text-center { + text-align: center; +} +.text-right { + text-align: right; +} +.text-primary { + color: #0270d7; +} +.has-top-divider { + position: relative; +} +.has-top-divider::before { + content: ''; + position: absolute; + top: 0; + left: 0; + width: 100%; + display: block; + height: 1px; + background: #242830; +} +.has-bottom-divider { + position: relative; +} +.has-bottom-divider::after { + content: ''; + position: absolute; + bottom: 0; + left: 0; + width: 100%; + display: block; + height: 1px; + background: #242830; +} +.m-0 { + margin: 0; +} +.mt-0 { + margin-top: 0; +} +.mr-0 { + margin-right: 0; +} +.mb-0 { + margin-bottom: 0; +} +.ml-0 { + margin-left: 0; +} +.m-8 { + margin: 8px; +} +.mt-8 { + margin-top: 8px; +} +.mr-8 { + margin-right: 8px; +} +.mb-8 { + margin-bottom: 8px; +} +.ml-8 { + margin-left: 8px; +} +.m-16 { + margin: 16px; +} +.mt-16 { + margin-top: 16px; +} +.mr-16 { + margin-right: 16px; +} +.mb-16 { + margin-bottom: 16px; +} +.ml-16 { + margin-left: 16px; +} +.m-24 { + margin: 24px; +} +.mt-24 { + margin-top: 24px; +} +.mr-24 { + margin-right: 24px; +} +.mb-24 { + margin-bottom: 24px; +} +.ml-24 { + margin-left: 24px; +} +.m-32 { + margin: 32px; +} +.mt-32 { + margin-top: 32px; +} +.mr-32 { + margin-right: 32px; +} +.mb-32 { + margin-bottom: 32px; +} +.ml-32 { + margin-left: 32px; +} +.m-40 { + margin: 40px; +} +.mt-40 { + margin-top: 40px; +} +.mr-40 { + margin-right: 40px; +} +.mb-40 { + margin-bottom: 40px; +} +.ml-40 { + margin-left: 40px; +} +.m-48 { + margin: 48px; +} +.mt-48 { + margin-top: 48px; +} +.mr-48 { + margin-right: 48px; +} +.mb-48 { + margin-bottom: 48px; +} +.ml-48 { + margin-left: 48px; +} +.m-56 { + margin: 56px; +} +.mt-56 { + margin-top: 56px; +} +.mr-56 { + margin-right: 56px; +} +.mb-56 { + margin-bottom: 56px; +} +.ml-56 { + margin-left: 56px; +} +.m-64 { + margin: 64px; +} +.mt-64 { + margin-top: 64px; +} +.mr-64 { + margin-right: 64px; +} +.mb-64 { + margin-bottom: 64px; +} +.ml-64 { + margin-left: 64px; +} +.p-0 { + padding: 0; +} +.pt-0 { + padding-top: 0; +} +.pr-0 { + padding-right: 0; +} +.pb-0 { + padding-bottom: 0; +} +.pl-0 { + padding-left: 0; +} +.p-8 { + padding: 8px; +} +.pt-8 { + padding-top: 8px; +} +.pr-8 { + padding-right: 8px; +} +.pb-8 { + padding-bottom: 8px; +} +.pl-8 { + padding-left: 8px; +} +.p-16 { + padding: 16px; +} +.pt-16 { + padding-top: 16px; +} +.pr-16 { + padding-right: 16px; +} +.pb-16 { + padding-bottom: 16px; +} +.pl-16 { + padding-left: 16px; +} +.p-24 { + padding: 24px; +} +.pt-24 { + padding-top: 24px; +} +.pr-24 { + padding-right: 24px; +} +.pb-24 { + padding-bottom: 24px; +} +.pl-24 { + padding-left: 24px; +} +.p-32 { + padding: 32px; +} +.pt-32 { + padding-top: 32px; +} +.pr-32 { + padding-right: 32px; +} +.pb-32 { + padding-bottom: 32px; +} +.pl-32 { + padding-left: 32px; +} +.p-40 { + padding: 40px; +} +.pt-40 { + padding-top: 40px; +} +.pr-40 { + padding-right: 40px; +} +.pb-40 { + padding-bottom: 40px; +} +.pl-40 { + padding-left: 40px; +} +.p-48 { + padding: 48px; +} +.pt-48 { + padding-top: 48px; +} +.pr-48 { + padding-right: 48px; +} +.pb-48 { + padding-bottom: 48px; +} +.pl-48 { + padding-left: 48px; +} +.p-56 { + padding: 56px; +} +.pt-56 { + padding-top: 56px; +} +.pr-56 { + padding-right: 56px; +} +.pb-56 { + padding-bottom: 56px; +} +.pl-56 { + padding-left: 56px; +} +.p-64 { + padding: 64px; +} +.pt-64 { + padding-top: 64px; +} +.pr-64 { + padding-right: 64px; +} +.pb-64 { + padding-bottom: 64px; +} +.pl-64 { + padding-left: 64px; +} +.sr .has-animations .is-revealing { + visibility: hidden; +} +.has-animations .anime-element { + visibility: hidden; +} +.anime-ready .has-animations .anime-element { + visibility: visible; +} +.input, +.textarea { + background-color: #fff; + border-width: 1px; + border-style: solid; + border-color: #242830; + border-radius: 2px; + color: #8a94a7; + max-width: 100%; + width: 100%; +} +.input::-webkit-input-placeholder, +.textarea::-webkit-input-placeholder { + color: #3b404c; +} +.input:-ms-input-placeholder, +.textarea:-ms-input-placeholder { + color: #3b404c; +} +.input::-ms-input-placeholder, +.textarea::-ms-input-placeholder { + color: #3b404c; +} +.input::placeholder, +.textarea::placeholder { + color: #3b404c; +} +.input::-ms-input-placeholder, +.textarea::-ms-input-placeholder { + color: #3b404c; +} +.input:-ms-input-placeholder, +.textarea:-ms-input-placeholder { + color: #3b404c; +} +.input:hover, +.textarea:hover { + border-color: #191c21; +} +.input:active, +.input:focus, +.textarea:active, +.textarea:focus { + outline: none; + border-color: #242830; +} +.input[disabled], +.textarea[disabled] { + cursor: not-allowed; + background-color: #1d2026; + border-color: #1d2026; +} +.input { + -moz-appearance: none; + -webkit-appearance: none; + font-size: 16px; + letter-spacing: -0.1px; + line-height: 20px; + padding: 13px 16px; + height: 48px; + box-shadow: none; +} +.input .inline-input { + display: inline; + width: auto; +} +.textarea { + display: block; + min-width: 100%; + resize: vertical; +} +.textarea .inline-textarea { + display: inline; + width: auto; +} +.field-grouped > .control:not(:last-child) { + margin-bottom: 8px; +} +@media (min-width: 641px) { + .field-grouped { + display: flex; + } + .field-grouped > .control { + flex-shrink: 0; + } + .field-grouped > .control.control-expanded { + flex-grow: 1; + flex-shrink: 1; + } + .field-grouped > .control:not(:last-child) { + margin-bottom: 0; + margin-right: 8px; + } +} +.button { + display: inline-flex; + font-size: 14px; + letter-spacing: 0px; + font-weight: 600; + line-height: 16px; + text-decoration: none !important; + text-transform: uppercase; + background-color: #242830; + color: #fff !important; + border: none; + border-radius: 2px; + cursor: pointer; + justify-content: center; + padding: 16px 32px; + height: 48px; + text-align: center; + white-space: nowrap; +} +.button:hover { + background: #262a33; +} +.button:active { + outline: 0; +} +.button::before { + border-radius: 2px; +} +.button-sm { + padding: 8px 24px; + height: 32px; +} +.button-primary { + background: #097dea; + background: linear-gradient(65deg, #0270d7 0, #0f8afd 100%); +} +.button-primary:hover { + background: #0982f4; + background: linear-gradient(65deg, #0275e1 0, #198ffd 100%); +} +.button-block { + display: flex; +} +.button-block { + display: flex; + width: 100%; +} +@media (max-width: 640px) { + .button-wide-mobile { + width: 100%; + max-width: 280px; + } +} +.site-header { + padding: 24px 0; +} +.site-header-inner { + position: relative; + display: flex; + justify-content: space-between; + align-items: center; +} +.header-links { + display: inline-flex; +} +.header-links li { + display: inline-flex; +} +.header-links a:not(.button) { + font-size: 16px; + line-height: 24px; + letter-spacing: -0.1px; + font-weight: 600; + color: #8a94a7; + text-transform: uppercase; + text-decoration: none; + line-height: 16px; + padding: 8px 24px; +} +@media (min-width: 641px) { + .site-header { + position: relative; + } + .site-header::before { + content: ''; + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 700px; + background: #242830; + background: linear-gradient(80deg, rgba(36, 40, 48, 0.5) 0%, rgba(36, 40, 48, 0) 100%); + -webkit-transform-origin: 0; + transform-origin: 0; + -webkit-transform: skewY(-12deg); + transform: skewY(-12deg); + } +} +.hero { + text-align: center; + padding-top: 48px; + padding-bottom: 88px; +} +.hero-copy { + position: relative; + z-index: 1; +} +.hero-cta { + margin-bottom: 40px; +} +.hero-figure { + position: relative; +} +.hero-figure svg { + width: 100%; + height: auto; +} +.hero-figure::before, +.hero-figure::after { + content: ''; + position: absolute; + background-repeat: no-repeat; + background-size: 100%; +} +.has-animations .hero-figure::before, +.has-animations .hero-figure::after { + opacity: 0; + transition: opacity 2s ease; +} +.anime-ready .has-animations .hero-figure::before, +.anime-ready .has-animations .hero-figure::after { + opacity: 1; +} +.hero-figure::before { + top: -57.8%; + left: -1.3%; + width: 152.84%; + height: 178.78%; + background-image: url('../images/hero-back-illustration.svg'); +} +.hero-figure::after { + top: -35.6%; + left: 99.6%; + width: 57.2%; + height: 87.88%; + background-image: url('../images/hero-top-illustration.svg'); +} +.hero-figure-box { + position: absolute; + top: 0; + will-change: transform; +} +.hero-figure-box-01, +.hero-figure-box-02, +.hero-figure-box-03, +.hero-figure-box-04, +.hero-figure-box-08, +.hero-figure-box-09 { + overflow: hidden; +} +.hero-figure-box-01::before, +.hero-figure-box-02::before, +.hero-figure-box-03::before, +.hero-figure-box-04::before, +.hero-figure-box-08::before, +.hero-figure-box-09::before { + content: ''; + position: absolute; + top: 0; + bottom: 0; + left: 0; + right: 0; + -webkit-transform-origin: 100% 100%; + transform-origin: 100% 100%; +} +.hero-figure-box-01 { + left: 103.2%; + top: 41.9%; + width: 28.03%; + height: 37.37%; + background: linear-gradient(to left top, #00bffb, rgba(0, 191, 251, 0)); + -webkit-transform: rotateZ(45deg); + transform: rotateZ(45deg); +} +.hero-figure-box-01::before { + background: linear-gradient(to left, #15181d 0%, rgba(21, 24, 29, 0) 60%); + -webkit-transform: rotateZ(45deg) scale(1.5); + transform: rotateZ(45deg) scale(1.5); +} +.hero-figure-box-02 { + left: 61.3%; + top: 64.1%; + width: 37.87%; + height: 50.5%; + background: linear-gradient(to left top, #0270d7, rgba(2, 112, 215, 0)); + -webkit-transform: rotateZ(-45deg); + transform: rotateZ(-45deg); +} +.hero-figure-box-02::before { + background: linear-gradient(to top, #15181d 0%, rgba(21, 24, 29, 0) 60%); + -webkit-transform: rotateZ(-45deg) scale(1.5); + transform: rotateZ(-45deg) scale(1.5); +} +.hero-figure-box-03 { + left: 87.7%; + top: -56.8%; + width: 56.81%; + height: 75.75%; + background: linear-gradient(to left top, #00bffb, rgba(0, 191, 251, 0)); +} +.hero-figure-box-03::before { + background: linear-gradient(to left, #15181d 0%, rgba(21, 24, 29, 0) 60%); + -webkit-transform: rotateZ(45deg) scale(1.5); + transform: rotateZ(45deg) scale(1.5); +} +.hero-figure-box-04 { + left: 54.9%; + top: -8%; + width: 45.45%; + height: 60.6%; + background: linear-gradient(to left top, #0270d7, rgba(2, 112, 215, 0)); + -webkit-transform: rotateZ(-135deg); + transform: rotateZ(-135deg); +} +.hero-figure-box-04::before { + background: linear-gradient(to top, rgba(255, 255, 255, 0.24) 0%, rgba(255, 255, 255, 0) 60%); + -webkit-transform: rotateZ(-45deg) scale(1.5); + transform: rotateZ(-45deg) scale(1.5); +} +.hero-figure-box-05, +.hero-figure-box-06, +.hero-figure-box-07 { + background-color: #242830; + box-shadow: -20px 32px 64px rgba(0, 0, 0, 0.25); +} +.hero-figure-box-05 { + left: 17.4%; + top: 13.3%; + width: 64%; + height: 73.7%; + -webkit-transform: perspective(500px) rotateY(-15deg) rotateX(8deg) rotateZ(-1deg); + transform: perspective(500px) rotateY(-15deg) rotateX(8deg) rotateZ(-1deg); +} +.hero-figure-box-06 { + left: 65.5%; + top: 6.3%; + width: 30.3%; + height: 40.4%; + -webkit-transform: rotateZ(20deg); + transform: rotateZ(20deg); +} +.hero-figure-box-07 { + left: 1.9%; + top: 42.4%; + width: 12.12%; + height: 16.16%; + -webkit-transform: rotateZ(20deg); + transform: rotateZ(20deg); +} +.hero-figure-box-08 { + left: 27.1%; + top: 81.6%; + width: 19.51%; + height: 26.01%; + background: #0270d7; + -webkit-transform: rotateZ(-22deg); + transform: rotateZ(-22deg); +} +.hero-figure-box-08::before { + background: linear-gradient(to left, rgba(255, 255, 255, 0) 0%, rgba(255, 255, 255, 0.48) 100%); + -webkit-transform: rotateZ(45deg) scale(1.5); + transform: rotateZ(45deg) scale(1.5); +} +.hero-figure-box-09 { + left: 42.6%; + top: -17.9%; + width: 6.63%; + height: 8.83%; + background: #00bffb; + -webkit-transform: rotateZ(-52deg); + transform: rotateZ(-52deg); +} +.hero-figure-box-09::before { + background: linear-gradient(to left, rgba(255, 255, 255, 0) 0%, rgba(255, 255, 255, 0.64) 100%); + -webkit-transform: rotateZ(45deg) scale(1.5); + transform: rotateZ(45deg) scale(1.5); +} +.hero-figure-box-10 { + left: -3.8%; + top: 4.3%; + width: 3.03%; + height: 4.04%; + background: rgba(0, 191, 251, 0.32); + -webkit-transform: rotateZ(-50deg); + transform: rotateZ(-50deg); +} +@media (max-width: 640px) { + .hero-cta { + max-width: 280px; + margin-left: auto; + margin-right: auto; + } + .hero-cta .button { + display: flex; + } + .hero-cta .button + .button { + margin-top: 16px; + } + .hero-figure::after, + .hero-figure-box-03, + .hero-figure-box-04, + .hero-figure-box-09 { + display: none; + } +} +@media (min-width: 641px) { + .hero { + text-align: left; + padding-top: 64px; + padding-bottom: 88px; + } + .hero-inner { + display: flex; + justify-content: space-between; + align-items: center; + } + .hero-copy { + padding-right: 64px; + min-width: 552px; + width: 552px; + } + .hero-cta { + margin: 0; + } + .hero-cta .button { + min-width: 170px; + } + .hero-cta .button:first-child { + margin-right: 16px; + } + .hero-figure svg { + width: auto; + } +} +.features-wrap { + display: flex; + flex-wrap: wrap; + justify-content: space-evenly; + margin-right: -32px; + margin-left: -32px; +} +.features-wrap:first-of-type { + margin-top: -16px; +} +.features-wrap:last-of-type { + margin-bottom: -16px; +} +.feature { + padding: 16px 32px; + width: 380px; + max-width: 380px; + flex-grow: 1; +} +.feature-inner { + height: 100%; +} +.feature-icon { + display: flex; + justify-content: center; +} +@media (min-width: 641px) { + .features-wrap:first-of-type { + margin-top: -24px; + } + .features-wrap:last-of-type { + margin-bottom: -24px; + } + .feature { + padding: 32px 32px; + } +} +.pricing-header { + margin-bottom: 48px; +} +.pricing-tables-wrap { + display: flex; + flex-wrap: wrap; + justify-content: center; + margin-right: -12px; + margin-left: -12px; +} +.pricing-tables-wrap:first-child { + margin-top: -12px; +} +.pricing-tables-wrap:last-child { + margin-bottom: -12px; +} +.pricing-table { + position: relative; + padding: 12px; + width: 368px; + max-width: 368px; + flex-grow: 1; +} +.pricing-table::before { + content: ''; + position: absolute; + left: 50%; + width: 200%; + max-width: 200%; + height: 435px; + background-repeat: no-repeat; + background-position: center; + background-size: 100%; + bottom: 18.8%; + -webkit-transform: translateX(-50%); + transform: translateX(-50%); + background-image: url('../images/pricing-illustration.svg'); +} +.pricing-table-header, +.pricing-table-features-title, +.pricing-table-features li { + border-bottom: 1px solid rgba(138, 148, 167, 0.24); +} +.pricing-table-inner { + position: relative; + display: flex; + flex-wrap: wrap; + background: #2c3039; + padding: 24px; + height: 100%; +} +.pricing-table-inner > * { + position: relative; + width: 100%; +} +.pricing-table-inner::before { + content: ''; + position: absolute; + top: 0; + right: 0; + bottom: 0; + left: 0; + box-shadow: 0 24px 48px rgba(21, 24, 29, 0.24); + mix-blend-mode: multiply; +} +.pricing-table-price-currency { + color: #8a94a7; +} +.pricing-table-features-title { + color: #fff; + font-weight: 700; +} +.pricing-table-features li { + display: flex; + align-items: center; + padding: 14px 0; +} +.pricing-table-features li::before { + content: ''; + width: 16px; + height: 12px; + margin-right: 16px; + background-image: url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTYiIGhlaWdodD0iMTIiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PHBhdGggZD0iTTUuNiA2LjRMMS42IDQgMCA1LjYgNS42IDEyIDE2IDEuNiAxNC40IDB6IiBmaWxsPSIjMDJDQkIxIiBmaWxsLXJ1bGU9ImV2ZW5vZGQiLz48L3N2Zz4=); + background-repeat: no-repeat; +} +.pricing-table-cta { + align-self: flex-end; +} +@media (min-width: 641px) { + .pricing .section-paragraph { + padding-left: 90px; + padding-right: 90px; + } + .pricing-header { + margin-bottom: 52px; + } +} +.cta { + text-align: center; +} +.cta .section-inner { + padding: 48px 16px; +} +.cta .section-title { + margin-bottom: 40px; +} +.cta-inner { + position: relative; + background: #15181d; + overflow: hidden; +} +.cta-inner::before { + content: ''; + position: absolute; + right: 98px; + top: -117px; + width: 160px; + height: 187px; + background-image: url('../images/cta-illustration.svg'); + background-repeat: no-repeat; +} +.cta-inner > * { + position: relative; +} +@media (min-width: 641px) { + .cta { + text-align: left; + } + .cta .section-inner { + padding: 64px 32px; + } + .cta .section-title { + margin-bottom: 0; + padding-right: 24px; + } + .cta-inner { + display: flex; + align-items: center; + justify-content: space-between; + } +} +.is-boxed { + background: #242830; +} +.body-wrap { + background: #1d2026; + overflow: hidden; + display: flex; + flex-direction: column; + min-height: 100vh; +} +.boxed-container { + max-width: 1440px; + margin: 0 auto; + box-shadow: 0 24px 48px rgba(21, 24, 29, 0.24); + mix-blend-mode: multiply; + mix-blend-mode: normal; +} +main { + flex: 1 0 auto; +} +.section-inner { + position: relative; + padding-top: 48px; + padding-bottom: 48px; +} +@media (min-width: 641px) { + .section-inner { + padding-top: 88px; + padding-bottom: 88px; + } +} +.site-footer { + font-size: 14px; + line-height: 22px; + letter-spacing: 0px; +} +.site-footer a { + color: #8a94a7; + text-decoration: none; +} +.site-footer a:hover, +.site-footer a:active { + text-decoration: underline; +} +.site-footer-inner { + position: relative; + display: flex; + flex-wrap: wrap; + padding-top: 48px; + padding-bottom: 48px; +} +.footer-brand, +.footer-links, +.footer-social-links, +.footer-copyright { + flex: none; + width: 100%; + display: inline-flex; + justify-content: center; +} +.footer-brand, +.footer-links, +.footer-social-links { + margin-bottom: 24px; +} +.footer-social-links li { + display: inline-flex; +} +.footer-social-links li + li { + margin-left: 16px; +} +.footer-social-links li a { + padding: 8px; +} +.footer-links li + li { + margin-left: 24px; +} +@media (min-width: 641px) { + .site-footer { + margin-top: 20px; + } + .site-footer-inner { + justify-content: space-between; + padding-top: 64px; + padding-bottom: 64px; + } + .footer-brand, + .footer-links, + .footer-social-links, + .footer-copyright { + flex: 50%; + } + .footer-brand, + .footer-copyright { + justify-content: flex-start; + } + .footer-links, + .footer-social-links { + justify-content: flex-end; + } + .footer-links { + order: 1; + margin-bottom: 0; + } +} + +/*Pulse Button*/ +.pulse { + box-shadow: 0 0 0 rgba(120, 120, 120, 0.4); + animation: pulse 4s infinite; +} +.pulse:hover { + animation: none; +} + +@-webkit-keyframes pulse { + 0% { + -webkit-box-shadow: 0 0 0 0 rgba(120, 120, 120, 0.4); + } + 70% { + -webkit-box-shadow: 0 0 0 15px rgba(120, 120, 120, 0); + } + 100% { + -webkit-box-shadow: 0 0 0 0 rgba(120, 120, 120, 0); + } +} +@keyframes pulse { + 0% { + -moz-box-shadow: 0 0 0 0 rgba(120, 120, 120, 0.4); + box-shadow: 0 0 0 0 rgba(120, 120, 120, 0.4); + } + 70% { + -moz-box-shadow: 0 0 0 10px rgba(120, 120, 120, 0); + box-shadow: 0 0 0 15px rgba(120, 120, 120, 0); + } + 100% { + -moz-box-shadow: 0 0 0 0 rgba(120, 120, 120, 0); + box-shadow: 0 0 0 0 rgba(120, 120, 120, 0); + } +} +/*Pulse Button*/ + +.tiles-wrap { + display: flex; + flex-wrap: wrap; + justify-content: center; + margin-right: -12px; + margin-left: -12px; + margin-top: -12px; +} + +.tiles-wrap:last-of-type { + margin-bottom: -12px; +} + +.tiles-wrap:not(:last-of-type) { + margin-bottom: 12px; +} + +.tiles-wrap.push-left::after { + content: ''; + flex-basis: 330px; + max-width: 330px; + box-sizing: content-box; + padding-left: 12px; + padding-right: 12px; + height: 0; +} + +.tiles-item { + flex-basis: 330px; + max-width: 330px; + box-sizing: content-box; + padding: 12px; +} + +.tiles-item * { + box-sizing: border-box; +} + +.tiles-item-inner { + display: flex; + flex-wrap: wrap; + flex-direction: column; + height: 100%; + padding: 32px 24px; +} + +@media (min-width: 641px) { + .has-animations [class*='reveal-'] { + opacity: 0; + will-change: opacity, transform; + } + + .has-animations .reveal-from-top { + transform: translateY(-10px); + } + + .has-animations .reveal-from-bottom { + transform: translateY(10px); + } + + .has-animations .reveal-from-left { + transform: translateX(-10px); + } + + .has-animations .reveal-from-right { + transform: translateX(10px); + } + + .has-animations .reveal-scale-up { + transform: scale(0.95); + } + + .has-animations .reveal-scale-down { + transform: scale(1.05); + } + + .has-animations .reveal-rotate-from-left { + transform: perspective(1000px) rotateY(-45deg); + } + + .has-animations .reveal-rotate-from-right { + transform: perspective(1000px) rotateY(45deg); + } + + .has-animations.is-loaded [class*='reveal-'] { + transition: opacity 1s cubic-bezier(0.39, 0.575, 0.565, 1), transform 1s cubic-bezier(0.39, 0.575, 0.565, 1); + } + + .has-animations.is-loaded [class*='reveal-'].is-revealed { + opacity: 1; + transform: translate(0); + } +} + +.tiles-item-inner { + display: flex; + flex-wrap: wrap; + flex-direction: column; + height: 100%; + padding: 32px 24px; +} + +.features-tiles .tiles-item-inner { + padding-top: 0; + padding-bottom: 0; + padding-left: 0; + padding-right: 0; +} diff --git a/public/images/AGPLv3.png b/public/images/AGPLv3.png new file mode 100644 index 00000000..9bc4fc07 Binary files /dev/null and b/public/images/AGPLv3.png differ diff --git a/public/images/architecture.svg b/public/images/architecture.svg new file mode 100644 index 00000000..30a7023a --- /dev/null +++ b/public/images/architecture.svg @@ -0,0 +1,590 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
Host
+
+
+ Host +
+
+ + + + +
+
Worker 1
+
+
+ Worker 1 +
+
+ + + + +
+
WebRtcTransport
+
+
+ WebRtcTransport +
+
+ + + + + + +
+
Audio Producer
+
+
+ Audio Producer +
+
+ + + + +
+
Video Producer
+
+
+ Video Producer +
+
+ + + + + + + + +
+
PlainRtpTransport
+
+
+ PlainRtpTransport +
+
+ + + + +
+
Video Producer
+
+
+ Video Producer +
+
+ + + + + + +
+
PipeTransport
+
+
+ PipeTransport +
+
+ + + + +
+
Video Consumer
+
+
+ Video Consumer +
+
+ + + + + + + SRTP + + + + + + SRTP + + + + + +
+
Participant
+(mic/webcam on)
+
+
+
+ Participant<br>(mic/webcam on)<br> +
+
+ + + + +
+
Participant
+(viewer)
+
+
+
+ [Not supported by viewer] +
+
+ + + + + RTP + + + + + +
+
FFmpeg
+(recording)
+
+
+
+ [Not supported by viewer] +
+
+ + + + +
+
GStreamer
+(mp4 broadcaster)
+
+
+
+GStreamer<br>(mp4 broadcaster)<br> +
+
+ + + + +RTP + + + + + +
+
Router 2
+
+
+Router 2 +
+
+ + + + +
+
Router 1
+
+
+Router 1 +
+
+ + + + +
+
Worker 2
+
+
+Worker 2 +
+
+ + + + +
+
PipeTransport
+
+
+PipeTransport +
+
+ + + + +
+
Video Consumer
+
+
+Video Consumer +
+
+ + + + + + +
+
Router 3
+
+
+Router 3 +
+
+ + + + +
+
PipeTransport
+
+
+PipeTransport +
+
+ + + + +
+
Video Producer
+
+
+Video Producer +
+
+ + + + +
+
Worker 3
+
+
+Worker 3 +
+
+ + + + +
+
PipeTransport
+
+
+PipeTransport +
+
+ + + + +
+
Video Producer
+
+
+Video Producer +
+
+ + + + +
+
Router 4
+
+
+Router 4 +
+
+ + + + +RTP + + + + + +RTP + + + + + + + + + +
+
Participant
+(viewer)
+
+
+
+[Not supported by viewer] +
+
+ + + + +
+
Participant
+(viewer)
+
+
+
+[Not supported by viewer] +
+
+ + + + +SRTP + + + + + +SRTP + + + + + + + + + + + + + +SRTP + + + + + +
+
Participant
+(viewer)
+
+
+
+[Not supported by viewer] +
+
+ + + + +
+
WebRtcTransport
+
+
+WebRtcTransport +
+
+ + + + +
+
Video Consumer
+
+
+Video Consumer +
+
+ + + + +
+
WebRtcTransport
+
+
+WebRtcTransport +
+
+ + + + +
+
Video Consumer
+
+
+Video Consumer +
+
+ + + + +
+
WebRtcTransport
+
+
+WebRtcTransport +
+
+ + + + +
+
Video Consumer
+
+
+Video Consumer +
+
+ + + + +
+
WebRtcTransport
+
+
+WebRtcTransport +
+
+ + + + +
+
Video Consumer
+
+
+Video Consumer +
+
+ + + + +
+
Participant
+(viewer)
+
+
+
+[Not supported by viewer] +
+
+ + + + +SRTP + + + + + +
+
WebRtcTransport
+
+
+WebRtcTransport +
+
+ + + + +
+
Audio Consumer
+
+
+Audio Consumer +
+
+ + + + +
+
Video Consumer
+
+
+Video Consumer +
+
+ + + + +
+
PlainRtpTransport
+
+
+PlainRtpTransport +
+
+ + + + +
+
Audio Consumer
+
+
+Audio Consumer +
+
+
+
\ No newline at end of file diff --git a/public/images/background.jpg b/public/images/background.jpg new file mode 100644 index 00000000..e5a37200 Binary files /dev/null and b/public/images/background.jpg differ diff --git a/public/images/cta-illustration.svg b/public/images/cta-illustration.svg new file mode 100644 index 00000000..c46d2f37 --- /dev/null +++ b/public/images/cta-illustration.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/images/delete.png b/public/images/delete.png new file mode 100644 index 00000000..4d46760e Binary files /dev/null and b/public/images/delete.png differ diff --git a/public/images/feature-icon-01.svg b/public/images/feature-icon-01.svg new file mode 100644 index 00000000..60542994 --- /dev/null +++ b/public/images/feature-icon-01.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/images/feature-icon-02.svg b/public/images/feature-icon-02.svg new file mode 100644 index 00000000..62b11938 --- /dev/null +++ b/public/images/feature-icon-02.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/images/feature-icon-03.svg b/public/images/feature-icon-03.svg new file mode 100644 index 00000000..ab7bbaad --- /dev/null +++ b/public/images/feature-icon-03.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/images/feature-icon-04.svg b/public/images/feature-icon-04.svg new file mode 100644 index 00000000..1ca34e73 --- /dev/null +++ b/public/images/feature-icon-04.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/images/feature-icon-05.svg b/public/images/feature-icon-05.svg new file mode 100644 index 00000000..43b49046 --- /dev/null +++ b/public/images/feature-icon-05.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/images/feature-icon-06.svg b/public/images/feature-icon-06.svg new file mode 100644 index 00000000..3a0c70d7 --- /dev/null +++ b/public/images/feature-icon-06.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/images/github.png b/public/images/github.png new file mode 100644 index 00000000..a689f62e Binary files /dev/null and b/public/images/github.png differ diff --git a/public/images/hero-back-illustration.svg b/public/images/hero-back-illustration.svg new file mode 100644 index 00000000..8f0d1515 --- /dev/null +++ b/public/images/hero-back-illustration.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/images/hero-top-illustration.svg b/public/images/hero-top-illustration.svg new file mode 100644 index 00000000..3eec1021 --- /dev/null +++ b/public/images/hero-top-illustration.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/images/loader.gif b/public/images/loader.gif new file mode 100644 index 00000000..3f02eac1 Binary files /dev/null and b/public/images/loader.gif differ diff --git a/public/images/logo.svg b/public/images/logo.svg new file mode 100644 index 00000000..e2e655cb --- /dev/null +++ b/public/images/logo.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/images/miroslav-pejic.png b/public/images/miroslav-pejic.png new file mode 100644 index 00000000..5adec6df Binary files /dev/null and b/public/images/miroslav-pejic.png differ diff --git a/public/images/mirotalksfu.png b/public/images/mirotalksfu.png new file mode 100644 index 00000000..1e13d96f Binary files /dev/null and b/public/images/mirotalksfu.png differ diff --git a/public/images/pricing-illustration.svg b/public/images/pricing-illustration.svg new file mode 100644 index 00000000..982f9830 --- /dev/null +++ b/public/images/pricing-illustration.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/js/Room.js b/public/js/Room.js new file mode 100644 index 00000000..eee54652 --- /dev/null +++ b/public/js/Room.js @@ -0,0 +1,734 @@ +'use strict'; + +if (location.href.substr(0, 5) !== 'https') location.href = 'https' + location.href.substr(4, location.href.length - 4); + +const RoomURL = window.location.href; + +const swalBackground = 'rgba(0, 0, 0, 0.7)'; +const swalImageUrl = '../images/pricing-illustration.svg'; + +const url = { + ipLookup: 'https://extreme-ip-lookup.com/json/', +}; + +let rc = null; +let producer = null; + +let peer_name = 'peer_' + getRandomNumber(5); +let peer_geo = null; +let peer_info = null; + +let room_id = location.pathname.substring(6); +let isEnumerateDevices = false; + +let isAudioAllowed = false; +let isVideoAllowed = false; +let isAudioOn = true; +let isVideoOn = true; + +let recTimer = null; +let recElapsedTime = null; + +const socket = io(); + +function getRandomNumber(length) { + let result = ''; + let characters = '0123456789'; + let charactersLength = characters.length; + for (let i = 0; i < length; i++) { + result += characters.charAt(Math.floor(Math.random() * charactersLength)); + } + return result; +} + +function initClient() { + if (!DetectRTC.isMobileDevice) { + setTippy('sessionTime', 'Session time', 'bottom'); + setTippy('exitButton', 'Exit room', 'bottom'); + setTippy('shareButton', 'Share Room', 'bottom'); + setTippy('devicesButton', 'Devices', 'bottom'); + setTippy('chatButton', 'Chat', 'bottom'); + setTippy('chatCleanButton', 'Clean', 'bottom'); + setTippy('chatSaveButton', 'Save', 'bottom'); + setTippy('chatCloseButton', 'Close', 'bottom'); + setTippy('chatMessage', 'Press enter to send', 'top-start'); + setTippy('chatSendButton', 'Send', 'top'); + setTippy('chatEmojiButton', 'Emoji', 'top'); + setTippy('fullScreenButton', 'Full Screen', 'bottom'); + setTippy('recButton', 'Recording', 'bottom'); + setTippy('startRecButton', 'Start Recording', 'bottom'); + setTippy('stopRecButton', 'Stop Recording', 'bottom'); + setTippy('pauseRecButton', 'Pause Recording', 'bottom'); + setTippy('resumeRecButton', 'Resume Recording', 'bottom'); + setTippy('stopAudioButton', 'Stop Audio', 'bottom'); + setTippy('startAudioButton', 'Start Audio', 'bottom'); + setTippy('swapCameraButton', 'Swap Camera', 'bottom'); + setTippy('startVideoButton', 'Start Video', 'bottom'); + setTippy('stopVideoButton', 'Stop Video', 'bottom'); + setTippy('startScreenButton', 'Start Screen', 'bottom'); + setTippy('stopScreenButton', 'Stop Screen', 'bottom'); + setTippy('aboutButton', 'About', 'bottom'); + } + initEnumerateDevices(); +} + +function setTippy(elem, content, placement) { + tippy(document.getElementById(elem), { + content: content, + placement: placement, + }); +} + +// #################################################### +// ENUMERATE DEVICES +// #################################################### + +async function initEnumerateDevices() { + if (isEnumerateDevices) return; + console.log('01 ----> init Enumerate Devices'); + + // allow the audio + await navigator.mediaDevices + .getUserMedia({ audio: true }) + .then((stream) => { + enumerateAudioDevices(stream); + isAudioAllowed = true; + }) + .catch(() => { + isAudioAllowed = false; + }); + + // allow the video + await navigator.mediaDevices + .getUserMedia({ video: true }) + .then((stream) => { + enumerateVideoDevices(stream); + isVideoAllowed = true; + }) + .catch(() => { + isVideoAllowed = false; + }); + + if (!isAudioAllowed && !isVideoAllowed) { + window.location.href = `/permission?room_id=${room_id}&message=Not allowed both Audio and Video`; + } else { + getPeerInfo(); + getPeerGeoLocation(); + whoAreYou(); + } +} + +function enumerateAudioDevices(stream) { + console.log('02 ----> Get Audio Devices'); + navigator.mediaDevices + .enumerateDevices() + .then((devices) => + devices.forEach((device) => { + let el = null; + if ('audioinput' === device.kind) { + el = microphoneSelect; + } else if ('audiooutput' === device.kind) { + el = speakerSelect; + } + if (!el) return; + appenChild(device, el); + }), + ) + .then(() => { + stopTracks(stream); + isEnumerateDevices = true; + speakerSelect.disabled = !('sinkId' in HTMLMediaElement.prototype); + }); +} + +function enumerateVideoDevices(stream) { + console.log('03 ----> Get Video Devices'); + navigator.mediaDevices + .enumerateDevices() + .then((devices) => + devices.forEach((device) => { + let el = null; + if ('videoinput' === device.kind) { + el = videoSelect; + } + if (!el) return; + appenChild(device, el); + }), + ) + .then(() => { + stopTracks(stream); + isEnumerateDevices = true; + }); +} + +function stopTracks(stream) { + stream.getTracks().forEach((track) => { + track.stop(); + }); +} + +function appenChild(device, el) { + let option = document.createElement('option'); + option.value = device.deviceId; + option.innerText = device.label; + el.appendChild(option); +} + +// #################################################### +// SOME PEER INFO +// #################################################### + +function getPeerInfo() { + peer_info = { + detectRTCversion: DetectRTC.version, + isWebRTCSupported: DetectRTC.isWebRTCSupported, + isMobileDevice: DetectRTC.isMobileDevice, + osName: DetectRTC.osName, + osVersion: DetectRTC.osVersion, + browserName: DetectRTC.browser.name, + browserVersion: DetectRTC.browser.version, + }; +} + +function getPeerGeoLocation() { + fetch(url.ipLookup) + .then((res) => res.json()) + .then((outJson) => { + peer_geo = outJson; + }) + .catch((ex) => console.warn('IP Lookup', ex)); +} + +// #################################################### +// ENTER YOUR NAME | Enable/Disable AUDIO/VIDEO +// #################################################### + +function whoAreYou() { + console.log('04 ----> Who are you'); + + Swal.fire({ + allowOutsideClick: false, + allowEscapeKey: false, + background: swalBackground, + input: 'text', + inputPlaceholder: 'Enter your name', + html: `
+ + `, + confirmButtonText: `Join meeting`, + showClass: { + popup: 'animate__animated animate__fadeInDown', + }, + hideClass: { + popup: 'animate__animated animate__fadeOutUp', + }, + inputValidator: (name) => { + if (!name) return 'Please enter your name'; + peer_name = name; + }, + }).then(() => { + shareRoom(); + joinRoom(peer_name, room_id); + }); + + let initAudioButton = document.getElementById('initAudioButton'); + let initVideoButton = document.getElementById('initVideoButton'); + if (!isAudioAllowed) initAudioButton.className = 'hidden'; + if (!isVideoAllowed) initVideoButton.className = 'hidden'; + if (!DetectRTC.isMobileDevice) { + setTippy('initAudioButton', 'Enable / Disable audio', 'left'); + setTippy('initVideoButton', 'Enable / Disable video', 'right'); + } +} + +function handleAudio(e) { + isAudioOn ? (isAudioOn = false) : (isAudioOn = true); + e.target.className = 'fas fa-microphone' + (isAudioOn ? '' : '-slash'); +} + +function handleVideo(e) { + isVideoOn ? (isVideoOn = false) : (isVideoOn = true); + e.target.className = 'fas fa-video' + (isVideoOn ? '' : '-slash'); +} + +// #################################################### +// SHARE ROOM +// #################################################### + +async function shareRoom(useNavigator = false) { + if (navigator.share && useNavigator) { + try { + await navigator.share({ url: RoomURL }); + userLog('info', 'Room Shared successfully', 'top-end'); + } catch (err) { + share(); + } + } else { + share(); + } + function share() { + sound('open'); + + Swal.fire({ + background: swalBackground, + imageUrl: swalImageUrl, + position: 'center', + title: 'Hello ' + peer_name + '', + html: + ` +
+
+ +
+

+

Share this meeting invite others to join.

+

` + + RoomURL + + `

`, + showDenyButton: true, + showCancelButton: true, + confirmButtonText: `Copy meeting URL`, + denyButtonText: `Email invite`, + cancelButtonText: `Close`, + showClass: { + popup: 'animate__animated animate__fadeInUp', + }, + hideClass: { + popup: 'animate__animated animate__fadeOutUp', + }, + }).then((result) => { + if (result.isConfirmed) { + copyRoomURL(); + } else if (result.isDenied) { + let message = { + email: '', + subject: 'Please join our MiroTalkSfu Video Chat Meeting', + body: 'Click to join: ' + RoomURL, + }; + shareRoomByEmail(message); + } + }); + makeRoomQR(); + } +} + +// #################################################### +// ROOM UTILITY +// #################################################### + +function makeRoomQR() { + let qr = new QRious({ + element: document.getElementById('qrRoom'), + value: RoomURL, + }); + qr.set({ + size: 128, + }); +} + +function copyRoomURL() { + let tmpInput = document.createElement('input'); + document.body.appendChild(tmpInput); + tmpInput.value = RoomURL; + tmpInput.select(); + document.execCommand('copy'); + document.body.removeChild(tmpInput); + userLog('info', 'Room URL copied to clipboard', 'top-end'); +} + +function shareRoomByEmail(message) { + let email = message.email; + let subject = message.subject; + let emailBody = message.body; + document.location = 'mailto:' + email + '?subject=' + subject + '&body=' + emailBody; +} + +// #################################################### +// JOIN TO ROOM +// #################################################### + +function joinRoom(peer_name, room_id) { + if (rc && rc.isConnected()) { + console.log('Already connected to a room'); + } else { + console.log('05 ----> join to Room ' + room_id); + rc = new RoomClient( + localMedia, + remoteVideos, + remoteAudios, + window.mediasoupClient, + socket, + room_id, + peer_name, + peer_geo, + peer_info, + isAudioAllowed, + isVideoAllowed, + isAudioOn, + isVideoOn, + roomIsReady, + ); + handleRoomClientEvents(); + } +} + +function roomIsReady() { + control.className = ''; + show(exitButton); + show(shareButton); + show(recButton); + show(startRecButton); + show(chatButton); + show(chatSendButton); + show(chatEmojiButton); + if (DetectRTC.isMobileDevice) { + show(swapCameraButton); + setChatSize(); + } else { + rc.makeDraggable(chatRoom, chatHeader); + show(startScreenButton); + } + if (DetectRTC.browser.name != 'Safari') { + document.onfullscreenchange = () => { + if (!document.fullscreenElement) rc.isDocumentOnFullScreen = false; + }; + show(fullScreenButton); + } + show(devicesButton); + if (isAudioAllowed) show(startAudioButton); + if (isVideoAllowed) show(startVideoButton); + show(videoMedia); + show(aboutButton); + handleButtons(); + handleSelects(); + handleInputs(); + startSessionTimer(); +} + +function hide(elem) { + elem.className = 'hidden'; +} + +function show(elem) { + elem.className = ''; +} + +// #################################################### +// SET CHAT MOBILE +// #################################################### + +function setChatSize() { + document.documentElement.style.setProperty('--msger-width', '99%'); + document.documentElement.style.setProperty('--msger-height', '99%'); +} + +// #################################################### +// SESSION TIMER +// #################################################### + +function startSessionTimer() { + sessionTime.style.display = 'inline'; + let callStartTime = Date.now(); + setInterval(function printTime() { + let callElapsedTime = Date.now() - callStartTime; + sessionTime.innerHTML = getTimeToString(callElapsedTime); + }, 1000); +} + +function getTimeToString(time) { + let diffInHrs = time / 3600000; + let hh = Math.floor(diffInHrs); + let diffInMin = (diffInHrs - hh) * 60; + let mm = Math.floor(diffInMin); + let diffInSec = (diffInMin - mm) * 60; + let ss = Math.floor(diffInSec); + let formattedHH = hh.toString().padStart(2, '0'); + let formattedMM = mm.toString().padStart(2, '0'); + let formattedSS = ss.toString().padStart(2, '0'); + return `${formattedHH}:${formattedMM}:${formattedSS}`; +} + +// #################################################### +// RECORDING TIMER +// #################################################### + +function secondsToHms(d) { + d = Number(d); + let h = Math.floor(d / 3600); + let m = Math.floor((d % 3600) / 60); + let s = Math.floor((d % 3600) % 60); + let hDisplay = h > 0 ? h + 'h' : ''; + let mDisplay = m > 0 ? m + 'm' : ''; + let sDisplay = s > 0 ? s + 's' : ''; + return hDisplay + ' ' + mDisplay + ' ' + sDisplay; +} + +function startRecordingTimer() { + recElapsedTime = 0; + recTimer = setInterval(function printTime() { + if (rc.isRecording()) { + recElapsedTime++; + recordingStatus.innerHTML = '🔴 REC ' + secondsToHms(recElapsedTime); + } + }, 1000); +} +function stopRecordingTimer() { + clearInterval(recTimer); +} + +// #################################################### +// HTML BUTTONS +// #################################################### + +function handleButtons() { + exitButton.onclick = () => { + rc.exit(); + }; + shareButton.onclick = () => { + shareRoom(true); + }; + devicesButton.onclick = () => { + rc.toggleDevices(); + }; + chatButton.onclick = () => { + rc.toggleChat(); + }; + chatCleanButton.onclick = () => { + rc.chatClean(); + }; + chatSaveButton.onclick = () => { + rc.chatSave(); + }; + chatCloseButton.onclick = () => { + rc.toggleChat(); + }; + chatSendButton.onclick = () => { + rc.sendMessage(); + }; + chatEmojiButton.onclick = () => { + rc.toggleChatEmoji(); + }; + fullScreenButton.onclick = () => { + rc.toggleFullScreen(); + }; + recButton.onclick = () => { + rc.toggleRecording(); + }; + startRecButton.onclick = () => { + rc.startRecording(); + }; + stopRecButton.onclick = () => { + rc.stopRecording(); + }; + pauseRecButton.onclick = () => { + rc.pauseRecording(); + }; + resumeRecButton.onclick = () => { + rc.resumeRecording(); + }; + swapCameraButton.onclick = () => { + rc.closeThenProduce(RoomClient.mediaType.video, null, true); + }; + startAudioButton.onclick = () => { + rc.produce(RoomClient.mediaType.audio, microphoneSelect.value); + // rc.resumeProducer(RoomClient.mediaType.audio); + }; + stopAudioButton.onclick = () => { + rc.closeProducer(RoomClient.mediaType.audio); + // rc.pauseProducer(RoomClient.mediaType.audio); + }; + startVideoButton.onclick = () => { + rc.produce(RoomClient.mediaType.video, videoSelect.value); + // rc.resumeProducer(RoomClient.mediaType.video); + }; + stopVideoButton.onclick = () => { + rc.closeProducer(RoomClient.mediaType.video); + // rc.pauseProducer(RoomClient.mediaType.video); + }; + startScreenButton.onclick = () => { + rc.produce(RoomClient.mediaType.screen); + }; + stopScreenButton.onclick = () => { + rc.closeProducer(RoomClient.mediaType.screen); + }; + aboutButton.onclick = () => { + showAbout(); + }; +} + +// #################################################### +// HTML SELECTS +// #################################################### + +function handleSelects() { + microphoneSelect.onchange = () => { + rc.closeThenProduce(RoomClient.mediaType.audio, microphoneSelect.value); + }; + speakerSelect.onchange = () => { + rc.attachSinkId(localMedia, speakerSelect.value); + }; + videoSelect.onchange = () => { + rc.closeThenProduce(RoomClient.mediaType.video, videoSelect.value); + }; +} + +// #################################################### +// HTML INPUTS +// #################################################### + +function handleInputs() { + chatMessage.onkeyup = (e) => { + if (e.keyCode === 13) { + e.preventDefault(); + chatSendButton.click(); + } + }; + rc.getId('chatEmoji').addEventListener('emoji-click', (e) => { + chatMessage.value += e.detail.emoji.unicode; + rc.toggleChatEmoji(); + }); +} + +// #################################################### +// ROOM CLIENT EVENT LISTNERS +// #################################################### + +function handleRoomClientEvents() { + rc.on(RoomClient.EVENTS.startRec, () => { + hide(startRecButton); + show(stopRecButton); + show(pauseRecButton); + startRecordingTimer(); + }); + rc.on(RoomClient.EVENTS.pauseRec, () => { + hide(pauseRecButton); + show(resumeRecButton); + }); + rc.on(RoomClient.EVENTS.resumeRec, () => { + hide(resumeRecButton); + show(pauseRecButton); + }); + rc.on(RoomClient.EVENTS.stopRec, () => { + hide(stopRecButton); + hide(pauseRecButton); + hide(resumeRecButton); + show(startRecButton); + stopRecordingTimer(); + }); + rc.on(RoomClient.EVENTS.startAudio, () => { + hide(startAudioButton); + show(stopAudioButton); + }); + rc.on(RoomClient.EVENTS.pauseAudio, () => { + console.log('Room Client pause audio'); + hide(stopAudioButton); + show(startAudioButton); + }); + rc.on(RoomClient.EVENTS.resumeAudio, () => { + console.log('Room Client resume audio'); + hide(startAudioButton); + show(stopAudioButton); + }); + rc.on(RoomClient.EVENTS.stopAudio, () => { + hide(stopAudioButton); + show(startAudioButton); + }); + rc.on(RoomClient.EVENTS.startVideo, () => { + hide(startVideoButton); + show(stopVideoButton); + }); + rc.on(RoomClient.EVENTS.pauseVideo, () => { + console.log('Room Client pause video'); + hide(stopVideoButton); + show(startVideoButton); + }); + rc.on(RoomClient.EVENTS.resumeVideo, () => { + console.log('Room Client resume video'); + hide(startVideoButton); + show(stopVideoButton); + }); + rc.on(RoomClient.EVENTS.stopVideo, () => { + hide(stopVideoButton); + show(startVideoButton); + }); + rc.on(RoomClient.EVENTS.startScreen, () => { + hide(startScreenButton); + show(stopScreenButton); + }); + rc.on(RoomClient.EVENTS.pauseScreen, () => { + console.log('Room Client pause screen'); + }); + rc.on(RoomClient.EVENTS.resumeScreen, () => { + console.log('Room Client resume screen'); + }); + rc.on(RoomClient.EVENTS.stopScreen, () => { + hide(stopScreenButton); + show(startScreenButton); + }); + rc.on(RoomClient.EVENTS.exitRoom, () => { + window.location.href = '/newroom'; + }); +} + +// #################################################### +// SHOW LOG +// #################################################### + +function userLog(icon, message, position) { + const Toast = Swal.mixin({ + background: swalBackground, + toast: true, + position: position, + showConfirmButton: false, + timer: 3000, + }); + Toast.fire({ + icon: icon, + title: message, + }); +} + +// #################################################### +// SOUND +// #################################################### + +async function sound(name) { + let sound = '../sounds/' + name + '.wav'; + let audioToPlay = new Audio(sound); + try { + await audioToPlay.play(); + } catch (err) { + return false; + } +} + +// #################################################### +// ABOUT +// #################################################### + +function showAbout() { + sound('open'); + + Swal.fire({ + background: swalBackground, + imageUrl: swalImageUrl, + position: 'center', + html: ` +
+
+ Open Source project on +

+ mirotalksfu-github


+ +

+ Contact: Miroslav Pejic +
+ `, + showClass: { + popup: 'animate__animated animate__fadeInUp', + }, + hideClass: { + popup: 'animate__animated animate__fadeOutUp', + }, + }); +} diff --git a/public/js/RoomClient.js b/public/js/RoomClient.js new file mode 100644 index 00000000..d0be6595 --- /dev/null +++ b/public/js/RoomClient.js @@ -0,0 +1,1369 @@ +'use strict'; + +const cfg = { + msgAvatar: 'https://eu.ui-avatars.com/api', +}; + +const html = { + newline: '
', +}; + +const image = { + poster: '../images/loader.gif', + delete: '../images/delete.png', +}; + +const mediaType = { + audio: 'audioType', + video: 'videoType', + camera: 'cameraType', + screen: 'screenType', +}; + +const _EVENTS = { + openRoom: 'openRoom', + exitRoom: 'exitRoom', + startRec: 'startRec', + pauseRec: 'pauseRec', + resumeRec: 'resumeRec', + stopRec: 'stopRec', + startVideo: 'startVideo', + pauseVideo: 'pauseVideo', + resumeVideo: 'resumeVideo', + stopVideo: 'stopVideo', + startAudio: 'startAudio', + pauseAudio: 'pauseAudio', + resumeAudio: 'resumeAudio', + stopAudio: 'stopAudio', + startScreen: 'startScreen', + pauseScreen: 'pauseScreen', + resumeScreen: 'resumeScreen', + stopScreen: 'stopScreen', +}; + +let recordedBlobs; + +class RoomClient { + constructor( + localMediaEl, + remoteVideoEl, + remoteAudioEl, + mediasoupClient, + socket, + room_id, + peer_name, + peer_geo, + peer_info, + isAudioAllowed, + isVideoAllowed, + isAudioOn, + isVideoOn, + successCallback, + ) { + this.localMediaEl = localMediaEl; + this.remoteVideoEl = remoteVideoEl; + this.remoteAudioEl = remoteAudioEl; + this.mediasoupClient = mediasoupClient; + + this.socket = socket; + this.room_id = room_id; + this.peer_name = peer_name; + this.peer_geo = peer_geo; + this.peer_info = peer_info; + this.peer_info.peerName = peer_name; + + this.isAudioAllowed = isAudioAllowed; + this.isVideoAllowed = isVideoAllowed; + this.isAudioOn = isAudioOn; + this.isVideoOn = isVideoOn; + this.producerTransport = null; + this.consumerTransport = null; + this.device = null; + + this.isMobileDevice = DetectRTC.isMobileDevice; + + this._isConnected = false; + this.peerGeo = null; + this.peerInfo = null; + this.isVideoOnFullScreen = false; + this.isDocumentOnFullScreen = false; + this.isChatOpen = false; + this.camVideo = false; + this.camera = 'user'; + + this.chatMessages = []; + this.leftMsgAvatar = null; + this.rightMsgAvatar = null; + + this.localVideoStream = null; + this.localScreenStream = null; + this.localAudioStream = null; + this.mediaRecorder = null; + this.recScreenStream = null; + this._isRecording = false; + + this.connectedRoom = null; + this.debug = false; + + this.consumers = new Map(); + this.producers = new Map(); + this.producerLabel = new Map(); + this.eventListeners = new Map(); + + console.log('06 ----> Load Mediasoup Client v', mediasoupClient.version); + + Object.keys(_EVENTS).forEach( + function (evt) { + this.eventListeners.set(evt, []); + }.bind(this), + ); + + this.socket.request = function request(type, data = {}) { + return new Promise((resolve, reject) => { + socket.emit(type, data, (data) => { + if (data.error) { + reject(data.error); + } else { + resolve(data); + } + }); + }); + }; + + // #################################################### + // CREATE ROOM AND JOIN + // #################################################### + + this.createRoom(this.room_id).then( + async function () { + let data = { + room_id: this.room_id, + peer_name: this.peer_name, + peer_audio: this.isAudioOn, + peer_video: this.isVideoOn, + peer_info: this.peer_info, + peer_geo: this.peer_geo, + }; + await this.join(data); + this.initSockets(); + this._isConnected = true; + successCallback(); + }.bind(this), + ); + } + + // #################################################### + // GET STARTED + // #################################################### + + async createRoom(room_id) { + await this.socket + .request('createRoom', { + room_id, + }) + .catch((err) => { + console.log('Create room error:', err); + }); + } + + async join(data) { + socket + .request('join', data) + .then( + async function (room) { + this.connectedRoom = room; + console.log('07 ----> Joined to room', this.connectedRoom); + const data = await this.socket.request('getRouterRtpCapabilities'); + this.device = await this.loadDevice(data); + console.log('08 ----> Get Router Rtp Capabilities codecs: ', this.device.rtpCapabilities.codecs); + await this.initTransports(this.device); + this.startLocalMedia(); + this.socket.emit('getProducers'); + }.bind(this), + ) + .catch((err) => { + console.log('Join error:', err); + }); + } + + async loadDevice(routerRtpCapabilities) { + let device; + try { + device = new this.mediasoupClient.Device(); + } catch (error) { + if (error.name === 'UnsupportedError') { + console.error('Browser not supported'); + this.userLog('error', 'Browser not supported', 'center'); + } + console.error('Browser not supported: ', error); + this.userLog('error', 'Browser not supported: ' + error, 'center'); + } + await device.load({ + routerRtpCapabilities, + }); + return device; + } + + // #################################################### + // PRODUCER TRANSPORT + // #################################################### + + async initTransports(device) { + { + const data = await this.socket.request('createWebRtcTransport', { + forceTcp: false, + rtpCapabilities: device.rtpCapabilities, + }); + + if (data.error) { + console.error('Create WebRtc Transport for Producer err: ', data.error); + return; + } + + this.producerTransport = device.createSendTransport(data); + this.producerTransport.on( + 'connect', + async function ({ dtlsParameters }, callback, errback) { + this.socket + .request('connectTransport', { + dtlsParameters, + transport_id: data.id, + }) + .then(callback) + .catch(errback); + }.bind(this), + ); + + this.producerTransport.on( + 'produce', + async function ({ kind, rtpParameters }, callback, errback) { + try { + const { producer_id } = await this.socket.request('produce', { + producerTransportId: this.producerTransport.id, + kind, + rtpParameters, + }); + callback({ + id: producer_id, + }); + } catch (err) { + errback(err); + } + }.bind(this), + ); + + this.producerTransport.on( + 'connectionstatechange', + function (state) { + switch (state) { + case 'connecting': + break; + + case 'connected': + console.log('Producer Transport connected'); + break; + + case 'failed': + console.warn('Producer Transport failed'); + this.producerTransport.close(); + break; + + default: + break; + } + }.bind(this), + ); + } + + // #################################################### + // CONSUMER TRANSPORT + // #################################################### + + { + const data = await this.socket.request('createWebRtcTransport', { + forceTcp: false, + }); + + if (data.error) { + console.error('Create WebRtc Transport for Consumer err: ', data.error); + return; + } + + this.consumerTransport = device.createRecvTransport(data); + this.consumerTransport.on( + 'connect', + function ({ dtlsParameters }, callback, errback) { + this.socket + .request('connectTransport', { + transport_id: this.consumerTransport.id, + dtlsParameters, + }) + .then(callback) + .catch(errback); + }.bind(this), + ); + + this.consumerTransport.on( + 'connectionstatechange', + async function (state) { + switch (state) { + case 'connecting': + break; + + case 'connected': + console.log('Consumer Transport connected'); + break; + + case 'failed': + console.warn('Consumer Transport failed'); + this.consumerTransport.close(); + break; + + default: + break; + } + }.bind(this), + ); + } + } + + // #################################################### + // TODO DATACHANNEL TRANSPORT + // #################################################### + + + // #################################################### + // SOCKET ON + // #################################################### + + initSockets() { + this.socket.on( + 'consumerClosed', + function ({ consumer_id }) { + console.log('Closing consumer:', consumer_id); + this.removeConsumer(consumer_id); + }.bind(this), + ); + + this.socket.on( + 'newProducers', + async function (data) { + if (data.length > 0) { + console.log('New producers', data); + for (let { producer_id, peer_name, peer_info } of data) { + await this.consume(producer_id, peer_name, peer_info); + } + } + }.bind(this), + ); + + this.socket.on( + 'message', + function (data) { + console.log('message', data); + this.showMessage(data); + }.bind(this), + ); + + this.socket.on( + 'disconnect', + function () { + this.exit(true); + }.bind(this), + ); + } + + // #################################################### + // START LOCAL AUDIO VIDEO MEDIA + // #################################################### + + startLocalMedia() { + if (this.isAudioAllowed && this.isAudioOn) { + console.log('09 ----> Start audio media'); + this.produce(mediaType.audio, microphoneSelect.value); + } + if (this.isVideoAllowed && this.isVideoOn) { + console.log('10 ----> Start video media'); + this.produce(mediaType.video, videoSelect.value); + } + } + + // #################################################### + // PRODUCER + // #################################################### + + async produce(type, deviceId = null, swapCamera = false) { + let mediaConstraints = {}; + let audio = false; + let screen = false; + switch (type) { + case mediaType.audio: + mediaConstraints = this.getAudioConstraints(deviceId); + audio = true; + break; + case mediaType.video: + if (swapCamera) { + mediaConstraints = this.getCameraConstraints(); + } else { + mediaConstraints = this.getVideoConstraints(deviceId); + } + break; + case mediaType.screen: + mediaConstraints = this.getScreenConstraints(); + screen = true; + break; + default: + return; + } + if (!this.device.canProduce('video') && !audio) { + console.error('Cannot produce video'); + return; + } + if (this.producerLabel.has(type)) { + console.log('Producer already exists for this type ' + type); + return; + } + console.log('Mediacontraints:', mediaConstraints); + let stream; + try { + stream = screen + ? await navigator.mediaDevices.getDisplayMedia() + : await navigator.mediaDevices.getUserMedia(mediaConstraints); + console.log('Supported Constraints', navigator.mediaDevices.getSupportedConstraints()); + + const track = audio ? stream.getAudioTracks()[0] : stream.getVideoTracks()[0]; + const params = { + track, + }; + + if (!audio && !screen) { + params.encodings = this.getEncoding(); + params.codecOptions = { + videoGoogleStartBitrate: 1000, + }; + } + producer = await this.producerTransport.produce(params); + + console.log('Producer', producer); + + this.producers.set(producer.id, producer); + + let elem; + if (!audio) { + this.localVideoStream = stream; + elem = await this.handleProducer(producer.id, type, stream); + } else { + this.localAudioStream = stream; + } + + producer.on('trackended', () => { + this.closeProducer(type); + }); + + producer.on('transportclose', () => { + console.log('Producer transport close'); + if (!audio) { + elem.srcObject.getTracks().forEach(function (track) { + track.stop(); + }); + elem.parentNode.removeChild(elem); + } + this.producers.delete(producer.id); + }); + + producer.on('close', () => { + console.log('Closing producer'); + if (!audio) { + elem.srcObject.getTracks().forEach(function (track) { + track.stop(); + }); + elem.parentNode.removeChild(elem); + } + this.producers.delete(producer.id); + }); + + this.producerLabel.set(type, producer.id); + + switch (type) { + case mediaType.audio: + this.event(_EVENTS.startAudio); + break; + case mediaType.video: + this.event(_EVENTS.startVideo); + break; + case mediaType.screen: + this.event(_EVENTS.startScreen); + break; + default: + return; + } + } catch (err) { + console.log('Produce error:', err); + } + } + + getAudioConstraints(deviceId) { + return { + audio: { + echoCancellation: true, + noiseSuppression: true, + sampleRate: 44100, + deviceId: deviceId, + }, + video: false, + }; + } + + getCameraConstraints() { + this.camera = this.camera == 'user' ? 'environment' : 'user'; + if (this.camera != 'user') this.camVideo = { facingMode: { exact: this.camera } }; + else this.camVideo = true; + return { + audio: false, + video: this.camVideo, + }; + } + + getVideoConstraints(deviceId) { + return { + audio: false, + video: { + width: { + min: 640, + ideal: 1920, + max: 3840, + }, + height: { + min: 480, + ideal: 1080, + max: 2160, + }, + deviceId: deviceId, + aspectRatio: 1.777, // 16:9 + frameRate: 15, // 15fps + }, + }; + } + + getScreenConstraints() { + return { + video: { + frameRate: { + min: 5, + ideal: 15, + max: 30, + }, + }, + }; + } + + getEncoding() { + return [ + { + rid: 'r0', + maxBitrate: 100000, + scalabilityMode: 'S1T3', + }, + { + rid: 'r1', + maxBitrate: 300000, + scalabilityMode: 'S1T3', + }, + { + rid: 'r2', + maxBitrate: 900000, + scalabilityMode: 'S1T3', + }, + ]; + } + + closeThenProduce(type, deviceId, swapCamera = false) { + this.closeProducer(type); + this.produce(type, deviceId, swapCamera); + } + + async handleProducer(id, type, stream) { + let elem, d, p; + d = document.createElement('div'); + d.className = 'd'; + d.id = id + '_d'; + elem = document.createElement('video'); + elem.id = id; + elem.playsinline = false; + elem.autoplay = true; + elem.poster = image.poster; + if (this.isMobileDevice || type === mediaType.screen) elem.className = 'vid'; + else elem.className = 'vid mirror'; + p = document.createElement('p'); + p.id = id + '_name'; + p.className = 'pn'; + p.innerHTML = '👤 ' + this.peer_name; + d.appendChild(elem); + d.appendChild(p); + this.localMediaEl.appendChild(d); + this.attachMediaStream(elem, stream, type, 'Producer'); + this.handleFS(elem.id); + this.setTippy(elem.id, 'Full Screen', 'top-end'); + if (this.debug) { + this.setTippy( + p.id, + JSON.stringify( + this.peer_info, + ['peerName', 'isMobileDevice', 'osName', 'osVersion', 'browserName', 'browserVersion'], + 2, + ), + 'top-start', + ); + } + this.sound('joined'); + return elem; + } + + pauseProducer(type) { + if (!this.producerLabel.has(type)) { + console.log('There is no producer for this type ' + type); + return; + } + + let producer_id = this.producerLabel.get(type); + this.producers.get(producer_id).pause(); + + switch (type) { + case mediaType.audio: + this.event(_EVENTS.pauseAudio); + break; + case mediaType.video: + this.event(_EVENTS.pauseVideo); + break; + case mediaType.screen: + this.event(_EVENTS.pauseScreen); + break; + default: + return; + } + } + + resumeProducer(type) { + if (!this.producerLabel.has(type)) { + console.log('There is no producer for this type ' + type); + return; + } + + let producer_id = this.producerLabel.get(type); + this.producers.get(producer_id).resume(); + + switch (type) { + case mediaType.audio: + this.event(_EVENTS.resumeAudio); + break; + case mediaType.video: + this.event(_EVENTS.resumeVideo); + break; + case mediaType.screen: + this.event(_EVENTS.resumeScreen); + break; + default: + return; + } + } + + closeProducer(type) { + if (!this.producerLabel.has(type)) { + console.log('There is no producer for this type ' + type); + return; + } + + let producer_id = this.producerLabel.get(type); + console.log('Close producer', producer_id); + + this.socket.emit('producerClosed', { + producer_id, + }); + + this.producers.get(producer_id).close(); + this.producers.delete(producer_id); + this.producerLabel.delete(type); + + if (type !== mediaType.audio) { + let elem = this.getId(producer_id); + let d = this.getId(producer_id + '_d'); + elem.srcObject.getTracks().forEach(function (track) { + track.stop(); + }); + d.parentNode.removeChild(d); + } + + switch (type) { + case mediaType.audio: + this.event(_EVENTS.stopAudio); + break; + case mediaType.video: + this.event(_EVENTS.stopVideo); + break; + case mediaType.screen: + this.event(_EVENTS.stopScreen); + break; + default: + return; + } + + this.sound('left'); + } + + // #################################################### + // CONSUMER + // #################################################### + + async consume(producer_id, peer_name, peer_info) { + this.getConsumeStream(producer_id).then( + function ({ consumer, stream, kind }) { + this.consumers.set(consumer.id, consumer); + + if (kind === 'video') { + this.handleConsumer(consumer.id, mediaType.video, stream, peer_name, peer_info); + } else { + this.handleConsumer(consumer.id, mediaType.audio, stream, peer_name, peer_info); + } + + consumer.on( + 'trackended', + function () { + this.removeConsumer(consumer.id); + }.bind(this), + ); + + consumer.on( + 'transportclose', + function () { + this.removeConsumer(consumer.id); + }.bind(this), + ); + }.bind(this), + ); + } + + async getConsumeStream(producerId) { + const { rtpCapabilities } = this.device; + const data = await this.socket.request('consume', { + rtpCapabilities, + consumerTransportId: this.consumerTransport.id, + producerId, + }); + const { id, kind, rtpParameters } = data; + const codecOptions = {}; + const consumer = await this.consumerTransport.consume({ + id, + producerId, + kind, + rtpParameters, + codecOptions, + }); + const stream = new MediaStream(); + stream.addTrack(consumer.track); + + return { + consumer, + stream, + kind, + }; + } + + handleConsumer(id, type, stream, peer_name, peer_info) { + let elem, d, p; + switch (type) { + case mediaType.video: + d = document.createElement('div'); + d.className = 'd'; + d.id = id + '_d'; + elem = document.createElement('video'); + elem.id = id; + elem.playsinline = false; + elem.autoplay = true; + elem.className = 'vid'; + elem.poster = image.poster; + p = document.createElement('p'); + p.id = id + '_name'; + p.innerHTML = '👤 ' + peer_name; + d.appendChild(elem); + d.appendChild(p); + this.remoteVideoEl.appendChild(d); + this.attachMediaStream(elem, stream, mediaType.video, 'Consumer'); + this.handleFS(elem.id); + this.setTippy(elem.id, 'Full Screen', 'top-end'); + if (this.debug) { + this.setTippy( + p.id, + JSON.stringify( + peer_info, + ['peerName', 'isMobileDevice', 'osName', 'osVersion', 'browserName', 'browserVersion'], + 2, + ), + 'top-start', + ); + } + this.sound('joined'); + break; + case mediaType.audio: + elem = document.createElement('audio'); + elem.id = id; + elem.playsinline = false; + elem.autoplay = true; + this.remoteAudioEl.appendChild(elem); + this.attachMediaStream(elem, stream, mediaType.audio, 'Consumer'); + break; + } + return elem; + } + + removeConsumer(consumer_id) { + let elem = this.getId(consumer_id); + let d = this.getId(consumer_id + '_d'); + elem.srcObject.getTracks().forEach(function (track) { + track.stop(); + }); + if (d) d.parentNode.removeChild(d); + + this.consumers.delete(consumer_id); + this.sound('left'); + } + + // #################################################### + // EXIT ROOM + // #################################################### + + exit(offline = false) { + let clean = function () { + this._isConnected = false; + this.consumerTransport.close(); + this.producerTransport.close(); + this.socket.off('disconnect'); + this.socket.off('newProducers'); + this.socket.off('consumerClosed'); + }.bind(this); + + if (!offline) { + this.socket + .request('exitRoom') + .then((e) => console.log('Exit Room', e)) + .catch((e) => console.warn('Exit Room ', e)) + .finally( + function () { + clean(); + }.bind(this), + ); + } else { + clean(); + } + + this.event(_EVENTS.exitRoom); + } + + // #################################################### + // HELPERS + // #################################################### + + attachMediaStream(elem, stream, mediaType, type) { + console.log(type + ' Success attached media ' + mediaType); + elem.srcObject = stream; + } + + attachSinkId(elem, sinkId) { + if (typeof elem.sinkId !== 'undefined') { + elem.setSinkId(sinkId) + .then(() => { + console.log(`Success, audio output device attached: ${sinkId}`); + }) + .catch((err) => { + let errorMessage = err; + if (err.name === 'SecurityError') + errorMessage = `You need to use HTTPS for selecting audio output device: ${err}`; + console.error('Attach SinkId error: ', errorMessage); + this.userLog('error', errorMessage, 'top-end'); + this.getId('speakerSelect').selectedIndex = 0; + }); + } else { + let error = `Browser seems doesn't support output device selection.`; + console.warn(error); + this.userLog('error', error, 'top-end'); + } + } + + event(evt) { + if (this.eventListeners.has(evt)) { + this.eventListeners.get(evt).forEach((callback) => callback()); + } + } + + on(evt, callback) { + this.eventListeners.get(evt).push(callback); + } + + // #################################################### + // SET + // #################################################### + + setTippy(elem, content, placement) { + if (DetectRTC.isMobileDevice) return; + tippy(this.getId(elem), { + content: content, + placement: placement, + }); + } + + // #################################################### + // GET + // #################################################### + + isConnected() { + return this._isConnected; + } + + isRecording() { + return this._isRecording; + } + + static get mediaType() { + return mediaType; + } + + static get EVENTS() { + return _EVENTS; + } + + getTimeNow() { + return new Date().toTimeString().split(' ')[0]; + } + + getId(id) { + return document.getElementById(id); + } + + async getMyRoomInfo() { + let roomInfo = await this.socket.request('getMyRoomInfo'); + console.log('Room info', roomInfo); + return roomInfo; + } + + // #################################################### + // UTILITY + // #################################################### + + toggleDevices() { + this.getId('myDevices').classList.toggle('show'); + } + + async sound(name) { + let sound = '../sounds/' + name + '.wav'; + let audioToPlay = new Audio(sound); + try { + await audioToPlay.play(); + } catch (err) { + return false; + } + } + + userLog(icon, message, position) { + const Toast = Swal.mixin({ + background: swalBackground, + toast: true, + position: position, + showConfirmButton: false, + timer: 5000, + }); + Toast.fire({ + icon: icon, + title: message, + }); + } + + thereIsConsumers() { + if (this.consumers.size > 0) { + return true; + } + this.userLog('info', 'No participants in the room', 'top-end'); + return false; + } + + // #################################################### + // FULL SCREEN + // #################################################### + + toggleFullScreen() { + if (!document.fullscreenElement) { + document.documentElement.requestFullscreen(); + this.isDocumentOnFullScreen = true; + } else { + if (document.exitFullscreen) { + document.exitFullscreen(); + this.isDocumentOnFullScreen = false; + } + } + } + + handleFS(id) { + let videoPlayer = this.getId(id); + videoPlayer.addEventListener('fullscreenchange', (e) => { + if (videoPlayer.controls || this.isDocumentOnFullScreen) return; + let fullscreenElement = document.fullscreenElement; + if (!fullscreenElement) { + videoPlayer.style.pointerEvents = 'auto'; + this.isVideoOnFullScreen = false; + } + }); + videoPlayer.addEventListener('webkitfullscreenchange', (e) => { + if (videoPlayer.controls || this.isDocumentOnFullScreen) return; + let webkitIsFullScreen = document.webkitIsFullScreen; + if (!webkitIsFullScreen) { + videoPlayer.style.pointerEvents = 'auto'; + this.isVideoOnFullScreen = false; + } + }); + videoPlayer.addEventListener('click', () => { + if (videoPlayer.controls || this.isDocumentOnFullScreen) return; + if (!this.isVideoOnFullScreen) { + if (videoPlayer.requestFullscreen) { + videoPlayer.requestFullscreen(); + } else if (videoPlayer.webkitRequestFullscreen) { + videoPlayer.webkitRequestFullscreen(); + } else if (videoPlayer.msRequestFullscreen) { + videoPlayer.msRequestFullscreen(); + } + this.isVideoOnFullScreen = true; + videoPlayer.style.pointerEvents = 'none'; + } else { + if (document.exitFullscreen) { + document.exitFullscreen(); + } else if (document.webkitCancelFullScreen) { + document.webkitCancelFullScreen(); + } else if (document.msExitFullscreen) { + document.msExitFullscreen(); + } + this.isVideoOnFullScreen = false; + videoPlayer.style.pointerEvents = 'auto'; + } + }); + } + + // #################################################### + // DRAGGABLE + // #################################################### + + makeDraggable(elmnt, dragObj) { + let pos1 = 0, + pos2 = 0, + pos3 = 0, + pos4 = 0; + if (dragObj) { + dragObj.onmousedown = dragMouseDown; + } else { + elmnt.onmousedown = dragMouseDown; + } + function dragMouseDown(e) { + e = e || window.event; + e.preventDefault(); + pos3 = e.clientX; + pos4 = e.clientY; + document.onmouseup = closeDragElement; + document.onmousemove = elementDrag; + } + function elementDrag(e) { + e = e || window.event; + e.preventDefault(); + pos1 = pos3 - e.clientX; + pos2 = pos4 - e.clientY; + pos3 = e.clientX; + pos4 = e.clientY; + elmnt.style.top = elmnt.offsetTop - pos2 + 'px'; + elmnt.style.left = elmnt.offsetLeft - pos1 + 'px'; + } + function closeDragElement() { + document.onmouseup = null; + document.onmousemove = null; + } + } + + // #################################################### + // CHAT + // #################################################### + + toggleChat() { + let chatRoom = this.getId('chatRoom'); + if (this.isChatOpen == false) { + chatRoom.style.display = 'block'; + chatRoom.style.top = '50%'; + chatRoom.style.left = '50%'; + this.sound('open'); + this.isChatOpen = true; + } else { + chatRoom.style.display = 'none'; + this.isChatOpen = false; + } + } + + toggleChatEmoji() { + this.getId('chatEmoji').classList.toggle('show'); + } + + sendMessage() { + if (!this.thereIsConsumers()) { + chatMessage.value = ''; + return; + } + let peer_msg = this.formatMsg(chatMessage.value); + if (!peer_msg) return; + let data = { + peer_name: this.peer_name, + peer_msg: peer_msg, + }; + this.socket.emit('message', data); + this.setMsgAvatar('right', this.peer_name); + this.appendMessage('right', this.rightMsgAvatar, this.peer_name, peer_msg); + chatMessage.value = ''; + } + + showMessage(data) { + if (!this.isChatOpen) this.toggleChat(); + this.setMsgAvatar('left', data.peer_name); + this.appendMessage('left', this.leftMsgAvatar, data.peer_name, data.peer_msg); + this.sound('message'); + } + + setMsgAvatar(avatar, peerName) { + let avatarImg = cfg.msgAvatar + '?name=' + peerName + '&size=32' + '&background=random&rounded=true'; + avatar === 'left' ? (this.leftMsgAvatar = avatarImg) : (this.rightMsgAvatar = avatarImg); + } + + appendMessage(side, img, from, msg) { + let time = this.getTimeNow(); + let msgHTML = ` +
+
+
+
+
${from}
+
${time}
+
+
${msg}
+
+
+ `; + this.collectMessages(time, from, msg); + chatMsger.insertAdjacentHTML('beforeend', msgHTML); + chatMsger.scrollTop += 500; + } + + formatMsg(message) { + let urlRegex = /(https?:\/\/[^\s]+)/g; + return message.replace(urlRegex, (url) => { + if (message.match(/\.(jpeg|jpg|gif|png|tiff|bmp)$/)) + return 'img'; + return '' + url + ''; + }); + } + + collectMessages(time, from, msg) { + this.chatMessages.push({ + time: time, + from: from, + msg: msg, + }); + } + + chatClean() { + Swal.fire({ + background: swalBackground, + position: 'center', + title: 'Clean up chat Messages?', + imageUrl: image.delete, + showDenyButton: true, + confirmButtonText: `Yes`, + denyButtonText: `No`, + showClass: { + popup: 'animate__animated animate__fadeInDown', + }, + hideClass: { + popup: 'animate__animated animate__fadeOutUp', + }, + }).then((result) => { + if (result.isConfirmed) { + let msgs = chatMsger.firstChild; + while (msgs) { + chatMsger.removeChild(msgs); + msgs = chatMsger.firstChild; + } + this.chatMessages = []; + } + }); + } + + chatSave() { + if (this.chatMessages.length === 0) { + userLog('info', 'No chat messages to save', 'top-end'); + return; + } + + const newDate = new Date(); + const date = newDate.toISOString().split('T')[0]; + const time = newDate.toTimeString().split(' ')[0]; + + let a = document.createElement('a'); + a.href = 'data:text/json;charset=utf-8,' + encodeURIComponent(JSON.stringify(this.chatMessages, null, 1)); + a.download = `${date}-${time}` + '-CHAT.txt'; + document.body.appendChild(a); + a.click(); + setTimeout(() => { + document.body.removeChild(a); + window.URL.revokeObjectURL(url); + }, 100); + } + + // #################################################### + // RECORDING + // #################################################### + + toggleRecording() { + this.getId('recording').classList.toggle('show'); + } + + getSupportedMimeTypes() { + const possibleTypes = [ + 'video/webm;codecs=vp9,opus', + 'video/webm;codecs=vp8,opus', + 'video/webm;codecs=h264,opus', + 'video/mp4;codecs=h264,aac', + 'video/mp4', + ]; + return possibleTypes.filter((mimeType) => { + return MediaRecorder.isTypeSupported(mimeType); + }); + } + + startRecording() { + recordedBlobs = []; + let options = this.getSupportedMimeTypes(); + console.log('MediaRecorder supported options', options); + options = { mimeType: options[0] }; + try { + if (this.isMobileDevice) { + // on mobile devices recording camera + audio + let newStream = this.getNewStream(this.localVideoStream, this.localAudioStream); + this.mediaRecorder = new MediaRecorder(newStream, options); + console.log('Created MediaRecorder', this.mediaRecorder, 'with options', options); + this.getId('swapCameraButton').className = 'hidden'; + this._isRecording = true; + this.handleMediaRecorder(); + this.event(_EVENTS.startRec); + this.sound('recStart'); + } else { + // on desktop devices recording screen/window... + audio + const constraints = { video: true }; + navigator.mediaDevices + .getDisplayMedia(constraints) + .then((screenStream) => { + this.recScreenStream = this.getNewStream(screenStream, this.localAudioStream); + this.mediaRecorder = new MediaRecorder(this.recScreenStream, options); + console.log('Created MediaRecorder', this.mediaRecorder, 'with options', options); + this._isRecording = true; + this.handleMediaRecorder(); + this.event(_EVENTS.startRec); + this.sound('recStart'); + }) + .catch((err) => { + console.error('Error Unable to recording the screen + audio', err); + this.userLog('error', 'Unable to recording the screen + audio reason: ' + err, 'top-end'); + }); + } + } catch (err) { + console.error('Exception while creating MediaRecorder: ', err); + this.userLog('error', "Can't start stream recording reason: " + err, 'top-end'); + return; + } + } + + getNewStream(videoStream, audioStream) { + let newStream = null; + let videoStreamTrack = videoStream ? videoStream.getVideoTracks()[0] : undefined; + let audioStreamTrack = audioStream ? audioStream.getAudioTracks()[0] : undefined; + if (videoStreamTrack && audioStreamTrack) { + newStream = new MediaStream([videoStreamTrack, audioStreamTrack]); + } else if (videoStreamTrack) { + newStream = new MediaStream([videoStreamTrack]); + } else if (audioStreamTrack) { + newStream = new MediaStream([audioStreamTrack]); + } + return newStream; + } + + handleMediaRecorder() { + this.mediaRecorder.start(); + this.mediaRecorder.addEventListener('start', this.handleMediaRecorderStart); + this.mediaRecorder.addEventListener('dataavailable', this.handleMediaRecorderData); + this.mediaRecorder.addEventListener('stop', this.handleMediaRecorderStop); + } + + handleMediaRecorderStart(evt) { + console.log('MediaRecorder started: ', evt); + } + + handleMediaRecorderData(evt) { + console.log('MediaRecorder data: ', evt); + if (evt.data && evt.data.size > 0) recordedBlobs.push(evt.data); + } + + handleMediaRecorderStop(evt) { + try { + console.log('MediaRecorder stopped: ', evt); + console.log('MediaRecorder Blobs: ', recordedBlobs); + + const newDate = new Date(); + const date = newDate.toISOString().split('T')[0]; + const time = newDate.toTimeString().split(' ')[0]; + + const type = recordedBlobs[0].type.includes('mp4') ? 'mp4' : 'webm'; + const blob = new Blob(recordedBlobs, { type: 'video/' + type }); + const recFileName = `${date}-${time}` + '-REC.' + type; + + console.log('MediaRecorder Download Blobs'); + const url = window.URL.createObjectURL(blob); + const a = document.createElement('a'); + a.style.display = 'none'; + a.href = url; + a.download = recFileName; + document.body.appendChild(a); + a.click(); + setTimeout(() => { + document.body.removeChild(a); + window.URL.revokeObjectURL(url); + }, 100); + console.log(`🔴 Recording FILE: ${recFileName} done 👍`); + } catch (ex) { + console.warn('Recording save failed', ex); + } + } + + pauseRecording() { + this._isRecording = false; + this.mediaRecorder.pause(); + this.event(_EVENTS.pauseRec); + } + + resumeRecording() { + this._isRecording = true; + this.mediaRecorder.resume(); + this.event(_EVENTS.resumeRec); + } + + stopRecording() { + this._isRecording = false; + this.mediaRecorder.stop(); + if (this.recScreenStream) { + this.recScreenStream.getTracks().forEach((track) => { + if (track.kind === 'video') track.stop(); + }); + } + if (this.isMobileDevice) this.getId('swapCameraButton').className = ''; + this.getId('recordingStatus').innerHTML = '🔴 REC 0s'; + this.event(_EVENTS.stopRec); + this.sound('recStop'); + } +} diff --git a/public/js/landing.js b/public/js/landing.js new file mode 100644 index 00000000..e90aaf7c --- /dev/null +++ b/public/js/landing.js @@ -0,0 +1,58 @@ +!(function () { + window; + const e = document.documentElement; + if ((e.classList.remove('no-js'), e.classList.add('js'), document.body.classList.contains('has-animations'))) { + (window.sr = ScrollReveal()).reveal('.feature, .pricing-table-inner', { + duration: 600, + distance: '20px', + easing: 'cubic-bezier(0.5, -0.01, 0, 1.005)', + origin: 'bottom', + interval: 100, + }), + e.classList.add('anime-ready'), + anime + .timeline({ targets: '.hero-figure-box-05' }) + .add({ + duration: 400, + easing: 'easeInOutExpo', + scaleX: [0.05, 0.05], + scaleY: [0, 1], + perspective: '500px', + delay: anime.random(0, 400), + }) + .add({ duration: 400, easing: 'easeInOutExpo', scaleX: 1 }) + .add({ + duration: 800, + rotateY: '-15deg', + rotateX: '8deg', + rotateZ: '-1deg', + }), + anime + .timeline({ targets: '.hero-figure-box-06, .hero-figure-box-07' }) + .add({ + duration: 400, + easing: 'easeInOutExpo', + scaleX: [0.05, 0.05], + scaleY: [0, 1], + perspective: '500px', + delay: anime.random(0, 400), + }) + .add({ duration: 400, easing: 'easeInOutExpo', scaleX: 1 }) + .add({ duration: 800, rotateZ: '20deg' }), + anime({ + targets: + '.hero-figure-box-01, .hero-figure-box-02, .hero-figure-box-03, .hero-figure-box-04, .hero-figure-box-08, .hero-figure-box-09, .hero-figure-box-10', + duration: anime.random(600, 800), + delay: anime.random(600, 800), + rotate: [ + anime.random(-360, 360), + function (e) { + return e.getAttribute('data-rotation'); + }, + ], + scale: [0.7, 1], + opacity: [0, 1], + easing: 'easeInOutExpo', + }); + } +})(); diff --git a/public/js/newroom.js b/public/js/newroom.js new file mode 100644 index 00000000..995c73ae --- /dev/null +++ b/public/js/newroom.js @@ -0,0 +1,155 @@ +'use strict'; + +let adjectives = [ + 'small', + 'big', + 'large', + 'smelly', + 'new', + 'happy', + 'shiny', + 'old', + 'clean', + 'nice', + 'bad', + 'cool', + 'hot', + 'cold', + 'warm', + 'hungry', + 'slow', + 'fast', + 'red', + 'white', + 'black', + 'blue', + 'green', + 'basic', + 'strong', + 'cute', + 'poor', + 'nice', + 'huge', + 'rare', + 'lucky', + 'weak', + 'tall', + 'short', + 'tiny', + 'great', + 'long', + 'single', + 'rich', + 'young', + 'dirty', + 'fresh', + 'brown', + 'dark', + 'crazy', + 'sad', + 'loud', + 'brave', + 'calm', + 'silly', + 'smart', +]; + +let nouns = [ + 'dog', + 'bat', + 'wrench', + 'apple', + 'pear', + 'ghost', + 'cat', + 'wolf', + 'squid', + 'goat', + 'snail', + 'hat', + 'sock', + 'plum', + 'bear', + 'snake', + 'turtle', + 'horse', + 'spoon', + 'fork', + 'spider', + 'tree', + 'chair', + 'table', + 'couch', + 'towel', + 'panda', + 'bread', + 'grape', + 'cake', + 'brick', + 'rat', + 'mouse', + 'bird', + 'oven', + 'phone', + 'photo', + 'frog', + 'bear', + 'camel', + 'sheep', + 'shark', + 'tiger', + 'zebra', + 'duck', + 'eagle', + 'fish', + 'kitten', + 'lobster', + 'monkey', + 'owl', + 'puppy', + 'pig', + 'rabbit', + 'fox', + 'whale', + 'beaver', + 'gorilla', + 'lizard', + 'parrot', + 'sloth', + 'swan', +]; + +function getRandomNumber(length) { + let result = ''; + let characters = '0123456789'; + let charactersLength = characters.length; + for (let i = 0; i < length; i++) { + result += characters.charAt(Math.floor(Math.random() * charactersLength)); + } + return result; +} + +let adjective = adjectives[Math.floor(Math.random() * adjectives.length)]; +let noun = nouns[Math.floor(Math.random() * nouns.length)]; +let num = getRandomNumber(5); +noun = noun.charAt(0).toUpperCase() + noun.substring(1); +adjective = adjective.charAt(0).toUpperCase() + adjective.substring(1); +document.getElementById('roomName').value = ''; + +// #################################################### +// TYPPING EFECT +// #################################################### + +let i = 0; +let txt = num + adjective + noun; +let speed = 100; + +function typeWriter() { + if (i < txt.length) { + document.getElementById('roomName').value += txt.charAt(i); + i++; + setTimeout(typeWriter, speed); + } +} + +typeWriter(); diff --git a/public/landing.html b/public/landing.html new file mode 100644 index 00000000..4482cfd8 --- /dev/null +++ b/public/landing.html @@ -0,0 +1,246 @@ + + + + + + MiroTalk SFU a Free Video Calls and Screen Sharing. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+
+
+
+
+

+ MiroTalk SFU
Free browser based real-time video calls.
+ Simple, Secure, Fast. +

+

+ Start your next video call with a single click. No download, plug-in or login + required. Just get straight to talking, messaging and sharing your screen. +

+
+ TRY NOW +
+
+ +
+ + + +
+
+
+
+
+
+
+
+
+
+
+
+
+
+ +
+
+
+
+

+ Unlimited number of conference rooms and users without call time limitation! +

+

+ MiroTalk with SFU integrated Server. We engineered a platform with maximum video + quality lowest latency that makes your calls crystal clear. +

+
+
+
+ +
+
+
+
+
+
+ mirotalksfu-screen +
+

Screen Sharing

+

+ Share your screen, application window, present your documents, slides and + more. +

+
+
+
+
+
+ mirotalksfu-webcam +
+

WebCam Streaming

+

+ Having the webcam on, allows participants to make a deeper connection with + you. Up to 4k resolution. +

+
+
+
+
+
+ mirotalksfu-audio +
+

Audio Streaming

+

+ Echo cancellation and noise suppression that makes your audio crystal clear. +

+
+
+
+
+
+ mirotalksfu-chat +
+

Chat

+

+ Chat with others in meeting with integrated emoji picker to show you + feeling. +

+
+
+
+
+
+ mirotalksfu-recording +
+

Recording meeting

+

+ Record your Screen, Video and Audio on Your browser Blobs. Save it for use + it in future or to share with others. +

+
+
+
+
+
+ mirotalksfu-recording +
+

Total privacy

+

+ Data stays between you and your participants. MiroTalk SFU don't collect or + share personal information. +

+
+
+
+
+
+
+ +
+
+

Meet the team

+
+ Team member author +
+ +
Miroslav Pejic
+
+ Full Stack Developer +
+
+
+
+
+ + +
+ + + + + diff --git a/public/modules/MediasoupClient.js b/public/modules/MediasoupClient.js new file mode 100644 index 00000000..2af13586 --- /dev/null +++ b/public/modules/MediasoupClient.js @@ -0,0 +1,13394 @@ +(function () { + function r(e, n, t) { + function o(i, f) { + if (!n[i]) { + if (!e[i]) { + var c = 'function' == typeof require && require; + if (!f && c) return c(i, !0); + if (u) return u(i, !0); + var a = new Error("Cannot find module '" + i + "'"); + throw ((a.code = 'MODULE_NOT_FOUND'), a); + } + var p = (n[i] = { exports: {} }); + e[i][0].call( + p.exports, + function (r) { + var n = e[i][1][r]; + return o(n || r); + }, + p, + p.exports, + r, + e, + n, + t, + ); + } + return n[i].exports; + } + for (var u = 'function' == typeof require && require, i = 0; i < t.length; i++) o(t[i]); + return o; + } + return r; +})()( + { + 1: [ + function (require, module, exports) { + const client = require('mediasoup-client'); + window.mediasoupClient = client; + }, + { 'mediasoup-client': 35 }, + ], + 2: [ + function (require, module, exports) { + 'use strict'; + var __awaiter = + (this && this.__awaiter) || + function (thisArg, _arguments, P, generator) { + function adopt(value) { + return value instanceof P + ? value + : new P(function (resolve) { + resolve(value); + }); + } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { + try { + step(generator.next(value)); + } catch (e) { + reject(e); + } + } + function rejected(value) { + try { + step(generator['throw'](value)); + } catch (e) { + reject(e); + } + } + function step(result) { + result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); + } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); + }; + Object.defineProperty(exports, '__esModule', { value: true }); + class AwaitQueue { + constructor( + { ClosedErrorClass = Error, StoppedErrorClass = Error } = { + ClosedErrorClass: Error, + StoppedErrorClass: Error, + }, + ) { + // Closed flag. + this.closed = false; + // Queue of pending tasks. + this.pendingTasks = []; + // Error class used when rejecting a task due to AwaitQueue being closed. + this.ClosedErrorClass = Error; + // Error class used when rejecting a task due to AwaitQueue being stopped. + this.StoppedErrorClass = Error; + this.ClosedErrorClass = ClosedErrorClass; + this.StoppedErrorClass = StoppedErrorClass; + } + /** + * The number of ongoing enqueued tasks. + */ + get size() { + return this.pendingTasks.length; + } + /** + * Closes the AwaitQueue. Pending tasks will be rejected with ClosedErrorClass + * error. + */ + close() { + if (this.closed) return; + this.closed = true; + for (const pendingTask of this.pendingTasks) { + pendingTask.stopped = true; + pendingTask.reject(new this.ClosedErrorClass('AwaitQueue closed')); + } + // Enpty the pending tasks array. + this.pendingTasks.length = 0; + } + /** + * Accepts a task as argument (and an optional task name) and enqueues it after + * pending tasks. Once processed, the push() method resolves (or rejects) with + * the result returned by the given task. + * + * The given task must return a Promise or directly a value. + */ + push(task, name) { + return __awaiter(this, void 0, void 0, function* () { + if (this.closed) throw new this.ClosedErrorClass('AwaitQueue closed'); + if (typeof task !== 'function') throw new TypeError('given task is not a function'); + if (!task.name && name) { + try { + Object.defineProperty(task, 'name', { value: name }); + } catch (error) {} + } + return new Promise((resolve, reject) => { + const pendingTask = { + task, + name, + resolve, + reject, + stopped: false, + enqueuedAt: new Date(), + executedAt: undefined, + }; + // Append task to the queue. + this.pendingTasks.push(pendingTask); + // And run it if this is the only task in the queue. + if (this.pendingTasks.length === 1) this.next(); + }); + }); + } + /** + * Make ongoing pending tasks reject with the given StoppedErrorClass error. + * The AwaitQueue instance is still usable for future tasks added via push() + * method. + */ + stop() { + if (this.closed) return; + for (const pendingTask of this.pendingTasks) { + pendingTask.stopped = true; + pendingTask.reject(new this.StoppedErrorClass('AwaitQueue stopped')); + } + // Enpty the pending tasks array. + this.pendingTasks.length = 0; + } + dump() { + const now = new Date(); + return this.pendingTasks.map((pendingTask) => { + return { + task: pendingTask.task, + name: pendingTask.name, + enqueuedTime: pendingTask.executedAt + ? pendingTask.executedAt.getTime() - pendingTask.enqueuedAt.getTime() + : now.getTime() - pendingTask.enqueuedAt.getTime(), + executingTime: pendingTask.executedAt + ? now.getTime() - pendingTask.executedAt.getTime() + : 0, + }; + }); + } + next() { + return __awaiter(this, void 0, void 0, function* () { + // Take the first pending task. + const pendingTask = this.pendingTasks[0]; + if (!pendingTask) return; + // Execute it. + yield this.executeTask(pendingTask); + // Remove the first pending task (the completed one) from the queue. + this.pendingTasks.shift(); + // And continue. + this.next(); + }); + } + executeTask(pendingTask) { + return __awaiter(this, void 0, void 0, function* () { + // If the task is stopped, ignore it. + if (pendingTask.stopped) return; + pendingTask.executedAt = new Date(); + try { + const result = yield pendingTask.task(); + // If the task is stopped, ignore it. + if (pendingTask.stopped) return; + // Resolve the task with the returned result (if any). + pendingTask.resolve(result); + } catch (error) { + // If the task is stopped, ignore it. + if (pendingTask.stopped) return; + // Reject the task with its own error. + pendingTask.reject(error); + } + }); + } + } + exports.AwaitQueue = AwaitQueue; + }, + {}, + ], + 3: [ + function (require, module, exports) { + !(function (e, t) { + 'object' == typeof exports && 'object' == typeof module + ? (module.exports = t()) + : 'function' == typeof define && define.amd + ? define([], t) + : 'object' == typeof exports + ? (exports.bowser = t()) + : (e.bowser = t()); + })(this, function () { + return (function (e) { + var t = {}; + function r(n) { + if (t[n]) return t[n].exports; + var i = (t[n] = { i: n, l: !1, exports: {} }); + return e[n].call(i.exports, i, i.exports, r), (i.l = !0), i.exports; + } + return ( + (r.m = e), + (r.c = t), + (r.d = function (e, t, n) { + r.o(e, t) || Object.defineProperty(e, t, { enumerable: !0, get: n }); + }), + (r.r = function (e) { + 'undefined' != typeof Symbol && + Symbol.toStringTag && + Object.defineProperty(e, Symbol.toStringTag, { value: 'Module' }), + Object.defineProperty(e, '__esModule', { value: !0 }); + }), + (r.t = function (e, t) { + if ((1 & t && (e = r(e)), 8 & t)) return e; + if (4 & t && 'object' == typeof e && e && e.__esModule) return e; + var n = Object.create(null); + if ( + (r.r(n), + Object.defineProperty(n, 'default', { enumerable: !0, value: e }), + 2 & t && 'string' != typeof e) + ) + for (var i in e) + r.d( + n, + i, + function (t) { + return e[t]; + }.bind(null, i), + ); + return n; + }), + (r.n = function (e) { + var t = + e && e.__esModule + ? function () { + return e.default; + } + : function () { + return e; + }; + return r.d(t, 'a', t), t; + }), + (r.o = function (e, t) { + return Object.prototype.hasOwnProperty.call(e, t); + }), + (r.p = ''), + r((r.s = 90)) + ); + })({ + 17: function (e, t, r) { + 'use strict'; + (t.__esModule = !0), (t.default = void 0); + var n = r(18), + i = (function () { + function e() {} + return ( + (e.getFirstMatch = function (e, t) { + var r = t.match(e); + return (r && r.length > 0 && r[1]) || ''; + }), + (e.getSecondMatch = function (e, t) { + var r = t.match(e); + return (r && r.length > 1 && r[2]) || ''; + }), + (e.matchAndReturnConst = function (e, t, r) { + if (e.test(t)) return r; + }), + (e.getWindowsVersionName = function (e) { + switch (e) { + case 'NT': + return 'NT'; + case 'XP': + return 'XP'; + case 'NT 5.0': + return '2000'; + case 'NT 5.1': + return 'XP'; + case 'NT 5.2': + return '2003'; + case 'NT 6.0': + return 'Vista'; + case 'NT 6.1': + return '7'; + case 'NT 6.2': + return '8'; + case 'NT 6.3': + return '8.1'; + case 'NT 10.0': + return '10'; + default: + return; + } + }), + (e.getMacOSVersionName = function (e) { + var t = e + .split('.') + .splice(0, 2) + .map(function (e) { + return parseInt(e, 10) || 0; + }); + if ((t.push(0), 10 === t[0])) + switch (t[1]) { + case 5: + return 'Leopard'; + case 6: + return 'Snow Leopard'; + case 7: + return 'Lion'; + case 8: + return 'Mountain Lion'; + case 9: + return 'Mavericks'; + case 10: + return 'Yosemite'; + case 11: + return 'El Capitan'; + case 12: + return 'Sierra'; + case 13: + return 'High Sierra'; + case 14: + return 'Mojave'; + case 15: + return 'Catalina'; + default: + return; + } + }), + (e.getAndroidVersionName = function (e) { + var t = e + .split('.') + .splice(0, 2) + .map(function (e) { + return parseInt(e, 10) || 0; + }); + if ((t.push(0), !(1 === t[0] && t[1] < 5))) + return 1 === t[0] && t[1] < 6 + ? 'Cupcake' + : 1 === t[0] && t[1] >= 6 + ? 'Donut' + : 2 === t[0] && t[1] < 2 + ? 'Eclair' + : 2 === t[0] && 2 === t[1] + ? 'Froyo' + : 2 === t[0] && t[1] > 2 + ? 'Gingerbread' + : 3 === t[0] + ? 'Honeycomb' + : 4 === t[0] && t[1] < 1 + ? 'Ice Cream Sandwich' + : 4 === t[0] && t[1] < 4 + ? 'Jelly Bean' + : 4 === t[0] && t[1] >= 4 + ? 'KitKat' + : 5 === t[0] + ? 'Lollipop' + : 6 === t[0] + ? 'Marshmallow' + : 7 === t[0] + ? 'Nougat' + : 8 === t[0] + ? 'Oreo' + : 9 === t[0] + ? 'Pie' + : void 0; + }), + (e.getVersionPrecision = function (e) { + return e.split('.').length; + }), + (e.compareVersions = function (t, r, n) { + void 0 === n && (n = !1); + var i = e.getVersionPrecision(t), + s = e.getVersionPrecision(r), + a = Math.max(i, s), + o = 0, + u = e.map([t, r], function (t) { + var r = a - e.getVersionPrecision(t), + n = t + new Array(r + 1).join('.0'); + return e + .map(n.split('.'), function (e) { + return new Array(20 - e.length).join('0') + e; + }) + .reverse(); + }); + for (n && (o = a - Math.min(i, s)), a -= 1; a >= o; ) { + if (u[0][a] > u[1][a]) return 1; + if (u[0][a] === u[1][a]) { + if (a === o) return 0; + a -= 1; + } else if (u[0][a] < u[1][a]) return -1; + } + }), + (e.map = function (e, t) { + var r, + n = []; + if (Array.prototype.map) return Array.prototype.map.call(e, t); + for (r = 0; r < e.length; r += 1) n.push(t(e[r])); + return n; + }), + (e.find = function (e, t) { + var r, n; + if (Array.prototype.find) return Array.prototype.find.call(e, t); + for (r = 0, n = e.length; r < n; r += 1) { + var i = e[r]; + if (t(i, r)) return i; + } + }), + (e.assign = function (e) { + for ( + var t, + r, + n = e, + i = arguments.length, + s = new Array(i > 1 ? i - 1 : 0), + a = 1; + a < i; + a++ + ) + s[a - 1] = arguments[a]; + if (Object.assign) return Object.assign.apply(Object, [e].concat(s)); + var o = function () { + var e = s[t]; + 'object' == typeof e && + null !== e && + Object.keys(e).forEach(function (t) { + n[t] = e[t]; + }); + }; + for (t = 0, r = s.length; t < r; t += 1) o(); + return e; + }), + (e.getBrowserAlias = function (e) { + return n.BROWSER_ALIASES_MAP[e]; + }), + (e.getBrowserTypeByAlias = function (e) { + return n.BROWSER_MAP[e] || ''; + }), + e + ); + })(); + (t.default = i), (e.exports = t.default); + }, + 18: function (e, t, r) { + 'use strict'; + (t.__esModule = !0), + (t.ENGINE_MAP = + t.OS_MAP = + t.PLATFORMS_MAP = + t.BROWSER_MAP = + t.BROWSER_ALIASES_MAP = + void 0); + t.BROWSER_ALIASES_MAP = { + 'Amazon Silk': 'amazon_silk', + 'Android Browser': 'android', + Bada: 'bada', + BlackBerry: 'blackberry', + Chrome: 'chrome', + Chromium: 'chromium', + Electron: 'electron', + Epiphany: 'epiphany', + Firefox: 'firefox', + Focus: 'focus', + Generic: 'generic', + 'Google Search': 'google_search', + Googlebot: 'googlebot', + 'Internet Explorer': 'ie', + 'K-Meleon': 'k_meleon', + Maxthon: 'maxthon', + 'Microsoft Edge': 'edge', + 'MZ Browser': 'mz', + 'NAVER Whale Browser': 'naver', + Opera: 'opera', + 'Opera Coast': 'opera_coast', + PhantomJS: 'phantomjs', + Puffin: 'puffin', + QupZilla: 'qupzilla', + QQ: 'qq', + QQLite: 'qqlite', + Safari: 'safari', + Sailfish: 'sailfish', + 'Samsung Internet for Android': 'samsung_internet', + SeaMonkey: 'seamonkey', + Sleipnir: 'sleipnir', + Swing: 'swing', + Tizen: 'tizen', + 'UC Browser': 'uc', + Vivaldi: 'vivaldi', + 'WebOS Browser': 'webos', + WeChat: 'wechat', + 'Yandex Browser': 'yandex', + Roku: 'roku', + }; + t.BROWSER_MAP = { + amazon_silk: 'Amazon Silk', + android: 'Android Browser', + bada: 'Bada', + blackberry: 'BlackBerry', + chrome: 'Chrome', + chromium: 'Chromium', + electron: 'Electron', + epiphany: 'Epiphany', + firefox: 'Firefox', + focus: 'Focus', + generic: 'Generic', + googlebot: 'Googlebot', + google_search: 'Google Search', + ie: 'Internet Explorer', + k_meleon: 'K-Meleon', + maxthon: 'Maxthon', + edge: 'Microsoft Edge', + mz: 'MZ Browser', + naver: 'NAVER Whale Browser', + opera: 'Opera', + opera_coast: 'Opera Coast', + phantomjs: 'PhantomJS', + puffin: 'Puffin', + qupzilla: 'QupZilla', + qq: 'QQ Browser', + qqlite: 'QQ Browser Lite', + safari: 'Safari', + sailfish: 'Sailfish', + samsung_internet: 'Samsung Internet for Android', + seamonkey: 'SeaMonkey', + sleipnir: 'Sleipnir', + swing: 'Swing', + tizen: 'Tizen', + uc: 'UC Browser', + vivaldi: 'Vivaldi', + webos: 'WebOS Browser', + wechat: 'WeChat', + yandex: 'Yandex Browser', + }; + t.PLATFORMS_MAP = { tablet: 'tablet', mobile: 'mobile', desktop: 'desktop', tv: 'tv' }; + t.OS_MAP = { + WindowsPhone: 'Windows Phone', + Windows: 'Windows', + MacOS: 'macOS', + iOS: 'iOS', + Android: 'Android', + WebOS: 'WebOS', + BlackBerry: 'BlackBerry', + Bada: 'Bada', + Tizen: 'Tizen', + Linux: 'Linux', + ChromeOS: 'Chrome OS', + PlayStation4: 'PlayStation 4', + Roku: 'Roku', + }; + t.ENGINE_MAP = { + EdgeHTML: 'EdgeHTML', + Blink: 'Blink', + Trident: 'Trident', + Presto: 'Presto', + Gecko: 'Gecko', + WebKit: 'WebKit', + }; + }, + 90: function (e, t, r) { + 'use strict'; + (t.__esModule = !0), (t.default = void 0); + var n, + i = (n = r(91)) && n.__esModule ? n : { default: n }, + s = r(18); + function a(e, t) { + for (var r = 0; r < t.length; r++) { + var n = t[r]; + (n.enumerable = n.enumerable || !1), + (n.configurable = !0), + 'value' in n && (n.writable = !0), + Object.defineProperty(e, n.key, n); + } + } + var o = (function () { + function e() {} + var t, r, n; + return ( + (e.getParser = function (e, t) { + if ((void 0 === t && (t = !1), 'string' != typeof e)) + throw new Error('UserAgent should be a string'); + return new i.default(e, t); + }), + (e.parse = function (e) { + return new i.default(e).getResult(); + }), + (t = e), + (n = [ + { + key: 'BROWSER_MAP', + get: function () { + return s.BROWSER_MAP; + }, + }, + { + key: 'ENGINE_MAP', + get: function () { + return s.ENGINE_MAP; + }, + }, + { + key: 'OS_MAP', + get: function () { + return s.OS_MAP; + }, + }, + { + key: 'PLATFORMS_MAP', + get: function () { + return s.PLATFORMS_MAP; + }, + }, + ]), + (r = null) && a(t.prototype, r), + n && a(t, n), + e + ); + })(); + (t.default = o), (e.exports = t.default); + }, + 91: function (e, t, r) { + 'use strict'; + (t.__esModule = !0), (t.default = void 0); + var n = u(r(92)), + i = u(r(93)), + s = u(r(94)), + a = u(r(95)), + o = u(r(17)); + function u(e) { + return e && e.__esModule ? e : { default: e }; + } + var d = (function () { + function e(e, t) { + if ((void 0 === t && (t = !1), null == e || '' === e)) + throw new Error("UserAgent parameter can't be empty"); + (this._ua = e), (this.parsedResult = {}), !0 !== t && this.parse(); + } + var t = e.prototype; + return ( + (t.getUA = function () { + return this._ua; + }), + (t.test = function (e) { + return e.test(this._ua); + }), + (t.parseBrowser = function () { + var e = this; + this.parsedResult.browser = {}; + var t = o.default.find(n.default, function (t) { + if ('function' == typeof t.test) return t.test(e); + if (t.test instanceof Array) + return t.test.some(function (t) { + return e.test(t); + }); + throw new Error("Browser's test function is not valid"); + }); + return ( + t && (this.parsedResult.browser = t.describe(this.getUA())), + this.parsedResult.browser + ); + }), + (t.getBrowser = function () { + return this.parsedResult.browser + ? this.parsedResult.browser + : this.parseBrowser(); + }), + (t.getBrowserName = function (e) { + return e + ? String(this.getBrowser().name).toLowerCase() || '' + : this.getBrowser().name || ''; + }), + (t.getBrowserVersion = function () { + return this.getBrowser().version; + }), + (t.getOS = function () { + return this.parsedResult.os ? this.parsedResult.os : this.parseOS(); + }), + (t.parseOS = function () { + var e = this; + this.parsedResult.os = {}; + var t = o.default.find(i.default, function (t) { + if ('function' == typeof t.test) return t.test(e); + if (t.test instanceof Array) + return t.test.some(function (t) { + return e.test(t); + }); + throw new Error("Browser's test function is not valid"); + }); + return ( + t && (this.parsedResult.os = t.describe(this.getUA())), this.parsedResult.os + ); + }), + (t.getOSName = function (e) { + var t = this.getOS().name; + return e ? String(t).toLowerCase() || '' : t || ''; + }), + (t.getOSVersion = function () { + return this.getOS().version; + }), + (t.getPlatform = function () { + return this.parsedResult.platform + ? this.parsedResult.platform + : this.parsePlatform(); + }), + (t.getPlatformType = function (e) { + void 0 === e && (e = !1); + var t = this.getPlatform().type; + return e ? String(t).toLowerCase() || '' : t || ''; + }), + (t.parsePlatform = function () { + var e = this; + this.parsedResult.platform = {}; + var t = o.default.find(s.default, function (t) { + if ('function' == typeof t.test) return t.test(e); + if (t.test instanceof Array) + return t.test.some(function (t) { + return e.test(t); + }); + throw new Error("Browser's test function is not valid"); + }); + return ( + t && (this.parsedResult.platform = t.describe(this.getUA())), + this.parsedResult.platform + ); + }), + (t.getEngine = function () { + return this.parsedResult.engine ? this.parsedResult.engine : this.parseEngine(); + }), + (t.getEngineName = function (e) { + return e + ? String(this.getEngine().name).toLowerCase() || '' + : this.getEngine().name || ''; + }), + (t.parseEngine = function () { + var e = this; + this.parsedResult.engine = {}; + var t = o.default.find(a.default, function (t) { + if ('function' == typeof t.test) return t.test(e); + if (t.test instanceof Array) + return t.test.some(function (t) { + return e.test(t); + }); + throw new Error("Browser's test function is not valid"); + }); + return ( + t && (this.parsedResult.engine = t.describe(this.getUA())), + this.parsedResult.engine + ); + }), + (t.parse = function () { + return ( + this.parseBrowser(), + this.parseOS(), + this.parsePlatform(), + this.parseEngine(), + this + ); + }), + (t.getResult = function () { + return o.default.assign({}, this.parsedResult); + }), + (t.satisfies = function (e) { + var t = this, + r = {}, + n = 0, + i = {}, + s = 0; + if ( + (Object.keys(e).forEach(function (t) { + var a = e[t]; + 'string' == typeof a + ? ((i[t] = a), (s += 1)) + : 'object' == typeof a && ((r[t] = a), (n += 1)); + }), + n > 0) + ) { + var a = Object.keys(r), + u = o.default.find(a, function (e) { + return t.isOS(e); + }); + if (u) { + var d = this.satisfies(r[u]); + if (void 0 !== d) return d; + } + var c = o.default.find(a, function (e) { + return t.isPlatform(e); + }); + if (c) { + var f = this.satisfies(r[c]); + if (void 0 !== f) return f; + } + } + if (s > 0) { + var l = Object.keys(i), + h = o.default.find(l, function (e) { + return t.isBrowser(e, !0); + }); + if (void 0 !== h) return this.compareVersion(i[h]); + } + }), + (t.isBrowser = function (e, t) { + void 0 === t && (t = !1); + var r = this.getBrowserName().toLowerCase(), + n = e.toLowerCase(), + i = o.default.getBrowserTypeByAlias(n); + return t && i && (n = i.toLowerCase()), n === r; + }), + (t.compareVersion = function (e) { + var t = [0], + r = e, + n = !1, + i = this.getBrowserVersion(); + if ('string' == typeof i) + return ( + '>' === e[0] || '<' === e[0] + ? ((r = e.substr(1)), + '=' === e[1] ? ((n = !0), (r = e.substr(2))) : (t = []), + '>' === e[0] ? t.push(1) : t.push(-1)) + : '=' === e[0] + ? (r = e.substr(1)) + : '~' === e[0] && ((n = !0), (r = e.substr(1))), + t.indexOf(o.default.compareVersions(i, r, n)) > -1 + ); + }), + (t.isOS = function (e) { + return this.getOSName(!0) === String(e).toLowerCase(); + }), + (t.isPlatform = function (e) { + return this.getPlatformType(!0) === String(e).toLowerCase(); + }), + (t.isEngine = function (e) { + return this.getEngineName(!0) === String(e).toLowerCase(); + }), + (t.is = function (e, t) { + return ( + void 0 === t && (t = !1), + this.isBrowser(e, t) || this.isOS(e) || this.isPlatform(e) + ); + }), + (t.some = function (e) { + var t = this; + return ( + void 0 === e && (e = []), + e.some(function (e) { + return t.is(e); + }) + ); + }), + e + ); + })(); + (t.default = d), (e.exports = t.default); + }, + 92: function (e, t, r) { + 'use strict'; + (t.__esModule = !0), (t.default = void 0); + var n, + i = (n = r(17)) && n.__esModule ? n : { default: n }; + var s = /version\/(\d+(\.?_?\d+)+)/i, + a = [ + { + test: [/googlebot/i], + describe: function (e) { + var t = { name: 'Googlebot' }, + r = + i.default.getFirstMatch(/googlebot\/(\d+(\.\d+))/i, e) || + i.default.getFirstMatch(s, e); + return r && (t.version = r), t; + }, + }, + { + test: [/opera/i], + describe: function (e) { + var t = { name: 'Opera' }, + r = + i.default.getFirstMatch(s, e) || + i.default.getFirstMatch(/(?:opera)[\s/](\d+(\.?_?\d+)+)/i, e); + return r && (t.version = r), t; + }, + }, + { + test: [/opr\/|opios/i], + describe: function (e) { + var t = { name: 'Opera' }, + r = + i.default.getFirstMatch(/(?:opr|opios)[\s/](\S+)/i, e) || + i.default.getFirstMatch(s, e); + return r && (t.version = r), t; + }, + }, + { + test: [/SamsungBrowser/i], + describe: function (e) { + var t = { name: 'Samsung Internet for Android' }, + r = + i.default.getFirstMatch(s, e) || + i.default.getFirstMatch( + /(?:SamsungBrowser)[\s/](\d+(\.?_?\d+)+)/i, + e, + ); + return r && (t.version = r), t; + }, + }, + { + test: [/Whale/i], + describe: function (e) { + var t = { name: 'NAVER Whale Browser' }, + r = + i.default.getFirstMatch(s, e) || + i.default.getFirstMatch(/(?:whale)[\s/](\d+(?:\.\d+)+)/i, e); + return r && (t.version = r), t; + }, + }, + { + test: [/MZBrowser/i], + describe: function (e) { + var t = { name: 'MZ Browser' }, + r = + i.default.getFirstMatch(/(?:MZBrowser)[\s/](\d+(?:\.\d+)+)/i, e) || + i.default.getFirstMatch(s, e); + return r && (t.version = r), t; + }, + }, + { + test: [/focus/i], + describe: function (e) { + var t = { name: 'Focus' }, + r = + i.default.getFirstMatch(/(?:focus)[\s/](\d+(?:\.\d+)+)/i, e) || + i.default.getFirstMatch(s, e); + return r && (t.version = r), t; + }, + }, + { + test: [/swing/i], + describe: function (e) { + var t = { name: 'Swing' }, + r = + i.default.getFirstMatch(/(?:swing)[\s/](\d+(?:\.\d+)+)/i, e) || + i.default.getFirstMatch(s, e); + return r && (t.version = r), t; + }, + }, + { + test: [/coast/i], + describe: function (e) { + var t = { name: 'Opera Coast' }, + r = + i.default.getFirstMatch(s, e) || + i.default.getFirstMatch(/(?:coast)[\s/](\d+(\.?_?\d+)+)/i, e); + return r && (t.version = r), t; + }, + }, + { + test: [/opt\/\d+(?:.?_?\d+)+/i], + describe: function (e) { + var t = { name: 'Opera Touch' }, + r = + i.default.getFirstMatch(/(?:opt)[\s/](\d+(\.?_?\d+)+)/i, e) || + i.default.getFirstMatch(s, e); + return r && (t.version = r), t; + }, + }, + { + test: [/yabrowser/i], + describe: function (e) { + var t = { name: 'Yandex Browser' }, + r = + i.default.getFirstMatch(/(?:yabrowser)[\s/](\d+(\.?_?\d+)+)/i, e) || + i.default.getFirstMatch(s, e); + return r && (t.version = r), t; + }, + }, + { + test: [/ucbrowser/i], + describe: function (e) { + var t = { name: 'UC Browser' }, + r = + i.default.getFirstMatch(s, e) || + i.default.getFirstMatch(/(?:ucbrowser)[\s/](\d+(\.?_?\d+)+)/i, e); + return r && (t.version = r), t; + }, + }, + { + test: [/Maxthon|mxios/i], + describe: function (e) { + var t = { name: 'Maxthon' }, + r = + i.default.getFirstMatch(s, e) || + i.default.getFirstMatch( + /(?:Maxthon|mxios)[\s/](\d+(\.?_?\d+)+)/i, + e, + ); + return r && (t.version = r), t; + }, + }, + { + test: [/epiphany/i], + describe: function (e) { + var t = { name: 'Epiphany' }, + r = + i.default.getFirstMatch(s, e) || + i.default.getFirstMatch(/(?:epiphany)[\s/](\d+(\.?_?\d+)+)/i, e); + return r && (t.version = r), t; + }, + }, + { + test: [/puffin/i], + describe: function (e) { + var t = { name: 'Puffin' }, + r = + i.default.getFirstMatch(s, e) || + i.default.getFirstMatch(/(?:puffin)[\s/](\d+(\.?_?\d+)+)/i, e); + return r && (t.version = r), t; + }, + }, + { + test: [/sleipnir/i], + describe: function (e) { + var t = { name: 'Sleipnir' }, + r = + i.default.getFirstMatch(s, e) || + i.default.getFirstMatch(/(?:sleipnir)[\s/](\d+(\.?_?\d+)+)/i, e); + return r && (t.version = r), t; + }, + }, + { + test: [/k-meleon/i], + describe: function (e) { + var t = { name: 'K-Meleon' }, + r = + i.default.getFirstMatch(s, e) || + i.default.getFirstMatch(/(?:k-meleon)[\s/](\d+(\.?_?\d+)+)/i, e); + return r && (t.version = r), t; + }, + }, + { + test: [/micromessenger/i], + describe: function (e) { + var t = { name: 'WeChat' }, + r = + i.default.getFirstMatch( + /(?:micromessenger)[\s/](\d+(\.?_?\d+)+)/i, + e, + ) || i.default.getFirstMatch(s, e); + return r && (t.version = r), t; + }, + }, + { + test: [/qqbrowser/i], + describe: function (e) { + var t = { + name: /qqbrowserlite/i.test(e) ? 'QQ Browser Lite' : 'QQ Browser', + }, + r = + i.default.getFirstMatch( + /(?:qqbrowserlite|qqbrowser)[/](\d+(\.?_?\d+)+)/i, + e, + ) || i.default.getFirstMatch(s, e); + return r && (t.version = r), t; + }, + }, + { + test: [/msie|trident/i], + describe: function (e) { + var t = { name: 'Internet Explorer' }, + r = i.default.getFirstMatch(/(?:msie |rv:)(\d+(\.?_?\d+)+)/i, e); + return r && (t.version = r), t; + }, + }, + { + test: [/\sedg\//i], + describe: function (e) { + var t = { name: 'Microsoft Edge' }, + r = i.default.getFirstMatch(/\sedg\/(\d+(\.?_?\d+)+)/i, e); + return r && (t.version = r), t; + }, + }, + { + test: [/edg([ea]|ios)/i], + describe: function (e) { + var t = { name: 'Microsoft Edge' }, + r = i.default.getSecondMatch(/edg([ea]|ios)\/(\d+(\.?_?\d+)+)/i, e); + return r && (t.version = r), t; + }, + }, + { + test: [/vivaldi/i], + describe: function (e) { + var t = { name: 'Vivaldi' }, + r = i.default.getFirstMatch(/vivaldi\/(\d+(\.?_?\d+)+)/i, e); + return r && (t.version = r), t; + }, + }, + { + test: [/seamonkey/i], + describe: function (e) { + var t = { name: 'SeaMonkey' }, + r = i.default.getFirstMatch(/seamonkey\/(\d+(\.?_?\d+)+)/i, e); + return r && (t.version = r), t; + }, + }, + { + test: [/sailfish/i], + describe: function (e) { + var t = { name: 'Sailfish' }, + r = i.default.getFirstMatch(/sailfish\s?browser\/(\d+(\.\d+)?)/i, e); + return r && (t.version = r), t; + }, + }, + { + test: [/silk/i], + describe: function (e) { + var t = { name: 'Amazon Silk' }, + r = i.default.getFirstMatch(/silk\/(\d+(\.?_?\d+)+)/i, e); + return r && (t.version = r), t; + }, + }, + { + test: [/phantom/i], + describe: function (e) { + var t = { name: 'PhantomJS' }, + r = i.default.getFirstMatch(/phantomjs\/(\d+(\.?_?\d+)+)/i, e); + return r && (t.version = r), t; + }, + }, + { + test: [/slimerjs/i], + describe: function (e) { + var t = { name: 'SlimerJS' }, + r = i.default.getFirstMatch(/slimerjs\/(\d+(\.?_?\d+)+)/i, e); + return r && (t.version = r), t; + }, + }, + { + test: [/blackberry|\bbb\d+/i, /rim\stablet/i], + describe: function (e) { + var t = { name: 'BlackBerry' }, + r = + i.default.getFirstMatch(s, e) || + i.default.getFirstMatch(/blackberry[\d]+\/(\d+(\.?_?\d+)+)/i, e); + return r && (t.version = r), t; + }, + }, + { + test: [/(web|hpw)[o0]s/i], + describe: function (e) { + var t = { name: 'WebOS Browser' }, + r = + i.default.getFirstMatch(s, e) || + i.default.getFirstMatch( + /w(?:eb)?[o0]sbrowser\/(\d+(\.?_?\d+)+)/i, + e, + ); + return r && (t.version = r), t; + }, + }, + { + test: [/bada/i], + describe: function (e) { + var t = { name: 'Bada' }, + r = i.default.getFirstMatch(/dolfin\/(\d+(\.?_?\d+)+)/i, e); + return r && (t.version = r), t; + }, + }, + { + test: [/tizen/i], + describe: function (e) { + var t = { name: 'Tizen' }, + r = + i.default.getFirstMatch( + /(?:tizen\s?)?browser\/(\d+(\.?_?\d+)+)/i, + e, + ) || i.default.getFirstMatch(s, e); + return r && (t.version = r), t; + }, + }, + { + test: [/qupzilla/i], + describe: function (e) { + var t = { name: 'QupZilla' }, + r = + i.default.getFirstMatch(/(?:qupzilla)[\s/](\d+(\.?_?\d+)+)/i, e) || + i.default.getFirstMatch(s, e); + return r && (t.version = r), t; + }, + }, + { + test: [/firefox|iceweasel|fxios/i], + describe: function (e) { + var t = { name: 'Firefox' }, + r = i.default.getFirstMatch( + /(?:firefox|iceweasel|fxios)[\s/](\d+(\.?_?\d+)+)/i, + e, + ); + return r && (t.version = r), t; + }, + }, + { + test: [/electron/i], + describe: function (e) { + var t = { name: 'Electron' }, + r = i.default.getFirstMatch(/(?:electron)\/(\d+(\.?_?\d+)+)/i, e); + return r && (t.version = r), t; + }, + }, + { + test: [/MiuiBrowser/i], + describe: function (e) { + var t = { name: 'Miui' }, + r = i.default.getFirstMatch(/(?:MiuiBrowser)[\s/](\d+(\.?_?\d+)+)/i, e); + return r && (t.version = r), t; + }, + }, + { + test: [/chromium/i], + describe: function (e) { + var t = { name: 'Chromium' }, + r = + i.default.getFirstMatch(/(?:chromium)[\s/](\d+(\.?_?\d+)+)/i, e) || + i.default.getFirstMatch(s, e); + return r && (t.version = r), t; + }, + }, + { + test: [/chrome|crios|crmo/i], + describe: function (e) { + var t = { name: 'Chrome' }, + r = i.default.getFirstMatch( + /(?:chrome|crios|crmo)\/(\d+(\.?_?\d+)+)/i, + e, + ); + return r && (t.version = r), t; + }, + }, + { + test: [/GSA/i], + describe: function (e) { + var t = { name: 'Google Search' }, + r = i.default.getFirstMatch(/(?:GSA)\/(\d+(\.?_?\d+)+)/i, e); + return r && (t.version = r), t; + }, + }, + { + test: function (e) { + var t = !e.test(/like android/i), + r = e.test(/android/i); + return t && r; + }, + describe: function (e) { + var t = { name: 'Android Browser' }, + r = i.default.getFirstMatch(s, e); + return r && (t.version = r), t; + }, + }, + { + test: [/playstation 4/i], + describe: function (e) { + var t = { name: 'PlayStation 4' }, + r = i.default.getFirstMatch(s, e); + return r && (t.version = r), t; + }, + }, + { + test: [/safari|applewebkit/i], + describe: function (e) { + var t = { name: 'Safari' }, + r = i.default.getFirstMatch(s, e); + return r && (t.version = r), t; + }, + }, + { + test: [/.*/i], + describe: function (e) { + var t = -1 !== e.search('\\(') ? /^(.*)\/(.*)[ \t]\((.*)/ : /^(.*)\/(.*) /; + return { + name: i.default.getFirstMatch(t, e), + version: i.default.getSecondMatch(t, e), + }; + }, + }, + ]; + (t.default = a), (e.exports = t.default); + }, + 93: function (e, t, r) { + 'use strict'; + (t.__esModule = !0), (t.default = void 0); + var n, + i = (n = r(17)) && n.__esModule ? n : { default: n }, + s = r(18); + var a = [ + { + test: [/Roku\/DVP/], + describe: function (e) { + var t = i.default.getFirstMatch(/Roku\/DVP-(\d+\.\d+)/i, e); + return { name: s.OS_MAP.Roku, version: t }; + }, + }, + { + test: [/windows phone/i], + describe: function (e) { + var t = i.default.getFirstMatch(/windows phone (?:os)?\s?(\d+(\.\d+)*)/i, e); + return { name: s.OS_MAP.WindowsPhone, version: t }; + }, + }, + { + test: [/windows /i], + describe: function (e) { + var t = i.default.getFirstMatch(/Windows ((NT|XP)( \d\d?.\d)?)/i, e), + r = i.default.getWindowsVersionName(t); + return { name: s.OS_MAP.Windows, version: t, versionName: r }; + }, + }, + { + test: [/Macintosh(.*?) FxiOS(.*?)\//], + describe: function (e) { + var t = { name: s.OS_MAP.iOS }, + r = i.default.getSecondMatch(/(Version\/)(\d[\d.]+)/, e); + return r && (t.version = r), t; + }, + }, + { + test: [/macintosh/i], + describe: function (e) { + var t = i.default + .getFirstMatch(/mac os x (\d+(\.?_?\d+)+)/i, e) + .replace(/[_\s]/g, '.'), + r = i.default.getMacOSVersionName(t), + n = { name: s.OS_MAP.MacOS, version: t }; + return r && (n.versionName = r), n; + }, + }, + { + test: [/(ipod|iphone|ipad)/i], + describe: function (e) { + var t = i.default + .getFirstMatch(/os (\d+([_\s]\d+)*) like mac os x/i, e) + .replace(/[_\s]/g, '.'); + return { name: s.OS_MAP.iOS, version: t }; + }, + }, + { + test: function (e) { + var t = !e.test(/like android/i), + r = e.test(/android/i); + return t && r; + }, + describe: function (e) { + var t = i.default.getFirstMatch(/android[\s/-](\d+(\.\d+)*)/i, e), + r = i.default.getAndroidVersionName(t), + n = { name: s.OS_MAP.Android, version: t }; + return r && (n.versionName = r), n; + }, + }, + { + test: [/(web|hpw)[o0]s/i], + describe: function (e) { + var t = i.default.getFirstMatch(/(?:web|hpw)[o0]s\/(\d+(\.\d+)*)/i, e), + r = { name: s.OS_MAP.WebOS }; + return t && t.length && (r.version = t), r; + }, + }, + { + test: [/blackberry|\bbb\d+/i, /rim\stablet/i], + describe: function (e) { + var t = + i.default.getFirstMatch(/rim\stablet\sos\s(\d+(\.\d+)*)/i, e) || + i.default.getFirstMatch(/blackberry\d+\/(\d+([_\s]\d+)*)/i, e) || + i.default.getFirstMatch(/\bbb(\d+)/i, e); + return { name: s.OS_MAP.BlackBerry, version: t }; + }, + }, + { + test: [/bada/i], + describe: function (e) { + var t = i.default.getFirstMatch(/bada\/(\d+(\.\d+)*)/i, e); + return { name: s.OS_MAP.Bada, version: t }; + }, + }, + { + test: [/tizen/i], + describe: function (e) { + var t = i.default.getFirstMatch(/tizen[/\s](\d+(\.\d+)*)/i, e); + return { name: s.OS_MAP.Tizen, version: t }; + }, + }, + { + test: [/linux/i], + describe: function () { + return { name: s.OS_MAP.Linux }; + }, + }, + { + test: [/CrOS/], + describe: function () { + return { name: s.OS_MAP.ChromeOS }; + }, + }, + { + test: [/PlayStation 4/], + describe: function (e) { + var t = i.default.getFirstMatch(/PlayStation 4[/\s](\d+(\.\d+)*)/i, e); + return { name: s.OS_MAP.PlayStation4, version: t }; + }, + }, + ]; + (t.default = a), (e.exports = t.default); + }, + 94: function (e, t, r) { + 'use strict'; + (t.__esModule = !0), (t.default = void 0); + var n, + i = (n = r(17)) && n.__esModule ? n : { default: n }, + s = r(18); + var a = [ + { + test: [/googlebot/i], + describe: function () { + return { type: 'bot', vendor: 'Google' }; + }, + }, + { + test: [/huawei/i], + describe: function (e) { + var t = i.default.getFirstMatch(/(can-l01)/i, e) && 'Nova', + r = { type: s.PLATFORMS_MAP.mobile, vendor: 'Huawei' }; + return t && (r.model = t), r; + }, + }, + { + test: [/nexus\s*(?:7|8|9|10).*/i], + describe: function () { + return { type: s.PLATFORMS_MAP.tablet, vendor: 'Nexus' }; + }, + }, + { + test: [/ipad/i], + describe: function () { + return { type: s.PLATFORMS_MAP.tablet, vendor: 'Apple', model: 'iPad' }; + }, + }, + { + test: [/Macintosh(.*?) FxiOS(.*?)\//], + describe: function () { + return { type: s.PLATFORMS_MAP.tablet, vendor: 'Apple', model: 'iPad' }; + }, + }, + { + test: [/kftt build/i], + describe: function () { + return { + type: s.PLATFORMS_MAP.tablet, + vendor: 'Amazon', + model: 'Kindle Fire HD 7', + }; + }, + }, + { + test: [/silk/i], + describe: function () { + return { type: s.PLATFORMS_MAP.tablet, vendor: 'Amazon' }; + }, + }, + { + test: [/tablet(?! pc)/i], + describe: function () { + return { type: s.PLATFORMS_MAP.tablet }; + }, + }, + { + test: function (e) { + var t = e.test(/ipod|iphone/i), + r = e.test(/like (ipod|iphone)/i); + return t && !r; + }, + describe: function (e) { + var t = i.default.getFirstMatch(/(ipod|iphone)/i, e); + return { type: s.PLATFORMS_MAP.mobile, vendor: 'Apple', model: t }; + }, + }, + { + test: [/nexus\s*[0-6].*/i, /galaxy nexus/i], + describe: function () { + return { type: s.PLATFORMS_MAP.mobile, vendor: 'Nexus' }; + }, + }, + { + test: [/[^-]mobi/i], + describe: function () { + return { type: s.PLATFORMS_MAP.mobile }; + }, + }, + { + test: function (e) { + return 'blackberry' === e.getBrowserName(!0); + }, + describe: function () { + return { type: s.PLATFORMS_MAP.mobile, vendor: 'BlackBerry' }; + }, + }, + { + test: function (e) { + return 'bada' === e.getBrowserName(!0); + }, + describe: function () { + return { type: s.PLATFORMS_MAP.mobile }; + }, + }, + { + test: function (e) { + return 'windows phone' === e.getBrowserName(); + }, + describe: function () { + return { type: s.PLATFORMS_MAP.mobile, vendor: 'Microsoft' }; + }, + }, + { + test: function (e) { + var t = Number(String(e.getOSVersion()).split('.')[0]); + return 'android' === e.getOSName(!0) && t >= 3; + }, + describe: function () { + return { type: s.PLATFORMS_MAP.tablet }; + }, + }, + { + test: function (e) { + return 'android' === e.getOSName(!0); + }, + describe: function () { + return { type: s.PLATFORMS_MAP.mobile }; + }, + }, + { + test: function (e) { + return 'macos' === e.getOSName(!0); + }, + describe: function () { + return { type: s.PLATFORMS_MAP.desktop, vendor: 'Apple' }; + }, + }, + { + test: function (e) { + return 'windows' === e.getOSName(!0); + }, + describe: function () { + return { type: s.PLATFORMS_MAP.desktop }; + }, + }, + { + test: function (e) { + return 'linux' === e.getOSName(!0); + }, + describe: function () { + return { type: s.PLATFORMS_MAP.desktop }; + }, + }, + { + test: function (e) { + return 'playstation 4' === e.getOSName(!0); + }, + describe: function () { + return { type: s.PLATFORMS_MAP.tv }; + }, + }, + { + test: function (e) { + return 'roku' === e.getOSName(!0); + }, + describe: function () { + return { type: s.PLATFORMS_MAP.tv }; + }, + }, + ]; + (t.default = a), (e.exports = t.default); + }, + 95: function (e, t, r) { + 'use strict'; + (t.__esModule = !0), (t.default = void 0); + var n, + i = (n = r(17)) && n.__esModule ? n : { default: n }, + s = r(18); + var a = [ + { + test: function (e) { + return 'microsoft edge' === e.getBrowserName(!0); + }, + describe: function (e) { + if (/\sedg\//i.test(e)) return { name: s.ENGINE_MAP.Blink }; + var t = i.default.getFirstMatch(/edge\/(\d+(\.?_?\d+)+)/i, e); + return { name: s.ENGINE_MAP.EdgeHTML, version: t }; + }, + }, + { + test: [/trident/i], + describe: function (e) { + var t = { name: s.ENGINE_MAP.Trident }, + r = i.default.getFirstMatch(/trident\/(\d+(\.?_?\d+)+)/i, e); + return r && (t.version = r), t; + }, + }, + { + test: function (e) { + return e.test(/presto/i); + }, + describe: function (e) { + var t = { name: s.ENGINE_MAP.Presto }, + r = i.default.getFirstMatch(/presto\/(\d+(\.?_?\d+)+)/i, e); + return r && (t.version = r), t; + }, + }, + { + test: function (e) { + var t = e.test(/gecko/i), + r = e.test(/like gecko/i); + return t && !r; + }, + describe: function (e) { + var t = { name: s.ENGINE_MAP.Gecko }, + r = i.default.getFirstMatch(/gecko\/(\d+(\.?_?\d+)+)/i, e); + return r && (t.version = r), t; + }, + }, + { + test: [/(apple)?webkit\/537\.36/i], + describe: function () { + return { name: s.ENGINE_MAP.Blink }; + }, + }, + { + test: [/(apple)?webkit/i], + describe: function (e) { + var t = { name: s.ENGINE_MAP.WebKit }, + r = i.default.getFirstMatch(/webkit\/(\d+(\.?_?\d+)+)/i, e); + return r && (t.version = r), t; + }, + }, + ]; + (t.default = a), (e.exports = t.default); + }, + }); + }); + }, + {}, + ], + 4: [ + function (require, module, exports) { + const debug = require('debug')('h264-profile-level-id'); + + /* eslint-disable no-console */ + debug.log = console.info.bind(console); + /* eslint-enable no-console */ + + const ProfileConstrainedBaseline = 1; + const ProfileBaseline = 2; + const ProfileMain = 3; + const ProfileConstrainedHigh = 4; + const ProfileHigh = 5; + + exports.ProfileConstrainedBaseline = ProfileConstrainedBaseline; + exports.ProfileBaseline = ProfileBaseline; + exports.ProfileMain = ProfileMain; + exports.ProfileConstrainedHigh = ProfileConstrainedHigh; + exports.ProfileHigh = ProfileHigh; + + // All values are equal to ten times the level number, except level 1b which is + // special. + const Level1_b = 0; + const Level1 = 10; + const Level1_1 = 11; + const Level1_2 = 12; + const Level1_3 = 13; + const Level2 = 20; + const Level2_1 = 21; + const Level2_2 = 22; + const Level3 = 30; + const Level3_1 = 31; + const Level3_2 = 32; + const Level4 = 40; + const Level4_1 = 41; + const Level4_2 = 42; + const Level5 = 50; + const Level5_1 = 51; + const Level5_2 = 52; + + exports.Level1_b = Level1_b; + exports.Level1 = Level1; + exports.Level1_1 = Level1_1; + exports.Level1_2 = Level1_2; + exports.Level1_3 = Level1_3; + exports.Level2 = Level2; + exports.Level2_1 = Level2_1; + exports.Level2_2 = Level2_2; + exports.Level3 = Level3; + exports.Level3_1 = Level3_1; + exports.Level3_2 = Level3_2; + exports.Level4 = Level4; + exports.Level4_1 = Level4_1; + exports.Level4_2 = Level4_2; + exports.Level5 = Level5; + exports.Level5_1 = Level5_1; + exports.Level5_2 = Level5_2; + + class ProfileLevelId { + constructor(profile, level) { + this.profile = profile; + this.level = level; + } + } + + exports.ProfileLevelId = ProfileLevelId; + + // Default ProfileLevelId. + // + // TODO: The default should really be profile Baseline and level 1 according to + // the spec: https://tools.ietf.org/html/rfc6184#section-8.1. In order to not + // break backwards compatibility with older versions of WebRTC where external + // codecs don't have any parameters, use profile ConstrainedBaseline level 3_1 + // instead. This workaround will only be done in an interim period to allow + // external clients to update their code. + // + // http://crbug/webrtc/6337. + const DefaultProfileLevelId = new ProfileLevelId(ProfileConstrainedBaseline, Level3_1); + + // For level_idc=11 and profile_idc=0x42, 0x4D, or 0x58, the constraint set3 + // flag specifies if level 1b or level 1.1 is used. + const ConstraintSet3Flag = 0x10; + + // Class for matching bit patterns such as "x1xx0000" where 'x' is allowed to be + // either 0 or 1. + class BitPattern { + constructor(str) { + this._mask = ~byteMaskString('x', str); + this._maskedValue = byteMaskString('1', str); + } + + isMatch(value) { + return this._maskedValue === (value & this._mask); + } + } + + // Class for converting between profile_idc/profile_iop to Profile. + class ProfilePattern { + constructor(profile_idc, profile_iop, profile) { + this.profile_idc = profile_idc; + this.profile_iop = profile_iop; + this.profile = profile; + } + } + + // This is from https://tools.ietf.org/html/rfc6184#section-8.1. + const ProfilePatterns = [ + new ProfilePattern(0x42, new BitPattern('x1xx0000'), ProfileConstrainedBaseline), + new ProfilePattern(0x4d, new BitPattern('1xxx0000'), ProfileConstrainedBaseline), + new ProfilePattern(0x58, new BitPattern('11xx0000'), ProfileConstrainedBaseline), + new ProfilePattern(0x42, new BitPattern('x0xx0000'), ProfileBaseline), + new ProfilePattern(0x58, new BitPattern('10xx0000'), ProfileBaseline), + new ProfilePattern(0x4d, new BitPattern('0x0x0000'), ProfileMain), + new ProfilePattern(0x64, new BitPattern('00000000'), ProfileHigh), + new ProfilePattern(0x64, new BitPattern('00001100'), ProfileConstrainedHigh), + ]; + + /** + * Parse profile level id that is represented as a string of 3 hex bytes. + * Nothing will be returned if the string is not a recognized H264 profile + * level id. + * + * @param {String} str - profile-level-id value as a string of 3 hex bytes. + * + * @returns {ProfileLevelId} + */ + exports.parseProfileLevelId = function (str) { + // The string should consist of 3 bytes in hexadecimal format. + if (typeof str !== 'string' || str.length !== 6) return null; + + const profile_level_id_numeric = parseInt(str, 16); + + if (profile_level_id_numeric === 0) return null; + + // Separate into three bytes. + const level_idc = profile_level_id_numeric & 0xff; + const profile_iop = (profile_level_id_numeric >> 8) & 0xff; + const profile_idc = (profile_level_id_numeric >> 16) & 0xff; + + // Parse level based on level_idc and constraint set 3 flag. + let level; + + switch (level_idc) { + case Level1_1: { + level = (profile_iop & ConstraintSet3Flag) !== 0 ? Level1_b : Level1_1; + break; + } + case Level1: + case Level1_2: + case Level1_3: + case Level2: + case Level2_1: + case Level2_2: + case Level3: + case Level3_1: + case Level3_2: + case Level4: + case Level4_1: + case Level4_2: + case Level5: + case Level5_1: + case Level5_2: { + level = level_idc; + break; + } + // Unrecognized level_idc. + default: { + debug('parseProfileLevelId() | unrecognized level_idc:%s', level_idc); + + return null; + } + } + + // Parse profile_idc/profile_iop into a Profile enum. + for (const pattern of ProfilePatterns) { + if (profile_idc === pattern.profile_idc && pattern.profile_iop.isMatch(profile_iop)) { + return new ProfileLevelId(pattern.profile, level); + } + } + + debug('parseProfileLevelId() | unrecognized profile_idc/profile_iop combination'); + + return null; + }; + + /** + * Returns canonical string representation as three hex bytes of the profile + * level id, or returns nothing for invalid profile level ids. + * + * @param {ProfileLevelId} profile_level_id + * + * @returns {String} + */ + exports.profileLevelIdToString = function (profile_level_id) { + // Handle special case level == 1b. + if (profile_level_id.level == Level1_b) { + switch (profile_level_id.profile) { + case ProfileConstrainedBaseline: { + return '42f00b'; + } + case ProfileBaseline: { + return '42100b'; + } + case ProfileMain: { + return '4d100b'; + } + // Level 1_b is not allowed for other profiles. + default: { + debug( + 'profileLevelIdToString() | Level 1_b not is allowed for profile:%s', + profile_level_id.profile, + ); + + return null; + } + } + } + + let profile_idc_iop_string; + + switch (profile_level_id.profile) { + case ProfileConstrainedBaseline: { + profile_idc_iop_string = '42e0'; + break; + } + case ProfileBaseline: { + profile_idc_iop_string = '4200'; + break; + } + case ProfileMain: { + profile_idc_iop_string = '4d00'; + break; + } + case ProfileConstrainedHigh: { + profile_idc_iop_string = '640c'; + break; + } + case ProfileHigh: { + profile_idc_iop_string = '6400'; + break; + } + default: { + debug('profileLevelIdToString() | unrecognized profile:%s', profile_level_id.profile); + + return null; + } + } + + let levelStr = profile_level_id.level.toString(16); + + if (levelStr.length === 1) levelStr = `0${levelStr}`; + + return `${profile_idc_iop_string}${levelStr}`; + }; + + /** + * Parse profile level id that is represented as a string of 3 hex bytes + * contained in an SDP key-value map. A default profile level id will be + * returned if the profile-level-id key is missing. Nothing will be returned if + * the key is present but the string is invalid. + * + * @param {Object} [params={}] - Codec parameters object. + * + * @returns {ProfileLevelId} + */ + exports.parseSdpProfileLevelId = function (params = {}) { + const profile_level_id = params['profile-level-id']; + + return !profile_level_id ? DefaultProfileLevelId : exports.parseProfileLevelId(profile_level_id); + }; + + /** + * Returns true if the parameters have the same H264 profile, i.e. the same + * H264 profile (Baseline, High, etc). + * + * @param {Object} [params1={}] - Codec parameters object. + * @param {Object} [params2={}] - Codec parameters object. + * + * @returns {Boolean} + */ + exports.isSameProfile = function (params1 = {}, params2 = {}) { + const profile_level_id_1 = exports.parseSdpProfileLevelId(params1); + const profile_level_id_2 = exports.parseSdpProfileLevelId(params2); + + // Compare H264 profiles, but not levels. + return Boolean( + profile_level_id_1 && + profile_level_id_2 && + profile_level_id_1.profile === profile_level_id_2.profile, + ); + }; + + /** + * Generate codec parameters that will be used as answer in an SDP negotiation + * based on local supported parameters and remote offered parameters. Both + * local_supported_params and remote_offered_params represent sendrecv media + * descriptions, i.e they are a mix of both encode and decode capabilities. In + * theory, when the profile in local_supported_params represent a strict superset + * of the profile in remote_offered_params, we could limit the profile in the + * answer to the profile in remote_offered_params. + * + * However, to simplify the code, each supported H264 profile should be listed + * explicitly in the list of local supported codecs, even if they are redundant. + * Then each local codec in the list should be tested one at a time against the + * remote codec, and only when the profiles are equal should this function be + * called. Therefore, this function does not need to handle profile intersection, + * and the profile of local_supported_params and remote_offered_params must be + * equal before calling this function. The parameters that are used when + * negotiating are the level part of profile-level-id and level-asymmetry-allowed. + * + * @param {Object} [local_supported_params={}] + * @param {Object} [remote_offered_params={}] + * + * @returns {String} Canonical string representation as three hex bytes of the + * profile level id, or null if no one of the params have profile-level-id. + * + * @throws {TypeError} If Profile mismatch or invalid params. + */ + exports.generateProfileLevelIdForAnswer = function ( + local_supported_params = {}, + remote_offered_params = {}, + ) { + // If both local and remote params do not contain profile-level-id, they are + // both using the default profile. In this case, don't return anything. + if (!local_supported_params['profile-level-id'] && !remote_offered_params['profile-level-id']) { + debug('generateProfileLevelIdForAnswer() | no profile-level-id in local and remote params'); + + return null; + } + + // Parse profile-level-ids. + const local_profile_level_id = exports.parseSdpProfileLevelId(local_supported_params); + const remote_profile_level_id = exports.parseSdpProfileLevelId(remote_offered_params); + + // The local and remote codec must have valid and equal H264 Profiles. + if (!local_profile_level_id) throw new TypeError('invalid local_profile_level_id'); + + if (!remote_profile_level_id) throw new TypeError('invalid remote_profile_level_id'); + + if (local_profile_level_id.profile !== remote_profile_level_id.profile) + throw new TypeError('H264 Profile mismatch'); + + // Parse level information. + const level_asymmetry_allowed = + isLevelAsymmetryAllowed(local_supported_params) && + isLevelAsymmetryAllowed(remote_offered_params); + + const local_level = local_profile_level_id.level; + const remote_level = remote_profile_level_id.level; + const min_level = minLevel(local_level, remote_level); + + // Determine answer level. When level asymmetry is not allowed, level upgrade + // is not allowed, i.e., the level in the answer must be equal to or lower + // than the level in the offer. + const answer_level = level_asymmetry_allowed ? local_level : min_level; + + debug( + 'generateProfileLevelIdForAnswer() | result: [profile:%s, level:%s]', + local_profile_level_id.profile, + answer_level, + ); + + // Return the resulting profile-level-id for the answer parameters. + return exports.profileLevelIdToString( + new ProfileLevelId(local_profile_level_id.profile, answer_level), + ); + }; + + // Convert a string of 8 characters into a byte where the positions containing + // character c will have their bit set. For example, c = 'x', str = "x1xx0000" + // will return 0b10110000. + function byteMaskString(c, str) { + return ( + ((str[0] === c) << 7) | + ((str[1] === c) << 6) | + ((str[2] === c) << 5) | + ((str[3] === c) << 4) | + ((str[4] === c) << 3) | + ((str[5] === c) << 2) | + ((str[6] === c) << 1) | + ((str[7] === c) << 0) + ); + } + + // Compare H264 levels and handle the level 1b case. + function isLessLevel(a, b) { + if (a === Level1_b) return b !== Level1 && b !== Level1_b; + + if (b === Level1_b) return a !== Level1; + + return a < b; + } + + function minLevel(a, b) { + return isLessLevel(a, b) ? a : b; + } + + function isLevelAsymmetryAllowed(params = {}) { + const level_asymmetry_allowed = params['level-asymmetry-allowed']; + + return level_asymmetry_allowed === 1 || level_asymmetry_allowed === '1'; + } + }, + { debug: 5 }, + ], + 5: [ + function (require, module, exports) { + (function (process) { + (function () { + /* eslint-env browser */ + + /** + * This is the web browser implementation of `debug()`. + */ + + exports.formatArgs = formatArgs; + exports.save = save; + exports.load = load; + exports.useColors = useColors; + exports.storage = localstorage(); + exports.destroy = (() => { + let warned = false; + + return () => { + if (!warned) { + warned = true; + console.warn( + 'Instance method `debug.destroy()` is deprecated and no longer does anything. It will be removed in the next major version of `debug`.', + ); + } + }; + })(); + + /** + * Colors. + */ + + exports.colors = [ + '#0000CC', + '#0000FF', + '#0033CC', + '#0033FF', + '#0066CC', + '#0066FF', + '#0099CC', + '#0099FF', + '#00CC00', + '#00CC33', + '#00CC66', + '#00CC99', + '#00CCCC', + '#00CCFF', + '#3300CC', + '#3300FF', + '#3333CC', + '#3333FF', + '#3366CC', + '#3366FF', + '#3399CC', + '#3399FF', + '#33CC00', + '#33CC33', + '#33CC66', + '#33CC99', + '#33CCCC', + '#33CCFF', + '#6600CC', + '#6600FF', + '#6633CC', + '#6633FF', + '#66CC00', + '#66CC33', + '#9900CC', + '#9900FF', + '#9933CC', + '#9933FF', + '#99CC00', + '#99CC33', + '#CC0000', + '#CC0033', + '#CC0066', + '#CC0099', + '#CC00CC', + '#CC00FF', + '#CC3300', + '#CC3333', + '#CC3366', + '#CC3399', + '#CC33CC', + '#CC33FF', + '#CC6600', + '#CC6633', + '#CC9900', + '#CC9933', + '#CCCC00', + '#CCCC33', + '#FF0000', + '#FF0033', + '#FF0066', + '#FF0099', + '#FF00CC', + '#FF00FF', + '#FF3300', + '#FF3333', + '#FF3366', + '#FF3399', + '#FF33CC', + '#FF33FF', + '#FF6600', + '#FF6633', + '#FF9900', + '#FF9933', + '#FFCC00', + '#FFCC33', + ]; + + /** + * Currently only WebKit-based Web Inspectors, Firefox >= v31, + * and the Firebug extension (any Firefox version) are known + * to support "%c" CSS customizations. + * + * TODO: add a `localStorage` variable to explicitly enable/disable colors + */ + + // eslint-disable-next-line complexity + function useColors() { + // NB: In an Electron preload script, document will be defined but not fully + // initialized. Since we know we're in Chrome, we'll just detect this case + // explicitly + if ( + typeof window !== 'undefined' && + window.process && + (window.process.type === 'renderer' || window.process.__nwjs) + ) { + return true; + } + + // Internet Explorer and Edge do not support colors. + if ( + typeof navigator !== 'undefined' && + navigator.userAgent && + navigator.userAgent.toLowerCase().match(/(edge|trident)\/(\d+)/) + ) { + return false; + } + + // Is webkit? http://stackoverflow.com/a/16459606/376773 + // document is undefined in react-native: https://github.com/facebook/react-native/pull/1632 + return ( + (typeof document !== 'undefined' && + document.documentElement && + document.documentElement.style && + document.documentElement.style.WebkitAppearance) || + // Is firebug? http://stackoverflow.com/a/398120/376773 + (typeof window !== 'undefined' && + window.console && + (window.console.firebug || (window.console.exception && window.console.table))) || + // Is firefox >= v31? + // https://developer.mozilla.org/en-US/docs/Tools/Web_Console#Styling_messages + (typeof navigator !== 'undefined' && + navigator.userAgent && + navigator.userAgent.toLowerCase().match(/firefox\/(\d+)/) && + parseInt(RegExp.$1, 10) >= 31) || + // Double check webkit in userAgent just in case we are in a worker + (typeof navigator !== 'undefined' && + navigator.userAgent && + navigator.userAgent.toLowerCase().match(/applewebkit\/(\d+)/)) + ); + } + + /** + * Colorize log arguments if enabled. + * + * @api public + */ + + function formatArgs(args) { + args[0] = + (this.useColors ? '%c' : '') + + this.namespace + + (this.useColors ? ' %c' : ' ') + + args[0] + + (this.useColors ? '%c ' : ' ') + + '+' + + module.exports.humanize(this.diff); + + if (!this.useColors) { + return; + } + + const c = 'color: ' + this.color; + args.splice(1, 0, c, 'color: inherit'); + + // The final "%c" is somewhat tricky, because there could be other + // arguments passed either before or after the %c, so we need to + // figure out the correct index to insert the CSS into + let index = 0; + let lastC = 0; + args[0].replace(/%[a-zA-Z%]/g, (match) => { + if (match === '%%') { + return; + } + index++; + if (match === '%c') { + // We only are interested in the *last* %c + // (the user may have provided their own) + lastC = index; + } + }); + + args.splice(lastC, 0, c); + } + + /** + * Invokes `console.debug()` when available. + * No-op when `console.debug` is not a "function". + * If `console.debug` is not available, falls back + * to `console.log`. + * + * @api public + */ + exports.log = console.debug || console.log || (() => {}); + + /** + * Save `namespaces`. + * + * @param {String} namespaces + * @api private + */ + function save(namespaces) { + try { + if (namespaces) { + exports.storage.setItem('debug', namespaces); + } else { + exports.storage.removeItem('debug'); + } + } catch (error) { + // Swallow + // XXX (@Qix-) should we be logging these? + } + } + + /** + * Load `namespaces`. + * + * @return {String} returns the previously persisted debug modes + * @api private + */ + function load() { + let r; + try { + r = exports.storage.getItem('debug'); + } catch (error) { + // Swallow + // XXX (@Qix-) should we be logging these? + } + + // If debug isn't set in LS, and we're in Electron, try to load $DEBUG + if (!r && typeof process !== 'undefined' && 'env' in process) { + r = process.env.DEBUG; + } + + return r; + } + + /** + * Localstorage attempts to return the localstorage. + * + * This is necessary because safari throws + * when a user disables cookies/localstorage + * and you attempt to access it. + * + * @return {LocalStorage} + * @api private + */ + + function localstorage() { + try { + // TVMLKit (Apple TV JS Runtime) does not have a window object, just localStorage in the global context + // The Browser also has localStorage in the global context. + return localStorage; + } catch (error) { + // Swallow + // XXX (@Qix-) should we be logging these? + } + } + + module.exports = require('./common')(exports); + + const { formatters } = module.exports; + + /** + * Map %j to `JSON.stringify()`, since no Web Inspectors do that by default. + */ + + formatters.j = function (v) { + try { + return JSON.stringify(v); + } catch (error) { + return '[UnexpectedJSONParseError]: ' + error.message; + } + }; + }.call(this)); + }.call(this, require('_process'))); + }, + { './common': 6, _process: 48 }, + ], + 6: [ + function (require, module, exports) { + /** + * This is the common logic for both the Node.js and web browser + * implementations of `debug()`. + */ + + function setup(env) { + createDebug.debug = createDebug; + createDebug.default = createDebug; + createDebug.coerce = coerce; + createDebug.disable = disable; + createDebug.enable = enable; + createDebug.enabled = enabled; + createDebug.humanize = require('ms'); + createDebug.destroy = destroy; + + Object.keys(env).forEach((key) => { + createDebug[key] = env[key]; + }); + + /** + * The currently active debug mode names, and names to skip. + */ + + createDebug.names = []; + createDebug.skips = []; + + /** + * Map of special "%n" handling functions, for the debug "format" argument. + * + * Valid key names are a single, lower or upper-case letter, i.e. "n" and "N". + */ + createDebug.formatters = {}; + + /** + * Selects a color for a debug namespace + * @param {String} namespace The namespace string for the for the debug instance to be colored + * @return {Number|String} An ANSI color code for the given namespace + * @api private + */ + function selectColor(namespace) { + let hash = 0; + + for (let i = 0; i < namespace.length; i++) { + hash = (hash << 5) - hash + namespace.charCodeAt(i); + hash |= 0; // Convert to 32bit integer + } + + return createDebug.colors[Math.abs(hash) % createDebug.colors.length]; + } + createDebug.selectColor = selectColor; + + /** + * Create a debugger with the given `namespace`. + * + * @param {String} namespace + * @return {Function} + * @api public + */ + function createDebug(namespace) { + let prevTime; + let enableOverride = null; + let namespacesCache; + let enabledCache; + + function debug(...args) { + // Disabled? + if (!debug.enabled) { + return; + } + + const self = debug; + + // Set `diff` timestamp + const curr = Number(new Date()); + const ms = curr - (prevTime || curr); + self.diff = ms; + self.prev = prevTime; + self.curr = curr; + prevTime = curr; + + args[0] = createDebug.coerce(args[0]); + + if (typeof args[0] !== 'string') { + // Anything else let's inspect with %O + args.unshift('%O'); + } + + // Apply any `formatters` transformations + let index = 0; + args[0] = args[0].replace(/%([a-zA-Z%])/g, (match, format) => { + // If we encounter an escaped % then don't increase the array index + if (match === '%%') { + return '%'; + } + index++; + const formatter = createDebug.formatters[format]; + if (typeof formatter === 'function') { + const val = args[index]; + match = formatter.call(self, val); + + // Now we need to remove `args[index]` since it's inlined in the `format` + args.splice(index, 1); + index--; + } + return match; + }); + + // Apply env-specific formatting (colors, etc.) + createDebug.formatArgs.call(self, args); + + const logFn = self.log || createDebug.log; + logFn.apply(self, args); + } + + debug.namespace = namespace; + debug.useColors = createDebug.useColors(); + debug.color = createDebug.selectColor(namespace); + debug.extend = extend; + debug.destroy = createDebug.destroy; // XXX Temporary. Will be removed in the next major release. + + Object.defineProperty(debug, 'enabled', { + enumerable: true, + configurable: false, + get: () => { + if (enableOverride !== null) { + return enableOverride; + } + if (namespacesCache !== createDebug.namespaces) { + namespacesCache = createDebug.namespaces; + enabledCache = createDebug.enabled(namespace); + } + + return enabledCache; + }, + set: (v) => { + enableOverride = v; + }, + }); + + // Env-specific initialization logic for debug instances + if (typeof createDebug.init === 'function') { + createDebug.init(debug); + } + + return debug; + } + + function extend(namespace, delimiter) { + const newDebug = createDebug( + this.namespace + (typeof delimiter === 'undefined' ? ':' : delimiter) + namespace, + ); + newDebug.log = this.log; + return newDebug; + } + + /** + * Enables a debug mode by namespaces. This can include modes + * separated by a colon and wildcards. + * + * @param {String} namespaces + * @api public + */ + function enable(namespaces) { + createDebug.save(namespaces); + createDebug.namespaces = namespaces; + + createDebug.names = []; + createDebug.skips = []; + + let i; + const split = (typeof namespaces === 'string' ? namespaces : '').split(/[\s,]+/); + const len = split.length; + + for (i = 0; i < len; i++) { + if (!split[i]) { + // ignore empty strings + continue; + } + + namespaces = split[i].replace(/\*/g, '.*?'); + + if (namespaces[0] === '-') { + createDebug.skips.push(new RegExp('^' + namespaces.substr(1) + '$')); + } else { + createDebug.names.push(new RegExp('^' + namespaces + '$')); + } + } + } + + /** + * Disable debug output. + * + * @return {String} namespaces + * @api public + */ + function disable() { + const namespaces = [ + ...createDebug.names.map(toNamespace), + ...createDebug.skips.map(toNamespace).map((namespace) => '-' + namespace), + ].join(','); + createDebug.enable(''); + return namespaces; + } + + /** + * Returns true if the given mode name is enabled, false otherwise. + * + * @param {String} name + * @return {Boolean} + * @api public + */ + function enabled(name) { + if (name[name.length - 1] === '*') { + return true; + } + + let i; + let len; + + for (i = 0, len = createDebug.skips.length; i < len; i++) { + if (createDebug.skips[i].test(name)) { + return false; + } + } + + for (i = 0, len = createDebug.names.length; i < len; i++) { + if (createDebug.names[i].test(name)) { + return true; + } + } + + return false; + } + + /** + * Convert regexp to namespace + * + * @param {RegExp} regxep + * @return {String} namespace + * @api private + */ + function toNamespace(regexp) { + return regexp + .toString() + .substring(2, regexp.toString().length - 2) + .replace(/\.\*\?$/, '*'); + } + + /** + * Coerce `val`. + * + * @param {Mixed} val + * @return {Mixed} + * @api private + */ + function coerce(val) { + if (val instanceof Error) { + return val.stack || val.message; + } + return val; + } + + /** + * XXX DO NOT USE. This is a temporary stub function. + * XXX It WILL be removed in the next major release. + */ + function destroy() { + console.warn( + 'Instance method `debug.destroy()` is deprecated and no longer does anything. It will be removed in the next major version of `debug`.', + ); + } + + createDebug.enable(createDebug.load()); + + return createDebug; + } + + module.exports = setup; + }, + { ms: 7 }, + ], + 7: [ + function (require, module, exports) { + /** + * Helpers. + */ + + var s = 1000; + var m = s * 60; + var h = m * 60; + var d = h * 24; + var w = d * 7; + var y = d * 365.25; + + /** + * Parse or format the given `val`. + * + * Options: + * + * - `long` verbose formatting [false] + * + * @param {String|Number} val + * @param {Object} [options] + * @throws {Error} throw an error if val is not a non-empty string or a number + * @return {String|Number} + * @api public + */ + + module.exports = function (val, options) { + options = options || {}; + var type = typeof val; + if (type === 'string' && val.length > 0) { + return parse(val); + } else if (type === 'number' && isFinite(val)) { + return options.long ? fmtLong(val) : fmtShort(val); + } + throw new Error('val is not a non-empty string or a valid number. val=' + JSON.stringify(val)); + }; + + /** + * Parse the given `str` and return milliseconds. + * + * @param {String} str + * @return {Number} + * @api private + */ + + function parse(str) { + str = String(str); + if (str.length > 100) { + return; + } + var match = + /^(-?(?:\d+)?\.?\d+) *(milliseconds?|msecs?|ms|seconds?|secs?|s|minutes?|mins?|m|hours?|hrs?|h|days?|d|weeks?|w|years?|yrs?|y)?$/i.exec( + str, + ); + if (!match) { + return; + } + var n = parseFloat(match[1]); + var type = (match[2] || 'ms').toLowerCase(); + switch (type) { + case 'years': + case 'year': + case 'yrs': + case 'yr': + case 'y': + return n * y; + case 'weeks': + case 'week': + case 'w': + return n * w; + case 'days': + case 'day': + case 'd': + return n * d; + case 'hours': + case 'hour': + case 'hrs': + case 'hr': + case 'h': + return n * h; + case 'minutes': + case 'minute': + case 'mins': + case 'min': + case 'm': + return n * m; + case 'seconds': + case 'second': + case 'secs': + case 'sec': + case 's': + return n * s; + case 'milliseconds': + case 'millisecond': + case 'msecs': + case 'msec': + case 'ms': + return n; + default: + return undefined; + } + } + + /** + * Short format for `ms`. + * + * @param {Number} ms + * @return {String} + * @api private + */ + + function fmtShort(ms) { + var msAbs = Math.abs(ms); + if (msAbs >= d) { + return Math.round(ms / d) + 'd'; + } + if (msAbs >= h) { + return Math.round(ms / h) + 'h'; + } + if (msAbs >= m) { + return Math.round(ms / m) + 'm'; + } + if (msAbs >= s) { + return Math.round(ms / s) + 's'; + } + return ms + 'ms'; + } + + /** + * Long format for `ms`. + * + * @param {Number} ms + * @return {String} + * @api private + */ + + function fmtLong(ms) { + var msAbs = Math.abs(ms); + if (msAbs >= d) { + return plural(ms, msAbs, d, 'day'); + } + if (msAbs >= h) { + return plural(ms, msAbs, h, 'hour'); + } + if (msAbs >= m) { + return plural(ms, msAbs, m, 'minute'); + } + if (msAbs >= s) { + return plural(ms, msAbs, s, 'second'); + } + return ms + ' ms'; + } + + /** + * Pluralization helper. + */ + + function plural(ms, msAbs, n, name) { + var isPlural = msAbs >= n * 1.5; + return Math.round(ms / n) + ' ' + name + (isPlural ? 's' : ''); + } + }, + {}, + ], + 8: [ + function (require, module, exports) { + 'use strict'; + Object.defineProperty(exports, '__esModule', { value: true }); + exports.Consumer = void 0; + const Logger_1 = require('./Logger'); + const EnhancedEventEmitter_1 = require('./EnhancedEventEmitter'); + const errors_1 = require('./errors'); + const logger = new Logger_1.Logger('Consumer'); + class Consumer extends EnhancedEventEmitter_1.EnhancedEventEmitter { + /** + * @emits transportclose + * @emits trackended + * @emits @getstats + * @emits @close + */ + constructor({ id, localId, producerId, rtpReceiver, track, rtpParameters, appData }) { + super(); + // Closed flag. + this._closed = false; + // Observer instance. + this._observer = new EnhancedEventEmitter_1.EnhancedEventEmitter(); + logger.debug('constructor()'); + this._id = id; + this._localId = localId; + this._producerId = producerId; + this._rtpReceiver = rtpReceiver; + this._track = track; + this._rtpParameters = rtpParameters; + this._paused = !track.enabled; + this._appData = appData; + this._onTrackEnded = this._onTrackEnded.bind(this); + this._handleTrack(); + } + /** + * Consumer id. + */ + get id() { + return this._id; + } + /** + * Local id. + */ + get localId() { + return this._localId; + } + /** + * Associated Producer id. + */ + get producerId() { + return this._producerId; + } + /** + * Whether the Consumer is closed. + */ + get closed() { + return this._closed; + } + /** + * Media kind. + */ + get kind() { + return this._track.kind; + } + /** + * Associated RTCRtpReceiver. + */ + get rtpReceiver() { + return this._rtpReceiver; + } + /** + * The associated track. + */ + get track() { + return this._track; + } + /** + * RTP parameters. + */ + get rtpParameters() { + return this._rtpParameters; + } + /** + * Whether the Consumer is paused. + */ + get paused() { + return this._paused; + } + /** + * App custom data. + */ + get appData() { + return this._appData; + } + /** + * Invalid setter. + */ + set appData(appData) { + throw new Error('cannot override appData object'); + } + /** + * Observer. + * + * @emits close + * @emits pause + * @emits resume + * @emits trackended + */ + get observer() { + return this._observer; + } + /** + * Closes the Consumer. + */ + close() { + if (this._closed) return; + logger.debug('close()'); + this._closed = true; + this._destroyTrack(); + this.emit('@close'); + // Emit observer event. + this._observer.safeEmit('close'); + } + /** + * Transport was closed. + */ + transportClosed() { + if (this._closed) return; + logger.debug('transportClosed()'); + this._closed = true; + this._destroyTrack(); + this.safeEmit('transportclose'); + // Emit observer event. + this._observer.safeEmit('close'); + } + /** + * Get associated RTCRtpReceiver stats. + */ + async getStats() { + if (this._closed) throw new errors_1.InvalidStateError('closed'); + return this.safeEmitAsPromise('@getstats'); + } + /** + * Pauses receiving media. + */ + pause() { + logger.debug('pause()'); + if (this._closed) { + logger.error('pause() | Consumer closed'); + return; + } + this._paused = true; + this._track.enabled = false; + // Emit observer event. + this._observer.safeEmit('pause'); + } + /** + * Resumes receiving media. + */ + resume() { + logger.debug('resume()'); + if (this._closed) { + logger.error('resume() | Consumer closed'); + return; + } + this._paused = false; + this._track.enabled = true; + // Emit observer event. + this._observer.safeEmit('resume'); + } + _onTrackEnded() { + logger.debug('track "ended" event'); + this.safeEmit('trackended'); + // Emit observer event. + this._observer.safeEmit('trackended'); + } + _handleTrack() { + this._track.addEventListener('ended', this._onTrackEnded); + } + _destroyTrack() { + try { + this._track.removeEventListener('ended', this._onTrackEnded); + this._track.stop(); + } catch (error) {} + } + } + exports.Consumer = Consumer; + }, + { './EnhancedEventEmitter': 12, './Logger': 13, './errors': 18 }, + ], + 9: [ + function (require, module, exports) { + 'use strict'; + Object.defineProperty(exports, '__esModule', { value: true }); + exports.DataConsumer = void 0; + const Logger_1 = require('./Logger'); + const EnhancedEventEmitter_1 = require('./EnhancedEventEmitter'); + const logger = new Logger_1.Logger('DataConsumer'); + class DataConsumer extends EnhancedEventEmitter_1.EnhancedEventEmitter { + /** + * @emits transportclose + * @emits open + * @emits error - (error: Error) + * @emits close + * @emits message - (message: any) + * @emits @close + */ + constructor({ id, dataProducerId, dataChannel, sctpStreamParameters, appData }) { + super(); + // Closed flag. + this._closed = false; + // Observer instance. + this._observer = new EnhancedEventEmitter_1.EnhancedEventEmitter(); + logger.debug('constructor()'); + this._id = id; + this._dataProducerId = dataProducerId; + this._dataChannel = dataChannel; + this._sctpStreamParameters = sctpStreamParameters; + this._appData = appData; + this._handleDataChannel(); + } + /** + * DataConsumer id. + */ + get id() { + return this._id; + } + /** + * Associated DataProducer id. + */ + get dataProducerId() { + return this._dataProducerId; + } + /** + * Whether the DataConsumer is closed. + */ + get closed() { + return this._closed; + } + /** + * SCTP stream parameters. + */ + get sctpStreamParameters() { + return this._sctpStreamParameters; + } + /** + * DataChannel readyState. + */ + get readyState() { + return this._dataChannel.readyState; + } + /** + * DataChannel label. + */ + get label() { + return this._dataChannel.label; + } + /** + * DataChannel protocol. + */ + get protocol() { + return this._dataChannel.protocol; + } + /** + * DataChannel binaryType. + */ + get binaryType() { + return this._dataChannel.binaryType; + } + /** + * Set DataChannel binaryType. + */ + set binaryType(binaryType) { + this._dataChannel.binaryType = binaryType; + } + /** + * App custom data. + */ + get appData() { + return this._appData; + } + /** + * Invalid setter. + */ + set appData(appData) { + throw new Error('cannot override appData object'); + } + /** + * Observer. + * + * @emits close + */ + get observer() { + return this._observer; + } + /** + * Closes the DataConsumer. + */ + close() { + if (this._closed) return; + logger.debug('close()'); + this._closed = true; + this._dataChannel.close(); + this.emit('@close'); + // Emit observer event. + this._observer.safeEmit('close'); + } + /** + * Transport was closed. + */ + transportClosed() { + if (this._closed) return; + logger.debug('transportClosed()'); + this._closed = true; + this._dataChannel.close(); + this.safeEmit('transportclose'); + // Emit observer event. + this._observer.safeEmit('close'); + } + _handleDataChannel() { + this._dataChannel.addEventListener('open', () => { + if (this._closed) return; + logger.debug('DataChannel "open" event'); + this.safeEmit('open'); + }); + this._dataChannel.addEventListener('error', (event) => { + if (this._closed) return; + let { error } = event; + if (!error) error = new Error('unknown DataChannel error'); + if (error.errorDetail === 'sctp-failure') { + logger.error( + 'DataChannel SCTP error [sctpCauseCode:%s]: %s', + error.sctpCauseCode, + error.message, + ); + } else { + logger.error('DataChannel "error" event: %o', error); + } + this.safeEmit('error', error); + }); + this._dataChannel.addEventListener('close', () => { + if (this._closed) return; + logger.warn('DataChannel "close" event'); + this._closed = true; + this.emit('@close'); + this.safeEmit('close'); + }); + this._dataChannel.addEventListener('message', (event) => { + if (this._closed) return; + this.safeEmit('message', event.data); + }); + } + } + exports.DataConsumer = DataConsumer; + }, + { './EnhancedEventEmitter': 12, './Logger': 13 }, + ], + 10: [ + function (require, module, exports) { + 'use strict'; + Object.defineProperty(exports, '__esModule', { value: true }); + exports.DataProducer = void 0; + const Logger_1 = require('./Logger'); + const EnhancedEventEmitter_1 = require('./EnhancedEventEmitter'); + const errors_1 = require('./errors'); + const logger = new Logger_1.Logger('DataProducer'); + class DataProducer extends EnhancedEventEmitter_1.EnhancedEventEmitter { + /** + * @emits transportclose + * @emits open + * @emits error - (error: Error) + * @emits close + * @emits bufferedamountlow + * @emits @close + */ + constructor({ id, dataChannel, sctpStreamParameters, appData }) { + super(); + // Closed flag. + this._closed = false; + // Observer instance. + this._observer = new EnhancedEventEmitter_1.EnhancedEventEmitter(); + logger.debug('constructor()'); + this._id = id; + this._dataChannel = dataChannel; + this._sctpStreamParameters = sctpStreamParameters; + this._appData = appData; + this._handleDataChannel(); + } + /** + * DataProducer id. + */ + get id() { + return this._id; + } + /** + * Whether the DataProducer is closed. + */ + get closed() { + return this._closed; + } + /** + * SCTP stream parameters. + */ + get sctpStreamParameters() { + return this._sctpStreamParameters; + } + /** + * DataChannel readyState. + */ + get readyState() { + return this._dataChannel.readyState; + } + /** + * DataChannel label. + */ + get label() { + return this._dataChannel.label; + } + /** + * DataChannel protocol. + */ + get protocol() { + return this._dataChannel.protocol; + } + /** + * DataChannel bufferedAmount. + */ + get bufferedAmount() { + return this._dataChannel.bufferedAmount; + } + /** + * DataChannel bufferedAmountLowThreshold. + */ + get bufferedAmountLowThreshold() { + return this._dataChannel.bufferedAmountLowThreshold; + } + /** + * Set DataChannel bufferedAmountLowThreshold. + */ + set bufferedAmountLowThreshold(bufferedAmountLowThreshold) { + this._dataChannel.bufferedAmountLowThreshold = bufferedAmountLowThreshold; + } + /** + * App custom data. + */ + get appData() { + return this._appData; + } + /** + * Invalid setter. + */ + set appData(appData) { + throw new Error('cannot override appData object'); + } + /** + * Observer. + * + * @emits close + */ + get observer() { + return this._observer; + } + /** + * Closes the DataProducer. + */ + close() { + if (this._closed) return; + logger.debug('close()'); + this._closed = true; + this._dataChannel.close(); + this.emit('@close'); + // Emit observer event. + this._observer.safeEmit('close'); + } + /** + * Transport was closed. + */ + transportClosed() { + if (this._closed) return; + logger.debug('transportClosed()'); + this._closed = true; + this._dataChannel.close(); + this.safeEmit('transportclose'); + // Emit observer event. + this._observer.safeEmit('close'); + } + /** + * Send a message. + * + * @param {String|Blob|ArrayBuffer|ArrayBufferView} data. + */ + send(data) { + logger.debug('send()'); + if (this._closed) throw new errors_1.InvalidStateError('closed'); + this._dataChannel.send(data); + } + _handleDataChannel() { + this._dataChannel.addEventListener('open', () => { + if (this._closed) return; + logger.debug('DataChannel "open" event'); + this.safeEmit('open'); + }); + this._dataChannel.addEventListener('error', (event) => { + if (this._closed) return; + let { error } = event; + if (!error) error = new Error('unknown DataChannel error'); + if (error.errorDetail === 'sctp-failure') { + logger.error( + 'DataChannel SCTP error [sctpCauseCode:%s]: %s', + error.sctpCauseCode, + error.message, + ); + } else { + logger.error('DataChannel "error" event: %o', error); + } + this.safeEmit('error', error); + }); + this._dataChannel.addEventListener('close', () => { + if (this._closed) return; + logger.warn('DataChannel "close" event'); + this._closed = true; + this.emit('@close'); + this.safeEmit('close'); + }); + this._dataChannel.addEventListener('message', () => { + if (this._closed) return; + logger.warn('DataChannel "message" event in a DataProducer, message discarded'); + }); + this._dataChannel.addEventListener('bufferedamountlow', () => { + if (this._closed) return; + this.safeEmit('bufferedamountlow'); + }); + } + } + exports.DataProducer = DataProducer; + }, + { './EnhancedEventEmitter': 12, './Logger': 13, './errors': 18 }, + ], + 11: [ + function (require, module, exports) { + 'use strict'; + /* global RTCRtpTransceiver */ + var __createBinding = + (this && this.__createBinding) || + (Object.create + ? function (o, m, k, k2) { + if (k2 === undefined) k2 = k; + Object.defineProperty(o, k2, { + enumerable: true, + get: function () { + return m[k]; + }, + }); + } + : function (o, m, k, k2) { + if (k2 === undefined) k2 = k; + o[k2] = m[k]; + }); + var __setModuleDefault = + (this && this.__setModuleDefault) || + (Object.create + ? function (o, v) { + Object.defineProperty(o, 'default', { enumerable: true, value: v }); + } + : function (o, v) { + o['default'] = v; + }); + var __importStar = + (this && this.__importStar) || + function (mod) { + if (mod && mod.__esModule) return mod; + var result = {}; + if (mod != null) + for (var k in mod) + if (k !== 'default' && Object.hasOwnProperty.call(mod, k)) + __createBinding(result, mod, k); + __setModuleDefault(result, mod); + return result; + }; + var __importDefault = + (this && this.__importDefault) || + function (mod) { + return mod && mod.__esModule ? mod : { default: mod }; + }; + Object.defineProperty(exports, '__esModule', { value: true }); + exports.Device = exports.detectDevice = void 0; + const bowser_1 = __importDefault(require('bowser')); + const Logger_1 = require('./Logger'); + const EnhancedEventEmitter_1 = require('./EnhancedEventEmitter'); + const errors_1 = require('./errors'); + const utils = __importStar(require('./utils')); + const ortc = __importStar(require('./ortc')); + const Transport_1 = require('./Transport'); + const Chrome74_1 = require('./handlers/Chrome74'); + const Chrome70_1 = require('./handlers/Chrome70'); + const Chrome67_1 = require('./handlers/Chrome67'); + const Chrome55_1 = require('./handlers/Chrome55'); + const Firefox60_1 = require('./handlers/Firefox60'); + const Safari12_1 = require('./handlers/Safari12'); + const Safari11_1 = require('./handlers/Safari11'); + const Edge11_1 = require('./handlers/Edge11'); + const ReactNative_1 = require('./handlers/ReactNative'); + const logger = new Logger_1.Logger('Device'); + function detectDevice() { + // React-Native. + // NOTE: react-native-webrtc >= 1.75.0 is required. + if (typeof navigator === 'object' && navigator.product === 'ReactNative') { + if (typeof RTCPeerConnection === 'undefined') { + logger.warn('this._detectDevice() | unsupported ReactNative without RTCPeerConnection'); + return undefined; + } + logger.debug('this._detectDevice() | ReactNative handler chosen'); + return 'ReactNative'; + } + // Browser. + else if (typeof navigator === 'object' && typeof navigator.userAgent === 'string') { + const ua = navigator.userAgent; + const browser = bowser_1.default.getParser(ua); + const engine = browser.getEngine(); + // Chrome and Chromium. + if (browser.satisfies({ chrome: '>=74', chromium: '>=74' })) { + return 'Chrome74'; + } else if (browser.satisfies({ chrome: '>=70', chromium: '>=70' })) { + return 'Chrome70'; + } else if (browser.satisfies({ chrome: '>=67', chromium: '>=67' })) { + return 'Chrome67'; + } else if (browser.satisfies({ chrome: '>=55', chromium: '>=55' })) { + return 'Chrome55'; + } + // Firefox. + else if (browser.satisfies({ firefox: '>=60' })) { + return 'Firefox60'; + } + // Safari with Unified-Plan support enabled. + else if ( + browser.satisfies({ safari: '>=12.0' }) && + typeof RTCRtpTransceiver !== 'undefined' && + RTCRtpTransceiver.prototype.hasOwnProperty('currentDirection') + ) { + return 'Safari12'; + } + // Safari with Plab-B support. + else if (browser.satisfies({ safari: '>=11' })) { + return 'Safari11'; + } + // Old Edge with ORTC support. + else if ( + browser.satisfies({ 'microsoft edge': '>=11' }) && + browser.satisfies({ 'microsoft edge': '<=18' }) + ) { + return 'Edge11'; + } + // Best effort for Chromium based browsers. + else if (engine.name && engine.name.toLowerCase() === 'blink') { + const match = ua.match(/(?:(?:Chrome|Chromium))[ /](\w+)/i); + if (match) { + const version = Number(match[1]); + if (version >= 74) { + return 'Chrome74'; + } else if (version >= 70) { + return 'Chrome70'; + } else if (version >= 67) { + return 'Chrome67'; + } else { + return 'Chrome55'; + } + } else { + return 'Chrome74'; + } + } + // Unsupported browser. + else { + logger.warn( + 'this._detectDevice() | browser not supported [name:%s, version:%s]', + browser.getBrowserName(), + browser.getBrowserVersion(), + ); + return undefined; + } + } + // Unknown device. + else { + logger.warn('this._detectDevice() | unknown device'); + return undefined; + } + } + exports.detectDevice = detectDevice; + class Device { + /** + * Create a new Device to connect to mediasoup server. + * + * @throws {UnsupportedError} if device is not supported. + */ + constructor({ handlerName, handlerFactory, Handler } = {}) { + // Loaded flag. + this._loaded = false; + // Observer instance. + this._observer = new EnhancedEventEmitter_1.EnhancedEventEmitter(); + logger.debug('constructor()'); + // Handle deprecated option. + if (Handler) { + logger.warn( + 'constructor() | Handler option is DEPRECATED, use handlerName or handlerFactory instead', + ); + if (typeof Handler === 'string') handlerName = Handler; + else + throw new TypeError( + 'non string Handler option no longer supported, use handlerFactory instead', + ); + } + if (handlerName && handlerFactory) { + throw new TypeError('just one of handlerName or handlerInterface can be given'); + } + if (handlerFactory) { + this._handlerFactory = handlerFactory; + } else { + if (handlerName) { + logger.debug('constructor() | handler given: %s', handlerName); + } else { + handlerName = detectDevice(); + if (handlerName) logger.debug('constructor() | detected handler: %s', handlerName); + else throw new errors_1.UnsupportedError('device not supported'); + } + switch (handlerName) { + case 'Chrome74': + this._handlerFactory = Chrome74_1.Chrome74.createFactory(); + break; + case 'Chrome70': + this._handlerFactory = Chrome70_1.Chrome70.createFactory(); + break; + case 'Chrome67': + this._handlerFactory = Chrome67_1.Chrome67.createFactory(); + break; + case 'Chrome55': + this._handlerFactory = Chrome55_1.Chrome55.createFactory(); + break; + case 'Firefox60': + this._handlerFactory = Firefox60_1.Firefox60.createFactory(); + break; + case 'Safari12': + this._handlerFactory = Safari12_1.Safari12.createFactory(); + break; + case 'Safari11': + this._handlerFactory = Safari11_1.Safari11.createFactory(); + break; + case 'Edge11': + this._handlerFactory = Edge11_1.Edge11.createFactory(); + break; + case 'ReactNative': + this._handlerFactory = ReactNative_1.ReactNative.createFactory(); + break; + default: + throw new TypeError(`unknown handlerName "${handlerName}"`); + } + } + // Create a temporal handler to get its name. + const handler = this._handlerFactory(); + this._handlerName = handler.name; + handler.close(); + this._extendedRtpCapabilities = undefined; + this._recvRtpCapabilities = undefined; + this._canProduceByKind = { + audio: false, + video: false, + }; + this._sctpCapabilities = undefined; + } + /** + * The RTC handler name. + */ + get handlerName() { + return this._handlerName; + } + /** + * Whether the Device is loaded. + */ + get loaded() { + return this._loaded; + } + /** + * RTP capabilities of the Device for receiving media. + * + * @throws {InvalidStateError} if not loaded. + */ + get rtpCapabilities() { + if (!this._loaded) throw new errors_1.InvalidStateError('not loaded'); + return this._recvRtpCapabilities; + } + /** + * SCTP capabilities of the Device. + * + * @throws {InvalidStateError} if not loaded. + */ + get sctpCapabilities() { + if (!this._loaded) throw new errors_1.InvalidStateError('not loaded'); + return this._sctpCapabilities; + } + /** + * Observer. + * + * @emits newtransport - (transport: Transport) + */ + get observer() { + return this._observer; + } + /** + * Initialize the Device. + */ + async load({ routerRtpCapabilities }) { + logger.debug('load() [routerRtpCapabilities:%o]', routerRtpCapabilities); + routerRtpCapabilities = utils.clone(routerRtpCapabilities, undefined); + // Temporal handler to get its capabilities. + let handler; + try { + if (this._loaded) throw new errors_1.InvalidStateError('already loaded'); + // This may throw. + ortc.validateRtpCapabilities(routerRtpCapabilities); + handler = this._handlerFactory(); + const nativeRtpCapabilities = await handler.getNativeRtpCapabilities(); + logger.debug('load() | got native RTP capabilities:%o', nativeRtpCapabilities); + // This may throw. + ortc.validateRtpCapabilities(nativeRtpCapabilities); + // Get extended RTP capabilities. + this._extendedRtpCapabilities = ortc.getExtendedRtpCapabilities( + nativeRtpCapabilities, + routerRtpCapabilities, + ); + logger.debug('load() | got extended RTP capabilities:%o', this._extendedRtpCapabilities); + // Check whether we can produce audio/video. + this._canProduceByKind.audio = ortc.canSend('audio', this._extendedRtpCapabilities); + this._canProduceByKind.video = ortc.canSend('video', this._extendedRtpCapabilities); + // Generate our receiving RTP capabilities for receiving media. + this._recvRtpCapabilities = ortc.getRecvRtpCapabilities(this._extendedRtpCapabilities); + // This may throw. + ortc.validateRtpCapabilities(this._recvRtpCapabilities); + logger.debug('load() | got receiving RTP capabilities:%o', this._recvRtpCapabilities); + // Generate our SCTP capabilities. + this._sctpCapabilities = await handler.getNativeSctpCapabilities(); + logger.debug('load() | got native SCTP capabilities:%o', this._sctpCapabilities); + // This may throw. + ortc.validateSctpCapabilities(this._sctpCapabilities); + logger.debug('load() succeeded'); + this._loaded = true; + handler.close(); + } catch (error) { + if (handler) handler.close(); + throw error; + } + } + /** + * Whether we can produce audio/video. + * + * @throws {InvalidStateError} if not loaded. + * @throws {TypeError} if wrong arguments. + */ + canProduce(kind) { + if (!this._loaded) throw new errors_1.InvalidStateError('not loaded'); + else if (kind !== 'audio' && kind !== 'video') throw new TypeError(`invalid kind "${kind}"`); + return this._canProduceByKind[kind]; + } + /** + * Creates a Transport for sending media. + * + * @throws {InvalidStateError} if not loaded. + * @throws {TypeError} if wrong arguments. + */ + createSendTransport({ + id, + iceParameters, + iceCandidates, + dtlsParameters, + sctpParameters, + iceServers, + iceTransportPolicy, + additionalSettings, + proprietaryConstraints, + appData = {}, + }) { + logger.debug('createSendTransport()'); + return this._createTransport({ + direction: 'send', + id: id, + iceParameters: iceParameters, + iceCandidates: iceCandidates, + dtlsParameters: dtlsParameters, + sctpParameters: sctpParameters, + iceServers: iceServers, + iceTransportPolicy: iceTransportPolicy, + additionalSettings: additionalSettings, + proprietaryConstraints: proprietaryConstraints, + appData: appData, + }); + } + /** + * Creates a Transport for receiving media. + * + * @throws {InvalidStateError} if not loaded. + * @throws {TypeError} if wrong arguments. + */ + createRecvTransport({ + id, + iceParameters, + iceCandidates, + dtlsParameters, + sctpParameters, + iceServers, + iceTransportPolicy, + additionalSettings, + proprietaryConstraints, + appData = {}, + }) { + logger.debug('createRecvTransport()'); + return this._createTransport({ + direction: 'recv', + id: id, + iceParameters: iceParameters, + iceCandidates: iceCandidates, + dtlsParameters: dtlsParameters, + sctpParameters: sctpParameters, + iceServers: iceServers, + iceTransportPolicy: iceTransportPolicy, + additionalSettings: additionalSettings, + proprietaryConstraints: proprietaryConstraints, + appData: appData, + }); + } + _createTransport({ + direction, + id, + iceParameters, + iceCandidates, + dtlsParameters, + sctpParameters, + iceServers, + iceTransportPolicy, + additionalSettings, + proprietaryConstraints, + appData = {}, + }) { + if (!this._loaded) throw new errors_1.InvalidStateError('not loaded'); + else if (typeof id !== 'string') throw new TypeError('missing id'); + else if (typeof iceParameters !== 'object') throw new TypeError('missing iceParameters'); + else if (!Array.isArray(iceCandidates)) throw new TypeError('missing iceCandidates'); + else if (typeof dtlsParameters !== 'object') throw new TypeError('missing dtlsParameters'); + else if (sctpParameters && typeof sctpParameters !== 'object') + throw new TypeError('wrong sctpParameters'); + else if (appData && typeof appData !== 'object') + throw new TypeError('if given, appData must be an object'); + // Create a new Transport. + const transport = new Transport_1.Transport({ + direction, + id, + iceParameters, + iceCandidates, + dtlsParameters, + sctpParameters, + iceServers, + iceTransportPolicy, + additionalSettings, + proprietaryConstraints, + appData, + handlerFactory: this._handlerFactory, + extendedRtpCapabilities: this._extendedRtpCapabilities, + canProduceByKind: this._canProduceByKind, + }); + // Emit observer event. + this._observer.safeEmit('newtransport', transport); + return transport; + } + } + exports.Device = Device; + }, + { + './EnhancedEventEmitter': 12, + './Logger': 13, + './Transport': 17, + './errors': 18, + './handlers/Chrome55': 19, + './handlers/Chrome67': 20, + './handlers/Chrome70': 21, + './handlers/Chrome74': 22, + './handlers/Edge11': 23, + './handlers/Firefox60': 24, + './handlers/ReactNative': 26, + './handlers/Safari11': 27, + './handlers/Safari12': 28, + './ortc': 36, + './utils': 39, + bowser: 3, + }, + ], + 12: [ + function (require, module, exports) { + 'use strict'; + Object.defineProperty(exports, '__esModule', { value: true }); + exports.EnhancedEventEmitter = void 0; + const events_1 = require('events'); + const Logger_1 = require('./Logger'); + const logger = new Logger_1.Logger('EnhancedEventEmitter'); + class EnhancedEventEmitter extends events_1.EventEmitter { + constructor() { + super(); + this.setMaxListeners(Infinity); + } + safeEmit(event, ...args) { + const numListeners = this.listenerCount(event); + try { + return this.emit(event, ...args); + } catch (error) { + logger.error('safeEmit() | event listener threw an error [event:%s]:%o', event, error); + return Boolean(numListeners); + } + } + async safeEmitAsPromise(event, ...args) { + return new Promise((resolve, reject) => { + try { + this.emit(event, ...args, resolve, reject); + } catch (error) { + logger.error( + 'safeEmitAsPromise() | event listener threw an error [event:%s]:%o', + event, + error, + ); + reject(error); + } + }); + } + } + exports.EnhancedEventEmitter = EnhancedEventEmitter; + }, + { './Logger': 13, events: 47 }, + ], + 13: [ + function (require, module, exports) { + 'use strict'; + var __importDefault = + (this && this.__importDefault) || + function (mod) { + return mod && mod.__esModule ? mod : { default: mod }; + }; + Object.defineProperty(exports, '__esModule', { value: true }); + exports.Logger = void 0; + const debug_1 = __importDefault(require('debug')); + const APP_NAME = 'mediasoup-client'; + class Logger { + constructor(prefix) { + if (prefix) { + this._debug = debug_1.default(`${APP_NAME}:${prefix}`); + this._warn = debug_1.default(`${APP_NAME}:WARN:${prefix}`); + this._error = debug_1.default(`${APP_NAME}:ERROR:${prefix}`); + } else { + this._debug = debug_1.default(APP_NAME); + this._warn = debug_1.default(`${APP_NAME}:WARN`); + this._error = debug_1.default(`${APP_NAME}:ERROR`); + } + /* eslint-disable no-console */ + this._debug.log = console.info.bind(console); + this._warn.log = console.warn.bind(console); + this._error.log = console.error.bind(console); + /* eslint-enable no-console */ + } + get debug() { + return this._debug; + } + get warn() { + return this._warn; + } + get error() { + return this._error; + } + } + exports.Logger = Logger; + }, + { debug: 40 }, + ], + 14: [ + function (require, module, exports) { + 'use strict'; + Object.defineProperty(exports, '__esModule', { value: true }); + exports.Producer = void 0; + const Logger_1 = require('./Logger'); + const EnhancedEventEmitter_1 = require('./EnhancedEventEmitter'); + const errors_1 = require('./errors'); + const logger = new Logger_1.Logger('Producer'); + class Producer extends EnhancedEventEmitter_1.EnhancedEventEmitter { + /** + * @emits transportclose + * @emits trackended + * @emits @replacetrack - (track: MediaStreamTrack | null) + * @emits @setmaxspatiallayer - (spatialLayer: string) + * @emits @setrtpencodingparameters - (params: any) + * @emits @getstats + * @emits @close + */ + constructor({ + id, + localId, + rtpSender, + track, + rtpParameters, + stopTracks, + disableTrackOnPause, + zeroRtpOnPause, + appData, + }) { + super(); + // Closed flag. + this._closed = false; + // Observer instance. + this._observer = new EnhancedEventEmitter_1.EnhancedEventEmitter(); + logger.debug('constructor()'); + this._id = id; + this._localId = localId; + this._rtpSender = rtpSender; + this._track = track; + this._kind = track.kind; + this._rtpParameters = rtpParameters; + this._paused = disableTrackOnPause ? !track.enabled : false; + this._maxSpatialLayer = undefined; + this._stopTracks = stopTracks; + this._disableTrackOnPause = disableTrackOnPause; + this._zeroRtpOnPause = zeroRtpOnPause; + this._appData = appData; + this._onTrackEnded = this._onTrackEnded.bind(this); + // NOTE: Minor issue. If zeroRtpOnPause is true, we cannot emit the + // '@replacetrack' event here, so RTCRtpSender.track won't be null. + this._handleTrack(); + } + /** + * Producer id. + */ + get id() { + return this._id; + } + /** + * Local id. + */ + get localId() { + return this._localId; + } + /** + * Whether the Producer is closed. + */ + get closed() { + return this._closed; + } + /** + * Media kind. + */ + get kind() { + return this._kind; + } + /** + * Associated RTCRtpSender. + */ + get rtpSender() { + return this._rtpSender; + } + /** + * The associated track. + */ + get track() { + return this._track; + } + /** + * RTP parameters. + */ + get rtpParameters() { + return this._rtpParameters; + } + /** + * Whether the Producer is paused. + */ + get paused() { + return this._paused; + } + /** + * Max spatial layer. + * + * @type {Number | undefined} + */ + get maxSpatialLayer() { + return this._maxSpatialLayer; + } + /** + * App custom data. + */ + get appData() { + return this._appData; + } + /** + * Invalid setter. + */ + set appData(appData) { + throw new Error('cannot override appData object'); + } + /** + * Observer. + * + * @emits close + * @emits pause + * @emits resume + * @emits trackended + */ + get observer() { + return this._observer; + } + /** + * Closes the Producer. + */ + close() { + if (this._closed) return; + logger.debug('close()'); + this._closed = true; + this._destroyTrack(); + this.emit('@close'); + // Emit observer event. + this._observer.safeEmit('close'); + } + /** + * Transport was closed. + */ + transportClosed() { + if (this._closed) return; + logger.debug('transportClosed()'); + this._closed = true; + this._destroyTrack(); + this.safeEmit('transportclose'); + // Emit observer event. + this._observer.safeEmit('close'); + } + /** + * Get associated RTCRtpSender stats. + */ + async getStats() { + if (this._closed) throw new errors_1.InvalidStateError('closed'); + return this.safeEmitAsPromise('@getstats'); + } + /** + * Pauses sending media. + */ + pause() { + logger.debug('pause()'); + if (this._closed) { + logger.error('pause() | Producer closed'); + return; + } + this._paused = true; + if (this._track && this._disableTrackOnPause) { + this._track.enabled = false; + } + if (this._zeroRtpOnPause) { + this.safeEmitAsPromise('@replacetrack', null).catch(() => {}); + } + // Emit observer event. + this._observer.safeEmit('pause'); + } + /** + * Resumes sending media. + */ + resume() { + logger.debug('resume()'); + if (this._closed) { + logger.error('resume() | Producer closed'); + return; + } + this._paused = false; + if (this._track && this._disableTrackOnPause) { + this._track.enabled = true; + } + if (this._zeroRtpOnPause) { + this.safeEmitAsPromise('@replacetrack', this._track).catch(() => {}); + } + // Emit observer event. + this._observer.safeEmit('resume'); + } + /** + * Replaces the current track with a new one or null. + */ + async replaceTrack({ track }) { + logger.debug('replaceTrack() [track:%o]', track); + if (this._closed) { + // This must be done here. Otherwise there is no chance to stop the given + // track. + if (track && this._stopTracks) { + try { + track.stop(); + } catch (error) {} + } + throw new errors_1.InvalidStateError('closed'); + } else if (track && track.readyState === 'ended') { + throw new errors_1.InvalidStateError('track ended'); + } + // Do nothing if this is the same track as the current handled one. + if (track === this._track) { + logger.debug('replaceTrack() | same track, ignored'); + return; + } + if (!this._zeroRtpOnPause || !this._paused) { + await this.safeEmitAsPromise('@replacetrack', track); + } + // Destroy the previous track. + this._destroyTrack(); + // Set the new track. + this._track = track; + // If this Producer was paused/resumed and the state of the new + // track does not match, fix it. + if (this._track && this._disableTrackOnPause) { + if (!this._paused) this._track.enabled = true; + else if (this._paused) this._track.enabled = false; + } + // Handle the effective track. + this._handleTrack(); + } + /** + * Sets the video max spatial layer to be sent. + */ + async setMaxSpatialLayer(spatialLayer) { + if (this._closed) throw new errors_1.InvalidStateError('closed'); + else if (this._kind !== 'video') throw new errors_1.UnsupportedError('not a video Producer'); + else if (typeof spatialLayer !== 'number') throw new TypeError('invalid spatialLayer'); + if (spatialLayer === this._maxSpatialLayer) return; + await this.safeEmitAsPromise('@setmaxspatiallayer', spatialLayer); + this._maxSpatialLayer = spatialLayer; + } + /** + * Sets the DSCP value. + */ + async setRtpEncodingParameters(params) { + if (this._closed) throw new errors_1.InvalidStateError('closed'); + else if (typeof params !== 'object') throw new TypeError('invalid params'); + await this.safeEmitAsPromise('@setrtpencodingparameters', params); + } + _onTrackEnded() { + logger.debug('track "ended" event'); + this.safeEmit('trackended'); + // Emit observer event. + this._observer.safeEmit('trackended'); + } + _handleTrack() { + if (!this._track) return; + this._track.addEventListener('ended', this._onTrackEnded); + } + _destroyTrack() { + if (!this._track) return; + try { + this._track.removeEventListener('ended', this._onTrackEnded); + // Just stop the track unless the app set stopTracks: false. + if (this._stopTracks) this._track.stop(); + } catch (error) {} + } + } + exports.Producer = Producer; + }, + { './EnhancedEventEmitter': 12, './Logger': 13, './errors': 18 }, + ], + 15: [ + function (require, module, exports) { + 'use strict'; + /** + * The RTP capabilities define what mediasoup or an endpoint can receive at + * media level. + */ + Object.defineProperty(exports, '__esModule', { value: true }); + }, + {}, + ], + 16: [ + function (require, module, exports) { + 'use strict'; + Object.defineProperty(exports, '__esModule', { value: true }); + }, + {}, + ], + 17: [ + function (require, module, exports) { + 'use strict'; + var __createBinding = + (this && this.__createBinding) || + (Object.create + ? function (o, m, k, k2) { + if (k2 === undefined) k2 = k; + Object.defineProperty(o, k2, { + enumerable: true, + get: function () { + return m[k]; + }, + }); + } + : function (o, m, k, k2) { + if (k2 === undefined) k2 = k; + o[k2] = m[k]; + }); + var __setModuleDefault = + (this && this.__setModuleDefault) || + (Object.create + ? function (o, v) { + Object.defineProperty(o, 'default', { enumerable: true, value: v }); + } + : function (o, v) { + o['default'] = v; + }); + var __importStar = + (this && this.__importStar) || + function (mod) { + if (mod && mod.__esModule) return mod; + var result = {}; + if (mod != null) + for (var k in mod) + if (k !== 'default' && Object.hasOwnProperty.call(mod, k)) + __createBinding(result, mod, k); + __setModuleDefault(result, mod); + return result; + }; + Object.defineProperty(exports, '__esModule', { value: true }); + exports.Transport = void 0; + const awaitqueue_1 = require('awaitqueue'); + const Logger_1 = require('./Logger'); + const EnhancedEventEmitter_1 = require('./EnhancedEventEmitter'); + const errors_1 = require('./errors'); + const utils = __importStar(require('./utils')); + const ortc = __importStar(require('./ortc')); + const Producer_1 = require('./Producer'); + const Consumer_1 = require('./Consumer'); + const DataProducer_1 = require('./DataProducer'); + const DataConsumer_1 = require('./DataConsumer'); + const logger = new Logger_1.Logger('Transport'); + class Transport extends EnhancedEventEmitter_1.EnhancedEventEmitter { + /** + * @emits connect - (transportLocalParameters: any, callback: Function, errback: Function) + * @emits connectionstatechange - (connectionState: ConnectionState) + * @emits produce - (producerLocalParameters: any, callback: Function, errback: Function) + * @emits producedata - (dataProducerLocalParameters: any, callback: Function, errback: Function) + */ + constructor({ + direction, + id, + iceParameters, + iceCandidates, + dtlsParameters, + sctpParameters, + iceServers, + iceTransportPolicy, + additionalSettings, + proprietaryConstraints, + appData, + handlerFactory, + extendedRtpCapabilities, + canProduceByKind, + }) { + super(); + // Closed flag. + this._closed = false; + // Transport connection state. + this._connectionState = 'new'; + // Map of Producers indexed by id. + this._producers = new Map(); + // Map of Consumers indexed by id. + this._consumers = new Map(); + // Map of DataProducers indexed by id. + this._dataProducers = new Map(); + // Map of DataConsumers indexed by id. + this._dataConsumers = new Map(); + // Whether the Consumer for RTP probation has been created. + this._probatorConsumerCreated = false; + // AwaitQueue instance to make async tasks happen sequentially. + this._awaitQueue = new awaitqueue_1.AwaitQueue({ + ClosedErrorClass: errors_1.InvalidStateError, + }); + // Observer instance. + this._observer = new EnhancedEventEmitter_1.EnhancedEventEmitter(); + logger.debug('constructor() [id:%s, direction:%s]', id, direction); + this._id = id; + this._direction = direction; + this._extendedRtpCapabilities = extendedRtpCapabilities; + this._canProduceByKind = canProduceByKind; + this._maxSctpMessageSize = sctpParameters ? sctpParameters.maxMessageSize : null; + // Clone and sanitize additionalSettings. + additionalSettings = utils.clone(additionalSettings, {}); + delete additionalSettings.iceServers; + delete additionalSettings.iceTransportPolicy; + delete additionalSettings.bundlePolicy; + delete additionalSettings.rtcpMuxPolicy; + delete additionalSettings.sdpSemantics; + this._handler = handlerFactory(); + this._handler.run({ + direction, + iceParameters, + iceCandidates, + dtlsParameters, + sctpParameters, + iceServers, + iceTransportPolicy, + additionalSettings, + proprietaryConstraints, + extendedRtpCapabilities, + }); + this._appData = appData; + this._handleHandler(); + } + /** + * Transport id. + */ + get id() { + return this._id; + } + /** + * Whether the Transport is closed. + */ + get closed() { + return this._closed; + } + /** + * Transport direction. + */ + get direction() { + return this._direction; + } + /** + * RTC handler instance. + */ + get handler() { + return this._handler; + } + /** + * Connection state. + */ + get connectionState() { + return this._connectionState; + } + /** + * App custom data. + */ + get appData() { + return this._appData; + } + /** + * Invalid setter. + */ + set appData(appData) { + throw new Error('cannot override appData object'); + } + /** + * Observer. + * + * @emits close + * @emits newproducer - (producer: Producer) + * @emits newconsumer - (producer: Producer) + * @emits newdataproducer - (dataProducer: DataProducer) + * @emits newdataconsumer - (dataProducer: DataProducer) + */ + get observer() { + return this._observer; + } + /** + * Close the Transport. + */ + close() { + if (this._closed) return; + logger.debug('close()'); + this._closed = true; + // Close the AwaitQueue. + this._awaitQueue.close(); + // Close the handler. + this._handler.close(); + // Close all Producers. + for (const producer of this._producers.values()) { + producer.transportClosed(); + } + this._producers.clear(); + // Close all Consumers. + for (const consumer of this._consumers.values()) { + consumer.transportClosed(); + } + this._consumers.clear(); + // Close all DataProducers. + for (const dataProducer of this._dataProducers.values()) { + dataProducer.transportClosed(); + } + this._dataProducers.clear(); + // Close all DataConsumers. + for (const dataConsumer of this._dataConsumers.values()) { + dataConsumer.transportClosed(); + } + this._dataConsumers.clear(); + // Emit observer event. + this._observer.safeEmit('close'); + } + /** + * Get associated Transport (RTCPeerConnection) stats. + * + * @returns {RTCStatsReport} + */ + async getStats() { + if (this._closed) throw new errors_1.InvalidStateError('closed'); + return this._handler.getTransportStats(); + } + /** + * Restart ICE connection. + */ + async restartIce({ iceParameters }) { + logger.debug('restartIce()'); + if (this._closed) throw new errors_1.InvalidStateError('closed'); + else if (!iceParameters) throw new TypeError('missing iceParameters'); + // Enqueue command. + return this._awaitQueue.push( + async () => this._handler.restartIce(iceParameters), + 'transport.restartIce()', + ); + } + /** + * Update ICE servers. + */ + async updateIceServers({ iceServers } = {}) { + logger.debug('updateIceServers()'); + if (this._closed) throw new errors_1.InvalidStateError('closed'); + else if (!Array.isArray(iceServers)) throw new TypeError('missing iceServers'); + // Enqueue command. + return this._awaitQueue.push( + async () => this._handler.updateIceServers(iceServers), + 'transport.updateIceServers()', + ); + } + /** + * Create a Producer. + */ + async produce({ + track, + encodings, + codecOptions, + codec, + stopTracks = true, + disableTrackOnPause = true, + zeroRtpOnPause = false, + appData = {}, + } = {}) { + logger.debug('produce() [track:%o]', track); + if (!track) throw new TypeError('missing track'); + else if (this._direction !== 'send') + throw new errors_1.UnsupportedError('not a sending Transport'); + else if (!this._canProduceByKind[track.kind]) + throw new errors_1.UnsupportedError(`cannot produce ${track.kind}`); + else if (track.readyState === 'ended') throw new errors_1.InvalidStateError('track ended'); + else if (this.listenerCount('connect') === 0 && this._connectionState === 'new') + throw new TypeError('no "connect" listener set into this transport'); + else if (this.listenerCount('produce') === 0) + throw new TypeError('no "produce" listener set into this transport'); + else if (appData && typeof appData !== 'object') + throw new TypeError('if given, appData must be an object'); + // Enqueue command. + return ( + this._awaitQueue + .push(async () => { + let normalizedEncodings; + if (encodings && !Array.isArray(encodings)) { + throw TypeError('encodings must be an array'); + } else if (encodings && encodings.length === 0) { + normalizedEncodings = undefined; + } else if (encodings) { + normalizedEncodings = encodings.map((encoding) => { + const normalizedEncoding = { active: true }; + if (encoding.active === false) normalizedEncoding.active = false; + if (typeof encoding.dtx === 'boolean') + normalizedEncoding.dtx = encoding.dtx; + if (typeof encoding.scalabilityMode === 'string') + normalizedEncoding.scalabilityMode = encoding.scalabilityMode; + if (typeof encoding.scaleResolutionDownBy === 'number') + normalizedEncoding.scaleResolutionDownBy = + encoding.scaleResolutionDownBy; + if (typeof encoding.maxBitrate === 'number') + normalizedEncoding.maxBitrate = encoding.maxBitrate; + if (typeof encoding.maxFramerate === 'number') + normalizedEncoding.maxFramerate = encoding.maxFramerate; + if (typeof encoding.adaptivePtime === 'boolean') + normalizedEncoding.adaptivePtime = encoding.adaptivePtime; + if (typeof encoding.priority === 'string') + normalizedEncoding.priority = encoding.priority; + if (typeof encoding.networkPriority === 'string') + normalizedEncoding.networkPriority = encoding.networkPriority; + return normalizedEncoding; + }); + } + const { localId, rtpParameters, rtpSender } = await this._handler.send({ + track, + encodings: normalizedEncodings, + codecOptions, + codec, + }); + try { + // This will fill rtpParameters's missing fields with default values. + ortc.validateRtpParameters(rtpParameters); + const { id } = await this.safeEmitAsPromise('produce', { + kind: track.kind, + rtpParameters, + appData, + }); + const producer = new Producer_1.Producer({ + id, + localId, + rtpSender, + track, + rtpParameters, + stopTracks, + disableTrackOnPause, + zeroRtpOnPause, + appData, + }); + this._producers.set(producer.id, producer); + this._handleProducer(producer); + // Emit observer event. + this._observer.safeEmit('newproducer', producer); + return producer; + } catch (error) { + this._handler.stopSending(localId).catch(() => {}); + throw error; + } + }, 'transport.produce()') + // This catch is needed to stop the given track if the command above + // failed due to closed Transport. + .catch((error) => { + if (stopTracks) { + try { + track.stop(); + } catch (error2) {} + } + throw error; + }) + ); + } + /** + * Create a Consumer to consume a remote Producer. + */ + async consume({ id, producerId, kind, rtpParameters, appData = {} }) { + logger.debug('consume()'); + rtpParameters = utils.clone(rtpParameters, undefined); + if (this._closed) throw new errors_1.InvalidStateError('closed'); + else if (this._direction !== 'recv') + throw new errors_1.UnsupportedError('not a receiving Transport'); + else if (typeof id !== 'string') throw new TypeError('missing id'); + else if (typeof producerId !== 'string') throw new TypeError('missing producerId'); + else if (kind !== 'audio' && kind !== 'video') throw new TypeError(`invalid kind '${kind}'`); + else if (this.listenerCount('connect') === 0 && this._connectionState === 'new') + throw new TypeError('no "connect" listener set into this transport'); + else if (appData && typeof appData !== 'object') + throw new TypeError('if given, appData must be an object'); + // Enqueue command. + return this._awaitQueue.push(async () => { + // Ensure the device can consume it. + const canConsume = ortc.canReceive(rtpParameters, this._extendedRtpCapabilities); + if (!canConsume) throw new errors_1.UnsupportedError('cannot consume this Producer'); + const { localId, rtpReceiver, track } = await this._handler.receive({ + trackId: id, + kind, + rtpParameters, + }); + const consumer = new Consumer_1.Consumer({ + id, + localId, + producerId, + rtpReceiver, + track, + rtpParameters, + appData, + }); + this._consumers.set(consumer.id, consumer); + this._handleConsumer(consumer); + // If this is the first video Consumer and the Consumer for RTP probation + // has not yet been created, create it now. + if (!this._probatorConsumerCreated && kind === 'video') { + try { + const probatorRtpParameters = ortc.generateProbatorRtpParameters( + consumer.rtpParameters, + ); + await this._handler.receive({ + trackId: 'probator', + kind: 'video', + rtpParameters: probatorRtpParameters, + }); + logger.debug('consume() | Consumer for RTP probation created'); + this._probatorConsumerCreated = true; + } catch (error) { + logger.error('consume() | failed to create Consumer for RTP probation:%o', error); + } + } + // Emit observer event. + this._observer.safeEmit('newconsumer', consumer); + return consumer; + }, 'transport.consume()'); + } + /** + * Create a DataProducer + */ + async produceData({ + ordered = true, + maxPacketLifeTime, + maxRetransmits, + label = '', + protocol = '', + appData = {}, + } = {}) { + logger.debug('produceData()'); + if (this._direction !== 'send') throw new errors_1.UnsupportedError('not a sending Transport'); + else if (!this._maxSctpMessageSize) + throw new errors_1.UnsupportedError('SCTP not enabled by remote Transport'); + else if (this.listenerCount('connect') === 0 && this._connectionState === 'new') + throw new TypeError('no "connect" listener set into this transport'); + else if (this.listenerCount('producedata') === 0) + throw new TypeError('no "producedata" listener set into this transport'); + else if (appData && typeof appData !== 'object') + throw new TypeError('if given, appData must be an object'); + if (maxPacketLifeTime || maxRetransmits) ordered = false; + // Enqueue command. + return this._awaitQueue.push(async () => { + const { dataChannel, sctpStreamParameters } = await this._handler.sendDataChannel({ + ordered, + maxPacketLifeTime, + maxRetransmits, + label, + protocol, + }); + // This will fill sctpStreamParameters's missing fields with default values. + ortc.validateSctpStreamParameters(sctpStreamParameters); + const { id } = await this.safeEmitAsPromise('producedata', { + sctpStreamParameters, + label, + protocol, + appData, + }); + const dataProducer = new DataProducer_1.DataProducer({ + id, + dataChannel, + sctpStreamParameters, + appData, + }); + this._dataProducers.set(dataProducer.id, dataProducer); + this._handleDataProducer(dataProducer); + // Emit observer event. + this._observer.safeEmit('newdataproducer', dataProducer); + return dataProducer; + }, 'transport.produceData()'); + } + /** + * Create a DataConsumer + */ + async consumeData({ + id, + dataProducerId, + sctpStreamParameters, + label = '', + protocol = '', + appData = {}, + }) { + logger.debug('consumeData()'); + sctpStreamParameters = utils.clone(sctpStreamParameters, undefined); + if (this._closed) throw new errors_1.InvalidStateError('closed'); + else if (this._direction !== 'recv') + throw new errors_1.UnsupportedError('not a receiving Transport'); + else if (!this._maxSctpMessageSize) + throw new errors_1.UnsupportedError('SCTP not enabled by remote Transport'); + else if (typeof id !== 'string') throw new TypeError('missing id'); + else if (typeof dataProducerId !== 'string') throw new TypeError('missing dataProducerId'); + else if (this.listenerCount('connect') === 0 && this._connectionState === 'new') + throw new TypeError('no "connect" listener set into this transport'); + else if (appData && typeof appData !== 'object') + throw new TypeError('if given, appData must be an object'); + // This may throw. + ortc.validateSctpStreamParameters(sctpStreamParameters); + // Enqueue command. + return this._awaitQueue.push(async () => { + const { dataChannel } = await this._handler.receiveDataChannel({ + sctpStreamParameters, + label, + protocol, + }); + const dataConsumer = new DataConsumer_1.DataConsumer({ + id, + dataProducerId, + dataChannel, + sctpStreamParameters, + appData, + }); + this._dataConsumers.set(dataConsumer.id, dataConsumer); + this._handleDataConsumer(dataConsumer); + // Emit observer event. + this._observer.safeEmit('newdataconsumer', dataConsumer); + return dataConsumer; + }, 'transport.consumeData()'); + } + _handleHandler() { + const handler = this._handler; + handler.on('@connect', ({ dtlsParameters }, callback, errback) => { + if (this._closed) { + errback(new errors_1.InvalidStateError('closed')); + return; + } + this.safeEmit('connect', { dtlsParameters }, callback, errback); + }); + handler.on('@connectionstatechange', (connectionState) => { + if (connectionState === this._connectionState) return; + logger.debug('connection state changed to %s', connectionState); + this._connectionState = connectionState; + if (!this._closed) this.safeEmit('connectionstatechange', connectionState); + }); + } + _handleProducer(producer) { + producer.on('@close', () => { + this._producers.delete(producer.id); + if (this._closed) return; + this._awaitQueue + .push(async () => this._handler.stopSending(producer.localId), 'producer @close event') + .catch((error) => logger.warn('producer.close() failed:%o', error)); + }); + producer.on('@replacetrack', (track, callback, errback) => { + this._awaitQueue + .push( + async () => this._handler.replaceTrack(producer.localId, track), + 'producer @replacetrack event', + ) + .then(callback) + .catch(errback); + }); + producer.on('@setmaxspatiallayer', (spatialLayer, callback, errback) => { + this._awaitQueue + .push( + async () => this._handler.setMaxSpatialLayer(producer.localId, spatialLayer), + 'producer @setmaxspatiallayer event', + ) + .then(callback) + .catch(errback); + }); + producer.on('@setrtpencodingparameters', (params, callback, errback) => { + this._awaitQueue + .push( + async () => this._handler.setRtpEncodingParameters(producer.localId, params), + 'producer @setrtpencodingparameters event', + ) + .then(callback) + .catch(errback); + }); + producer.on('@getstats', (callback, errback) => { + if (this._closed) return errback(new errors_1.InvalidStateError('closed')); + this._handler.getSenderStats(producer.localId).then(callback).catch(errback); + }); + } + _handleConsumer(consumer) { + consumer.on('@close', () => { + this._consumers.delete(consumer.id); + if (this._closed) return; + this._awaitQueue + .push( + async () => this._handler.stopReceiving(consumer.localId), + 'consumer @close event', + ) + .catch(() => {}); + }); + consumer.on('@getstats', (callback, errback) => { + if (this._closed) return errback(new errors_1.InvalidStateError('closed')); + this._handler.getReceiverStats(consumer.localId).then(callback).catch(errback); + }); + } + _handleDataProducer(dataProducer) { + dataProducer.on('@close', () => { + this._dataProducers.delete(dataProducer.id); + }); + } + _handleDataConsumer(dataConsumer) { + dataConsumer.on('@close', () => { + this._dataConsumers.delete(dataConsumer.id); + }); + } + } + exports.Transport = Transport; + }, + { + './Consumer': 8, + './DataConsumer': 9, + './DataProducer': 10, + './EnhancedEventEmitter': 12, + './Logger': 13, + './Producer': 14, + './errors': 18, + './ortc': 36, + './utils': 39, + awaitqueue: 2, + }, + ], + 18: [ + function (require, module, exports) { + 'use strict'; + Object.defineProperty(exports, '__esModule', { value: true }); + exports.InvalidStateError = exports.UnsupportedError = void 0; + /** + * Error indicating not support for something. + */ + class UnsupportedError extends Error { + constructor(message) { + super(message); + this.name = 'UnsupportedError'; + if (Error.hasOwnProperty('captureStackTrace')) { + // Just in V8. + // @ts-ignore + Error.captureStackTrace(this, UnsupportedError); + } else { + this.stack = new Error(message).stack; + } + } + } + exports.UnsupportedError = UnsupportedError; + /** + * Error produced when calling a method in an invalid state. + */ + class InvalidStateError extends Error { + constructor(message) { + super(message); + this.name = 'InvalidStateError'; + if (Error.hasOwnProperty('captureStackTrace')) { + // Just in V8. + // @ts-ignore + Error.captureStackTrace(this, InvalidStateError); + } else { + this.stack = new Error(message).stack; + } + } + } + exports.InvalidStateError = InvalidStateError; + }, + {}, + ], + 19: [ + function (require, module, exports) { + 'use strict'; + var __createBinding = + (this && this.__createBinding) || + (Object.create + ? function (o, m, k, k2) { + if (k2 === undefined) k2 = k; + Object.defineProperty(o, k2, { + enumerable: true, + get: function () { + return m[k]; + }, + }); + } + : function (o, m, k, k2) { + if (k2 === undefined) k2 = k; + o[k2] = m[k]; + }); + var __setModuleDefault = + (this && this.__setModuleDefault) || + (Object.create + ? function (o, v) { + Object.defineProperty(o, 'default', { enumerable: true, value: v }); + } + : function (o, v) { + o['default'] = v; + }); + var __importStar = + (this && this.__importStar) || + function (mod) { + if (mod && mod.__esModule) return mod; + var result = {}; + if (mod != null) + for (var k in mod) + if (k !== 'default' && Object.hasOwnProperty.call(mod, k)) + __createBinding(result, mod, k); + __setModuleDefault(result, mod); + return result; + }; + Object.defineProperty(exports, '__esModule', { value: true }); + exports.Chrome55 = void 0; + const sdpTransform = __importStar(require('sdp-transform')); + const Logger_1 = require('../Logger'); + const errors_1 = require('../errors'); + const utils = __importStar(require('../utils')); + const ortc = __importStar(require('../ortc')); + const sdpCommonUtils = __importStar(require('./sdp/commonUtils')); + const sdpPlanBUtils = __importStar(require('./sdp/planBUtils')); + const HandlerInterface_1 = require('./HandlerInterface'); + const RemoteSdp_1 = require('./sdp/RemoteSdp'); + const logger = new Logger_1.Logger('Chrome55'); + const SCTP_NUM_STREAMS = { OS: 1024, MIS: 1024 }; + class Chrome55 extends HandlerInterface_1.HandlerInterface { + constructor() { + super(); + // Local stream for sending. + this._sendStream = new MediaStream(); + // Map of sending MediaStreamTracks indexed by localId. + this._mapSendLocalIdTrack = new Map(); + // Next sending localId. + this._nextSendLocalId = 0; + // Map of MID, RTP parameters and RTCRtpReceiver indexed by local id. + // Value is an Object with mid, rtpParameters and rtpReceiver. + this._mapRecvLocalIdInfo = new Map(); + // Whether a DataChannel m=application section has been created. + this._hasDataChannelMediaSection = false; + // Sending DataChannel id value counter. Incremented for each new DataChannel. + this._nextSendSctpStreamId = 0; + // Got transport local and remote parameters. + this._transportReady = false; + } + /** + * Creates a factory function. + */ + static createFactory() { + return () => new Chrome55(); + } + get name() { + return 'Chrome55'; + } + close() { + logger.debug('close()'); + // Close RTCPeerConnection. + if (this._pc) { + try { + this._pc.close(); + } catch (error) {} + } + } + async getNativeRtpCapabilities() { + logger.debug('getNativeRtpCapabilities()'); + const pc = new RTCPeerConnection({ + iceServers: [], + iceTransportPolicy: 'all', + bundlePolicy: 'max-bundle', + rtcpMuxPolicy: 'require', + sdpSemantics: 'plan-b', + }); + try { + const offer = await pc.createOffer({ + offerToReceiveAudio: true, + offerToReceiveVideo: true, + }); + try { + pc.close(); + } catch (error) {} + const sdpObject = sdpTransform.parse(offer.sdp); + const nativeRtpCapabilities = sdpCommonUtils.extractRtpCapabilities({ sdpObject }); + return nativeRtpCapabilities; + } catch (error) { + try { + pc.close(); + } catch (error2) {} + throw error; + } + } + async getNativeSctpCapabilities() { + logger.debug('getNativeSctpCapabilities()'); + return { + numStreams: SCTP_NUM_STREAMS, + }; + } + run({ + direction, + iceParameters, + iceCandidates, + dtlsParameters, + sctpParameters, + iceServers, + iceTransportPolicy, + additionalSettings, + proprietaryConstraints, + extendedRtpCapabilities, + }) { + logger.debug('run()'); + this._direction = direction; + this._remoteSdp = new RemoteSdp_1.RemoteSdp({ + iceParameters, + iceCandidates, + dtlsParameters, + sctpParameters, + planB: true, + }); + this._sendingRtpParametersByKind = { + audio: ortc.getSendingRtpParameters('audio', extendedRtpCapabilities), + video: ortc.getSendingRtpParameters('video', extendedRtpCapabilities), + }; + this._sendingRemoteRtpParametersByKind = { + audio: ortc.getSendingRemoteRtpParameters('audio', extendedRtpCapabilities), + video: ortc.getSendingRemoteRtpParameters('video', extendedRtpCapabilities), + }; + this._pc = new RTCPeerConnection( + Object.assign( + { + iceServers: iceServers || [], + iceTransportPolicy: iceTransportPolicy || 'all', + bundlePolicy: 'max-bundle', + rtcpMuxPolicy: 'require', + sdpSemantics: 'plan-b', + }, + additionalSettings, + ), + proprietaryConstraints, + ); + // Handle RTCPeerConnection connection status. + this._pc.addEventListener('iceconnectionstatechange', () => { + switch (this._pc.iceConnectionState) { + case 'checking': + this.emit('@connectionstatechange', 'connecting'); + break; + case 'connected': + case 'completed': + this.emit('@connectionstatechange', 'connected'); + break; + case 'failed': + this.emit('@connectionstatechange', 'failed'); + break; + case 'disconnected': + this.emit('@connectionstatechange', 'disconnected'); + break; + case 'closed': + this.emit('@connectionstatechange', 'closed'); + break; + } + }); + } + async updateIceServers(iceServers) { + logger.debug('updateIceServers()'); + const configuration = this._pc.getConfiguration(); + configuration.iceServers = iceServers; + this._pc.setConfiguration(configuration); + } + async restartIce(iceParameters) { + logger.debug('restartIce()'); + // Provide the remote SDP handler with new remote ICE parameters. + this._remoteSdp.updateIceParameters(iceParameters); + if (!this._transportReady) return; + if (this._direction === 'send') { + const offer = await this._pc.createOffer({ iceRestart: true }); + logger.debug('restartIce() | calling pc.setLocalDescription() [offer:%o]', offer); + await this._pc.setLocalDescription(offer); + const answer = { type: 'answer', sdp: this._remoteSdp.getSdp() }; + logger.debug('restartIce() | calling pc.setRemoteDescription() [answer:%o]', answer); + await this._pc.setRemoteDescription(answer); + } else { + const offer = { type: 'offer', sdp: this._remoteSdp.getSdp() }; + logger.debug('restartIce() | calling pc.setRemoteDescription() [offer:%o]', offer); + await this._pc.setRemoteDescription(offer); + const answer = await this._pc.createAnswer(); + logger.debug('restartIce() | calling pc.setLocalDescription() [answer:%o]', answer); + await this._pc.setLocalDescription(answer); + } + } + async getTransportStats() { + return this._pc.getStats(); + } + async send({ track, encodings, codecOptions, codec }) { + this._assertSendDirection(); + logger.debug('send() [kind:%s, track.id:%s]', track.kind, track.id); + if (codec) { + logger.warn('send() | codec selection is not available in %s handler', this.name); + } + this._sendStream.addTrack(track); + this._pc.addStream(this._sendStream); + let offer = await this._pc.createOffer(); + let localSdpObject = sdpTransform.parse(offer.sdp); + let offerMediaObject; + const sendingRtpParameters = utils.clone(this._sendingRtpParametersByKind[track.kind], {}); + sendingRtpParameters.codecs = ortc.reduceCodecs(sendingRtpParameters.codecs); + const sendingRemoteRtpParameters = utils.clone( + this._sendingRemoteRtpParametersByKind[track.kind], + {}, + ); + sendingRemoteRtpParameters.codecs = ortc.reduceCodecs(sendingRemoteRtpParameters.codecs); + if (!this._transportReady) + await this._setupTransport({ localDtlsRole: 'server', localSdpObject }); + if (track.kind === 'video' && encodings && encodings.length > 1) { + logger.debug('send() | enabling simulcast'); + localSdpObject = sdpTransform.parse(offer.sdp); + offerMediaObject = localSdpObject.media.find((m) => m.type === 'video'); + sdpPlanBUtils.addLegacySimulcast({ + offerMediaObject, + track, + numStreams: encodings.length, + }); + offer = { type: 'offer', sdp: sdpTransform.write(localSdpObject) }; + } + logger.debug('send() | calling pc.setLocalDescription() [offer:%o]', offer); + await this._pc.setLocalDescription(offer); + localSdpObject = sdpTransform.parse(this._pc.localDescription.sdp); + offerMediaObject = localSdpObject.media.find((m) => m.type === track.kind); + // Set RTCP CNAME. + sendingRtpParameters.rtcp.cname = sdpCommonUtils.getCname({ offerMediaObject }); + // Set RTP encodings. + sendingRtpParameters.encodings = sdpPlanBUtils.getRtpEncodings({ offerMediaObject, track }); + // Complete encodings with given values. + if (encodings) { + for (let idx = 0; idx < sendingRtpParameters.encodings.length; ++idx) { + if (encodings[idx]) Object.assign(sendingRtpParameters.encodings[idx], encodings[idx]); + } + } + // If VP8 and there is effective simulcast, add scalabilityMode to each + // encoding. + if ( + sendingRtpParameters.encodings.length > 1 && + sendingRtpParameters.codecs[0].mimeType.toLowerCase() === 'video/vp8' + ) { + for (const encoding of sendingRtpParameters.encodings) { + encoding.scalabilityMode = 'S1T3'; + } + } + this._remoteSdp.send({ + offerMediaObject, + offerRtpParameters: sendingRtpParameters, + answerRtpParameters: sendingRemoteRtpParameters, + codecOptions, + }); + const answer = { type: 'answer', sdp: this._remoteSdp.getSdp() }; + logger.debug('send() | calling pc.setRemoteDescription() [answer:%o]', answer); + await this._pc.setRemoteDescription(answer); + const localId = String(this._nextSendLocalId); + this._nextSendLocalId++; + // Insert into the map. + this._mapSendLocalIdTrack.set(localId, track); + return { + localId: localId, + rtpParameters: sendingRtpParameters, + }; + } + async stopSending(localId) { + this._assertSendDirection(); + logger.debug('stopSending() [localId:%s]', localId); + const track = this._mapSendLocalIdTrack.get(localId); + if (!track) throw new Error('track not found'); + this._mapSendLocalIdTrack.delete(localId); + this._sendStream.removeTrack(track); + this._pc.addStream(this._sendStream); + const offer = await this._pc.createOffer(); + logger.debug('stopSending() | calling pc.setLocalDescription() [offer:%o]', offer); + try { + await this._pc.setLocalDescription(offer); + } catch (error) { + // NOTE: If there are no sending tracks, setLocalDescription() will fail with + // "Failed to create channels". If so, ignore it. + if (this._sendStream.getTracks().length === 0) { + logger.warn( + 'stopSending() | ignoring expected error due no sending tracks: %s', + error.toString(), + ); + return; + } + throw error; + } + if (this._pc.signalingState === 'stable') return; + const answer = { type: 'answer', sdp: this._remoteSdp.getSdp() }; + logger.debug('stopSending() | calling pc.setRemoteDescription() [answer:%o]', answer); + await this._pc.setRemoteDescription(answer); + } + async replaceTrack( + // eslint-disable-next-line @typescript-eslint/no-unused-vars + localId, + track, + ) { + throw new errors_1.UnsupportedError('not implemented'); + } + // eslint-disable-next-line @typescript-eslint/no-unused-vars + async setMaxSpatialLayer(localId, spatialLayer) { + throw new errors_1.UnsupportedError(' not implemented'); + } + // eslint-disable-next-line @typescript-eslint/no-unused-vars + async setRtpEncodingParameters(localId, params) { + throw new errors_1.UnsupportedError('not supported'); + } + // eslint-disable-next-line @typescript-eslint/no-unused-vars + async getSenderStats(localId) { + throw new errors_1.UnsupportedError('not implemented'); + } + async sendDataChannel({ ordered, maxPacketLifeTime, maxRetransmits, label, protocol }) { + this._assertSendDirection(); + const options = { + negotiated: true, + id: this._nextSendSctpStreamId, + ordered, + maxPacketLifeTime, + maxRetransmitTime: maxPacketLifeTime, + maxRetransmits, + protocol, + }; + logger.debug('sendDataChannel() [options:%o]', options); + const dataChannel = this._pc.createDataChannel(label, options); + // Increase next id. + this._nextSendSctpStreamId = ++this._nextSendSctpStreamId % SCTP_NUM_STREAMS.MIS; + // If this is the first DataChannel we need to create the SDP answer with + // m=application section. + if (!this._hasDataChannelMediaSection) { + const offer = await this._pc.createOffer(); + const localSdpObject = sdpTransform.parse(offer.sdp); + const offerMediaObject = localSdpObject.media.find((m) => m.type === 'application'); + if (!this._transportReady) + await this._setupTransport({ localDtlsRole: 'server', localSdpObject }); + logger.debug('sendDataChannel() | calling pc.setLocalDescription() [offer:%o]', offer); + await this._pc.setLocalDescription(offer); + this._remoteSdp.sendSctpAssociation({ offerMediaObject }); + const answer = { type: 'answer', sdp: this._remoteSdp.getSdp() }; + logger.debug('sendDataChannel() | calling pc.setRemoteDescription() [answer:%o]', answer); + await this._pc.setRemoteDescription(answer); + this._hasDataChannelMediaSection = true; + } + const sctpStreamParameters = { + streamId: options.id, + ordered: options.ordered, + maxPacketLifeTime: options.maxPacketLifeTime, + maxRetransmits: options.maxRetransmits, + }; + return { dataChannel, sctpStreamParameters }; + } + async receive({ trackId, kind, rtpParameters }) { + this._assertRecvDirection(); + logger.debug('receive() [trackId:%s, kind:%s]', trackId, kind); + const localId = trackId; + const mid = kind; + const streamId = rtpParameters.rtcp.cname; + this._remoteSdp.receive({ + mid, + kind, + offerRtpParameters: rtpParameters, + streamId, + trackId, + }); + const offer = { type: 'offer', sdp: this._remoteSdp.getSdp() }; + logger.debug('receive() | calling pc.setRemoteDescription() [offer:%o]', offer); + await this._pc.setRemoteDescription(offer); + let answer = await this._pc.createAnswer(); + const localSdpObject = sdpTransform.parse(answer.sdp); + const answerMediaObject = localSdpObject.media.find((m) => String(m.mid) === mid); + // May need to modify codec parameters in the answer based on codec + // parameters in the offer. + sdpCommonUtils.applyCodecParameters({ + offerRtpParameters: rtpParameters, + answerMediaObject, + }); + answer = { type: 'answer', sdp: sdpTransform.write(localSdpObject) }; + if (!this._transportReady) + await this._setupTransport({ localDtlsRole: 'client', localSdpObject }); + logger.debug('receive() | calling pc.setLocalDescription() [answer:%o]', answer); + await this._pc.setLocalDescription(answer); + const stream = this._pc.getRemoteStreams().find((s) => s.id === streamId); + const track = stream.getTrackById(localId); + if (!track) throw new Error('remote track not found'); + // Insert into the map. + this._mapRecvLocalIdInfo.set(localId, { mid, rtpParameters }); + return { localId, track }; + } + async stopReceiving(localId) { + this._assertRecvDirection(); + logger.debug('stopReceiving() [localId:%s]', localId); + const { mid, rtpParameters } = this._mapRecvLocalIdInfo.get(localId) || {}; + // Remove from the map. + this._mapRecvLocalIdInfo.delete(localId); + this._remoteSdp.planBStopReceiving({ mid: mid, offerRtpParameters: rtpParameters }); + const offer = { type: 'offer', sdp: this._remoteSdp.getSdp() }; + logger.debug('stopReceiving() | calling pc.setRemoteDescription() [offer:%o]', offer); + await this._pc.setRemoteDescription(offer); + const answer = await this._pc.createAnswer(); + logger.debug('stopReceiving() | calling pc.setLocalDescription() [answer:%o]', answer); + await this._pc.setLocalDescription(answer); + } + // eslint-disable-next-line @typescript-eslint/no-unused-vars + async getReceiverStats(localId) { + throw new errors_1.UnsupportedError('not implemented'); + } + async receiveDataChannel({ sctpStreamParameters, label, protocol }) { + this._assertRecvDirection(); + const { streamId, ordered, maxPacketLifeTime, maxRetransmits } = sctpStreamParameters; + const options = { + negotiated: true, + id: streamId, + ordered, + maxPacketLifeTime, + maxRetransmitTime: maxPacketLifeTime, + maxRetransmits, + protocol, + }; + logger.debug('receiveDataChannel() [options:%o]', options); + const dataChannel = this._pc.createDataChannel(label, options); + // If this is the first DataChannel we need to create the SDP offer with + // m=application section. + if (!this._hasDataChannelMediaSection) { + this._remoteSdp.receiveSctpAssociation({ oldDataChannelSpec: true }); + const offer = { type: 'offer', sdp: this._remoteSdp.getSdp() }; + logger.debug('receiveDataChannel() | calling pc.setRemoteDescription() [offer:%o]', offer); + await this._pc.setRemoteDescription(offer); + const answer = await this._pc.createAnswer(); + if (!this._transportReady) { + const localSdpObject = sdpTransform.parse(answer.sdp); + await this._setupTransport({ localDtlsRole: 'client', localSdpObject }); + } + logger.debug( + 'receiveDataChannel() | calling pc.setRemoteDescription() [answer:%o]', + answer, + ); + await this._pc.setLocalDescription(answer); + this._hasDataChannelMediaSection = true; + } + return { dataChannel }; + } + async _setupTransport({ localDtlsRole, localSdpObject }) { + if (!localSdpObject) localSdpObject = sdpTransform.parse(this._pc.localDescription.sdp); + // Get our local DTLS parameters. + const dtlsParameters = sdpCommonUtils.extractDtlsParameters({ sdpObject: localSdpObject }); + // Set our DTLS role. + dtlsParameters.role = localDtlsRole; + // Update the remote DTLS role in the SDP. + this._remoteSdp.updateDtlsRole(localDtlsRole === 'client' ? 'server' : 'client'); + // Need to tell the remote transport about our parameters. + await this.safeEmitAsPromise('@connect', { dtlsParameters }); + this._transportReady = true; + } + _assertSendDirection() { + if (this._direction !== 'send') { + throw new Error('method can just be called for handlers with "send" direction'); + } + } + _assertRecvDirection() { + if (this._direction !== 'recv') { + throw new Error('method can just be called for handlers with "recv" direction'); + } + } + } + exports.Chrome55 = Chrome55; + }, + { + '../Logger': 13, + '../errors': 18, + '../ortc': 36, + '../utils': 39, + './HandlerInterface': 25, + './sdp/RemoteSdp': 31, + './sdp/commonUtils': 32, + './sdp/planBUtils': 33, + 'sdp-transform': 44, + }, + ], + 20: [ + function (require, module, exports) { + 'use strict'; + var __createBinding = + (this && this.__createBinding) || + (Object.create + ? function (o, m, k, k2) { + if (k2 === undefined) k2 = k; + Object.defineProperty(o, k2, { + enumerable: true, + get: function () { + return m[k]; + }, + }); + } + : function (o, m, k, k2) { + if (k2 === undefined) k2 = k; + o[k2] = m[k]; + }); + var __setModuleDefault = + (this && this.__setModuleDefault) || + (Object.create + ? function (o, v) { + Object.defineProperty(o, 'default', { enumerable: true, value: v }); + } + : function (o, v) { + o['default'] = v; + }); + var __importStar = + (this && this.__importStar) || + function (mod) { + if (mod && mod.__esModule) return mod; + var result = {}; + if (mod != null) + for (var k in mod) + if (k !== 'default' && Object.hasOwnProperty.call(mod, k)) + __createBinding(result, mod, k); + __setModuleDefault(result, mod); + return result; + }; + Object.defineProperty(exports, '__esModule', { value: true }); + exports.Chrome67 = void 0; + const sdpTransform = __importStar(require('sdp-transform')); + const Logger_1 = require('../Logger'); + const utils = __importStar(require('../utils')); + const ortc = __importStar(require('../ortc')); + const sdpCommonUtils = __importStar(require('./sdp/commonUtils')); + const sdpPlanBUtils = __importStar(require('./sdp/planBUtils')); + const HandlerInterface_1 = require('./HandlerInterface'); + const RemoteSdp_1 = require('./sdp/RemoteSdp'); + const logger = new Logger_1.Logger('Chrome67'); + const SCTP_NUM_STREAMS = { OS: 1024, MIS: 1024 }; + class Chrome67 extends HandlerInterface_1.HandlerInterface { + constructor() { + super(); + // Local stream for sending. + this._sendStream = new MediaStream(); + // Map of RTCRtpSender indexed by localId. + this._mapSendLocalIdRtpSender = new Map(); + // Next sending localId. + this._nextSendLocalId = 0; + // Map of MID, RTP parameters and RTCRtpReceiver indexed by local id. + // Value is an Object with mid, rtpParameters and rtpReceiver. + this._mapRecvLocalIdInfo = new Map(); + // Whether a DataChannel m=application section has been created. + this._hasDataChannelMediaSection = false; + // Sending DataChannel id value counter. Incremented for each new DataChannel. + this._nextSendSctpStreamId = 0; + // Got transport local and remote parameters. + this._transportReady = false; + } + /** + * Creates a factory function. + */ + static createFactory() { + return () => new Chrome67(); + } + get name() { + return 'Chrome67'; + } + close() { + logger.debug('close()'); + // Close RTCPeerConnection. + if (this._pc) { + try { + this._pc.close(); + } catch (error) {} + } + } + async getNativeRtpCapabilities() { + logger.debug('getNativeRtpCapabilities()'); + const pc = new RTCPeerConnection({ + iceServers: [], + iceTransportPolicy: 'all', + bundlePolicy: 'max-bundle', + rtcpMuxPolicy: 'require', + sdpSemantics: 'plan-b', + }); + try { + const offer = await pc.createOffer({ + offerToReceiveAudio: true, + offerToReceiveVideo: true, + }); + try { + pc.close(); + } catch (error) {} + const sdpObject = sdpTransform.parse(offer.sdp); + const nativeRtpCapabilities = sdpCommonUtils.extractRtpCapabilities({ sdpObject }); + return nativeRtpCapabilities; + } catch (error) { + try { + pc.close(); + } catch (error2) {} + throw error; + } + } + async getNativeSctpCapabilities() { + logger.debug('getNativeSctpCapabilities()'); + return { + numStreams: SCTP_NUM_STREAMS, + }; + } + run({ + direction, + iceParameters, + iceCandidates, + dtlsParameters, + sctpParameters, + iceServers, + iceTransportPolicy, + additionalSettings, + proprietaryConstraints, + extendedRtpCapabilities, + }) { + logger.debug('run()'); + this._direction = direction; + this._remoteSdp = new RemoteSdp_1.RemoteSdp({ + iceParameters, + iceCandidates, + dtlsParameters, + sctpParameters, + planB: true, + }); + this._sendingRtpParametersByKind = { + audio: ortc.getSendingRtpParameters('audio', extendedRtpCapabilities), + video: ortc.getSendingRtpParameters('video', extendedRtpCapabilities), + }; + this._sendingRemoteRtpParametersByKind = { + audio: ortc.getSendingRemoteRtpParameters('audio', extendedRtpCapabilities), + video: ortc.getSendingRemoteRtpParameters('video', extendedRtpCapabilities), + }; + this._pc = new RTCPeerConnection( + Object.assign( + { + iceServers: iceServers || [], + iceTransportPolicy: iceTransportPolicy || 'all', + bundlePolicy: 'max-bundle', + rtcpMuxPolicy: 'require', + sdpSemantics: 'plan-b', + }, + additionalSettings, + ), + proprietaryConstraints, + ); + // Handle RTCPeerConnection connection status. + this._pc.addEventListener('iceconnectionstatechange', () => { + switch (this._pc.iceConnectionState) { + case 'checking': + this.emit('@connectionstatechange', 'connecting'); + break; + case 'connected': + case 'completed': + this.emit('@connectionstatechange', 'connected'); + break; + case 'failed': + this.emit('@connectionstatechange', 'failed'); + break; + case 'disconnected': + this.emit('@connectionstatechange', 'disconnected'); + break; + case 'closed': + this.emit('@connectionstatechange', 'closed'); + break; + } + }); + } + async updateIceServers(iceServers) { + logger.debug('updateIceServers()'); + const configuration = this._pc.getConfiguration(); + configuration.iceServers = iceServers; + this._pc.setConfiguration(configuration); + } + async restartIce(iceParameters) { + logger.debug('restartIce()'); + // Provide the remote SDP handler with new remote ICE parameters. + this._remoteSdp.updateIceParameters(iceParameters); + if (!this._transportReady) return; + if (this._direction === 'send') { + const offer = await this._pc.createOffer({ iceRestart: true }); + logger.debug('restartIce() | calling pc.setLocalDescription() [offer:%o]', offer); + await this._pc.setLocalDescription(offer); + const answer = { type: 'answer', sdp: this._remoteSdp.getSdp() }; + logger.debug('restartIce() | calling pc.setRemoteDescription() [answer:%o]', answer); + await this._pc.setRemoteDescription(answer); + } else { + const offer = { type: 'offer', sdp: this._remoteSdp.getSdp() }; + logger.debug('restartIce() | calling pc.setRemoteDescription() [offer:%o]', offer); + await this._pc.setRemoteDescription(offer); + const answer = await this._pc.createAnswer(); + logger.debug('restartIce() | calling pc.setLocalDescription() [answer:%o]', answer); + await this._pc.setLocalDescription(answer); + } + } + async getTransportStats() { + return this._pc.getStats(); + } + async send({ track, encodings, codecOptions, codec }) { + this._assertSendDirection(); + logger.debug('send() [kind:%s, track.id:%s]', track.kind, track.id); + if (codec) { + logger.warn('send() | codec selection is not available in %s handler', this.name); + } + this._sendStream.addTrack(track); + this._pc.addTrack(track, this._sendStream); + let offer = await this._pc.createOffer(); + let localSdpObject = sdpTransform.parse(offer.sdp); + let offerMediaObject; + const sendingRtpParameters = utils.clone(this._sendingRtpParametersByKind[track.kind], {}); + sendingRtpParameters.codecs = ortc.reduceCodecs(sendingRtpParameters.codecs); + const sendingRemoteRtpParameters = utils.clone( + this._sendingRemoteRtpParametersByKind[track.kind], + {}, + ); + sendingRemoteRtpParameters.codecs = ortc.reduceCodecs(sendingRemoteRtpParameters.codecs); + if (!this._transportReady) + await this._setupTransport({ localDtlsRole: 'server', localSdpObject }); + if (track.kind === 'video' && encodings && encodings.length > 1) { + logger.debug('send() | enabling simulcast'); + localSdpObject = sdpTransform.parse(offer.sdp); + offerMediaObject = localSdpObject.media.find((m) => m.type === 'video'); + sdpPlanBUtils.addLegacySimulcast({ + offerMediaObject, + track, + numStreams: encodings.length, + }); + offer = { type: 'offer', sdp: sdpTransform.write(localSdpObject) }; + } + logger.debug('send() | calling pc.setLocalDescription() [offer:%o]', offer); + await this._pc.setLocalDescription(offer); + localSdpObject = sdpTransform.parse(this._pc.localDescription.sdp); + offerMediaObject = localSdpObject.media.find((m) => m.type === track.kind); + // Set RTCP CNAME. + sendingRtpParameters.rtcp.cname = sdpCommonUtils.getCname({ offerMediaObject }); + // Set RTP encodings. + sendingRtpParameters.encodings = sdpPlanBUtils.getRtpEncodings({ offerMediaObject, track }); + // Complete encodings with given values. + if (encodings) { + for (let idx = 0; idx < sendingRtpParameters.encodings.length; ++idx) { + if (encodings[idx]) Object.assign(sendingRtpParameters.encodings[idx], encodings[idx]); + } + } + // If VP8 and there is effective simulcast, add scalabilityMode to each + // encoding. + if ( + sendingRtpParameters.encodings.length > 1 && + sendingRtpParameters.codecs[0].mimeType.toLowerCase() === 'video/vp8' + ) { + for (const encoding of sendingRtpParameters.encodings) { + encoding.scalabilityMode = 'S1T3'; + } + } + this._remoteSdp.send({ + offerMediaObject, + offerRtpParameters: sendingRtpParameters, + answerRtpParameters: sendingRemoteRtpParameters, + codecOptions, + }); + const answer = { type: 'answer', sdp: this._remoteSdp.getSdp() }; + logger.debug('send() | calling pc.setRemoteDescription() [answer:%o]', answer); + await this._pc.setRemoteDescription(answer); + const localId = String(this._nextSendLocalId); + this._nextSendLocalId++; + const rtpSender = this._pc.getSenders().find((s) => s.track === track); + // Insert into the map. + this._mapSendLocalIdRtpSender.set(localId, rtpSender); + return { + localId: localId, + rtpParameters: sendingRtpParameters, + rtpSender, + }; + } + async stopSending(localId) { + this._assertSendDirection(); + logger.debug('stopSending() [localId:%s]', localId); + const rtpSender = this._mapSendLocalIdRtpSender.get(localId); + if (!rtpSender) throw new Error('associated RTCRtpSender not found'); + this._pc.removeTrack(rtpSender); + if (rtpSender.track) this._sendStream.removeTrack(rtpSender.track); + this._mapSendLocalIdRtpSender.delete(localId); + const offer = await this._pc.createOffer(); + logger.debug('stopSending() | calling pc.setLocalDescription() [offer:%o]', offer); + try { + await this._pc.setLocalDescription(offer); + } catch (error) { + // NOTE: If there are no sending tracks, setLocalDescription() will fail with + // "Failed to create channels". If so, ignore it. + if (this._sendStream.getTracks().length === 0) { + logger.warn( + 'stopSending() | ignoring expected error due no sending tracks: %s', + error.toString(), + ); + return; + } + throw error; + } + if (this._pc.signalingState === 'stable') return; + const answer = { type: 'answer', sdp: this._remoteSdp.getSdp() }; + logger.debug('stopSending() | calling pc.setRemoteDescription() [answer:%o]', answer); + await this._pc.setRemoteDescription(answer); + } + async replaceTrack(localId, track) { + this._assertSendDirection(); + if (track) { + logger.debug('replaceTrack() [localId:%s, track.id:%s]', localId, track.id); + } else { + logger.debug('replaceTrack() [localId:%s, no track]', localId); + } + const rtpSender = this._mapSendLocalIdRtpSender.get(localId); + if (!rtpSender) throw new Error('associated RTCRtpSender not found'); + const oldTrack = rtpSender.track; + await rtpSender.replaceTrack(track); + // Remove the old track from the local stream. + if (oldTrack) this._sendStream.removeTrack(oldTrack); + // Add the new track to the local stream. + if (track) this._sendStream.addTrack(track); + } + async setMaxSpatialLayer(localId, spatialLayer) { + this._assertSendDirection(); + logger.debug('setMaxSpatialLayer() [localId:%s, spatialLayer:%s]', localId, spatialLayer); + const rtpSender = this._mapSendLocalIdRtpSender.get(localId); + if (!rtpSender) throw new Error('associated RTCRtpSender not found'); + const parameters = rtpSender.getParameters(); + parameters.encodings.forEach((encoding, idx) => { + if (idx <= spatialLayer) encoding.active = true; + else encoding.active = false; + }); + await rtpSender.setParameters(parameters); + } + async setRtpEncodingParameters(localId, params) { + this._assertSendDirection(); + logger.debug('setRtpEncodingParameters() [localId:%s, params:%o]', localId, params); + const rtpSender = this._mapSendLocalIdRtpSender.get(localId); + if (!rtpSender) throw new Error('associated RTCRtpSender not found'); + const parameters = rtpSender.getParameters(); + parameters.encodings.forEach((encoding, idx) => { + parameters.encodings[idx] = Object.assign(Object.assign({}, encoding), params); + }); + await rtpSender.setParameters(parameters); + } + async getSenderStats(localId) { + this._assertSendDirection(); + const rtpSender = this._mapSendLocalIdRtpSender.get(localId); + if (!rtpSender) throw new Error('associated RTCRtpSender not found'); + return rtpSender.getStats(); + } + async sendDataChannel({ ordered, maxPacketLifeTime, maxRetransmits, label, protocol }) { + this._assertSendDirection(); + const options = { + negotiated: true, + id: this._nextSendSctpStreamId, + ordered, + maxPacketLifeTime, + maxRetransmitTime: maxPacketLifeTime, + maxRetransmits, + protocol, + }; + logger.debug('sendDataChannel() [options:%o]', options); + const dataChannel = this._pc.createDataChannel(label, options); + // Increase next id. + this._nextSendSctpStreamId = ++this._nextSendSctpStreamId % SCTP_NUM_STREAMS.MIS; + // If this is the first DataChannel we need to create the SDP answer with + // m=application section. + if (!this._hasDataChannelMediaSection) { + const offer = await this._pc.createOffer(); + const localSdpObject = sdpTransform.parse(offer.sdp); + const offerMediaObject = localSdpObject.media.find((m) => m.type === 'application'); + if (!this._transportReady) + await this._setupTransport({ localDtlsRole: 'server', localSdpObject }); + logger.debug('sendDataChannel() | calling pc.setLocalDescription() [offer:%o]', offer); + await this._pc.setLocalDescription(offer); + this._remoteSdp.sendSctpAssociation({ offerMediaObject }); + const answer = { type: 'answer', sdp: this._remoteSdp.getSdp() }; + logger.debug('sendDataChannel() | calling pc.setRemoteDescription() [answer:%o]', answer); + await this._pc.setRemoteDescription(answer); + this._hasDataChannelMediaSection = true; + } + const sctpStreamParameters = { + streamId: options.id, + ordered: options.ordered, + maxPacketLifeTime: options.maxPacketLifeTime, + maxRetransmits: options.maxRetransmits, + }; + return { dataChannel, sctpStreamParameters }; + } + async receive({ trackId, kind, rtpParameters }) { + this._assertRecvDirection(); + logger.debug('receive() [trackId:%s, kind:%s]', trackId, kind); + const localId = trackId; + const mid = kind; + this._remoteSdp.receive({ + mid, + kind, + offerRtpParameters: rtpParameters, + streamId: rtpParameters.rtcp.cname, + trackId, + }); + const offer = { type: 'offer', sdp: this._remoteSdp.getSdp() }; + logger.debug('receive() | calling pc.setRemoteDescription() [offer:%o]', offer); + await this._pc.setRemoteDescription(offer); + let answer = await this._pc.createAnswer(); + const localSdpObject = sdpTransform.parse(answer.sdp); + const answerMediaObject = localSdpObject.media.find((m) => String(m.mid) === mid); + // May need to modify codec parameters in the answer based on codec + // parameters in the offer. + sdpCommonUtils.applyCodecParameters({ + offerRtpParameters: rtpParameters, + answerMediaObject, + }); + answer = { type: 'answer', sdp: sdpTransform.write(localSdpObject) }; + if (!this._transportReady) + await this._setupTransport({ localDtlsRole: 'client', localSdpObject }); + logger.debug('receive() | calling pc.setLocalDescription() [answer:%o]', answer); + await this._pc.setLocalDescription(answer); + const rtpReceiver = this._pc.getReceivers().find((r) => r.track && r.track.id === localId); + if (!rtpReceiver) throw new Error('new RTCRtpReceiver not'); + // Insert into the map. + this._mapRecvLocalIdInfo.set(localId, { mid, rtpParameters, rtpReceiver }); + return { + localId, + track: rtpReceiver.track, + rtpReceiver, + }; + } + async stopReceiving(localId) { + this._assertRecvDirection(); + logger.debug('stopReceiving() [localId:%s]', localId); + const { mid, rtpParameters } = this._mapRecvLocalIdInfo.get(localId) || {}; + // Remove from the map. + this._mapRecvLocalIdInfo.delete(localId); + this._remoteSdp.planBStopReceiving({ mid: mid, offerRtpParameters: rtpParameters }); + const offer = { type: 'offer', sdp: this._remoteSdp.getSdp() }; + logger.debug('stopReceiving() | calling pc.setRemoteDescription() [offer:%o]', offer); + await this._pc.setRemoteDescription(offer); + const answer = await this._pc.createAnswer(); + logger.debug('stopReceiving() | calling pc.setLocalDescription() [answer:%o]', answer); + await this._pc.setLocalDescription(answer); + } + async getReceiverStats(localId) { + this._assertRecvDirection(); + const { rtpReceiver } = this._mapRecvLocalIdInfo.get(localId) || {}; + if (!rtpReceiver) throw new Error('associated RTCRtpReceiver not found'); + return rtpReceiver.getStats(); + } + async receiveDataChannel({ sctpStreamParameters, label, protocol }) { + this._assertRecvDirection(); + const { streamId, ordered, maxPacketLifeTime, maxRetransmits } = sctpStreamParameters; + const options = { + negotiated: true, + id: streamId, + ordered, + maxPacketLifeTime, + maxRetransmitTime: maxPacketLifeTime, + maxRetransmits, + protocol, + }; + logger.debug('receiveDataChannel() [options:%o]', options); + const dataChannel = this._pc.createDataChannel(label, options); + // If this is the first DataChannel we need to create the SDP offer with + // m=application section. + if (!this._hasDataChannelMediaSection) { + this._remoteSdp.receiveSctpAssociation({ oldDataChannelSpec: true }); + const offer = { type: 'offer', sdp: this._remoteSdp.getSdp() }; + logger.debug('receiveDataChannel() | calling pc.setRemoteDescription() [offer:%o]', offer); + await this._pc.setRemoteDescription(offer); + const answer = await this._pc.createAnswer(); + if (!this._transportReady) { + const localSdpObject = sdpTransform.parse(answer.sdp); + await this._setupTransport({ localDtlsRole: 'client', localSdpObject }); + } + logger.debug( + 'receiveDataChannel() | calling pc.setRemoteDescription() [answer:%o]', + answer, + ); + await this._pc.setLocalDescription(answer); + this._hasDataChannelMediaSection = true; + } + return { dataChannel }; + } + async _setupTransport({ localDtlsRole, localSdpObject }) { + if (!localSdpObject) localSdpObject = sdpTransform.parse(this._pc.localDescription.sdp); + // Get our local DTLS parameters. + const dtlsParameters = sdpCommonUtils.extractDtlsParameters({ sdpObject: localSdpObject }); + // Set our DTLS role. + dtlsParameters.role = localDtlsRole; + // Update the remote DTLS role in the SDP. + this._remoteSdp.updateDtlsRole(localDtlsRole === 'client' ? 'server' : 'client'); + // Need to tell the remote transport about our parameters. + await this.safeEmitAsPromise('@connect', { dtlsParameters }); + this._transportReady = true; + } + _assertSendDirection() { + if (this._direction !== 'send') { + throw new Error('method can just be called for handlers with "send" direction'); + } + } + _assertRecvDirection() { + if (this._direction !== 'recv') { + throw new Error('method can just be called for handlers with "recv" direction'); + } + } + } + exports.Chrome67 = Chrome67; + }, + { + '../Logger': 13, + '../ortc': 36, + '../utils': 39, + './HandlerInterface': 25, + './sdp/RemoteSdp': 31, + './sdp/commonUtils': 32, + './sdp/planBUtils': 33, + 'sdp-transform': 44, + }, + ], + 21: [ + function (require, module, exports) { + 'use strict'; + var __createBinding = + (this && this.__createBinding) || + (Object.create + ? function (o, m, k, k2) { + if (k2 === undefined) k2 = k; + Object.defineProperty(o, k2, { + enumerable: true, + get: function () { + return m[k]; + }, + }); + } + : function (o, m, k, k2) { + if (k2 === undefined) k2 = k; + o[k2] = m[k]; + }); + var __setModuleDefault = + (this && this.__setModuleDefault) || + (Object.create + ? function (o, v) { + Object.defineProperty(o, 'default', { enumerable: true, value: v }); + } + : function (o, v) { + o['default'] = v; + }); + var __importStar = + (this && this.__importStar) || + function (mod) { + if (mod && mod.__esModule) return mod; + var result = {}; + if (mod != null) + for (var k in mod) + if (k !== 'default' && Object.hasOwnProperty.call(mod, k)) + __createBinding(result, mod, k); + __setModuleDefault(result, mod); + return result; + }; + Object.defineProperty(exports, '__esModule', { value: true }); + exports.Chrome70 = void 0; + const sdpTransform = __importStar(require('sdp-transform')); + const Logger_1 = require('../Logger'); + const utils = __importStar(require('../utils')); + const ortc = __importStar(require('../ortc')); + const sdpCommonUtils = __importStar(require('./sdp/commonUtils')); + const sdpUnifiedPlanUtils = __importStar(require('./sdp/unifiedPlanUtils')); + const HandlerInterface_1 = require('./HandlerInterface'); + const RemoteSdp_1 = require('./sdp/RemoteSdp'); + const scalabilityModes_1 = require('../scalabilityModes'); + const logger = new Logger_1.Logger('Chrome70'); + const SCTP_NUM_STREAMS = { OS: 1024, MIS: 1024 }; + class Chrome70 extends HandlerInterface_1.HandlerInterface { + constructor() { + super(); + // Map of RTCTransceivers indexed by MID. + this._mapMidTransceiver = new Map(); + // Local stream for sending. + this._sendStream = new MediaStream(); + // Whether a DataChannel m=application section has been created. + this._hasDataChannelMediaSection = false; + // Sending DataChannel id value counter. Incremented for each new DataChannel. + this._nextSendSctpStreamId = 0; + // Got transport local and remote parameters. + this._transportReady = false; + } + /** + * Creates a factory function. + */ + static createFactory() { + return () => new Chrome70(); + } + get name() { + return 'Chrome70'; + } + close() { + logger.debug('close()'); + // Close RTCPeerConnection. + if (this._pc) { + try { + this._pc.close(); + } catch (error) {} + } + } + async getNativeRtpCapabilities() { + logger.debug('getNativeRtpCapabilities()'); + const pc = new RTCPeerConnection({ + iceServers: [], + iceTransportPolicy: 'all', + bundlePolicy: 'max-bundle', + rtcpMuxPolicy: 'require', + sdpSemantics: 'unified-plan', + }); + try { + pc.addTransceiver('audio'); + pc.addTransceiver('video'); + const offer = await pc.createOffer(); + try { + pc.close(); + } catch (error) {} + const sdpObject = sdpTransform.parse(offer.sdp); + const nativeRtpCapabilities = sdpCommonUtils.extractRtpCapabilities({ sdpObject }); + return nativeRtpCapabilities; + } catch (error) { + try { + pc.close(); + } catch (error2) {} + throw error; + } + } + async getNativeSctpCapabilities() { + logger.debug('getNativeSctpCapabilities()'); + return { + numStreams: SCTP_NUM_STREAMS, + }; + } + run({ + direction, + iceParameters, + iceCandidates, + dtlsParameters, + sctpParameters, + iceServers, + iceTransportPolicy, + additionalSettings, + proprietaryConstraints, + extendedRtpCapabilities, + }) { + logger.debug('run()'); + this._direction = direction; + this._remoteSdp = new RemoteSdp_1.RemoteSdp({ + iceParameters, + iceCandidates, + dtlsParameters, + sctpParameters, + }); + this._sendingRtpParametersByKind = { + audio: ortc.getSendingRtpParameters('audio', extendedRtpCapabilities), + video: ortc.getSendingRtpParameters('video', extendedRtpCapabilities), + }; + this._sendingRemoteRtpParametersByKind = { + audio: ortc.getSendingRemoteRtpParameters('audio', extendedRtpCapabilities), + video: ortc.getSendingRemoteRtpParameters('video', extendedRtpCapabilities), + }; + this._pc = new RTCPeerConnection( + Object.assign( + { + iceServers: iceServers || [], + iceTransportPolicy: iceTransportPolicy || 'all', + bundlePolicy: 'max-bundle', + rtcpMuxPolicy: 'require', + sdpSemantics: 'unified-plan', + }, + additionalSettings, + ), + proprietaryConstraints, + ); + // Handle RTCPeerConnection connection status. + this._pc.addEventListener('iceconnectionstatechange', () => { + switch (this._pc.iceConnectionState) { + case 'checking': + this.emit('@connectionstatechange', 'connecting'); + break; + case 'connected': + case 'completed': + this.emit('@connectionstatechange', 'connected'); + break; + case 'failed': + this.emit('@connectionstatechange', 'failed'); + break; + case 'disconnected': + this.emit('@connectionstatechange', 'disconnected'); + break; + case 'closed': + this.emit('@connectionstatechange', 'closed'); + break; + } + }); + } + async updateIceServers(iceServers) { + logger.debug('updateIceServers()'); + const configuration = this._pc.getConfiguration(); + configuration.iceServers = iceServers; + this._pc.setConfiguration(configuration); + } + async restartIce(iceParameters) { + logger.debug('restartIce()'); + // Provide the remote SDP handler with new remote ICE parameters. + this._remoteSdp.updateIceParameters(iceParameters); + if (!this._transportReady) return; + if (this._direction === 'send') { + const offer = await this._pc.createOffer({ iceRestart: true }); + logger.debug('restartIce() | calling pc.setLocalDescription() [offer:%o]', offer); + await this._pc.setLocalDescription(offer); + const answer = { type: 'answer', sdp: this._remoteSdp.getSdp() }; + logger.debug('restartIce() | calling pc.setRemoteDescription() [answer:%o]', answer); + await this._pc.setRemoteDescription(answer); + } else { + const offer = { type: 'offer', sdp: this._remoteSdp.getSdp() }; + logger.debug('restartIce() | calling pc.setRemoteDescription() [offer:%o]', offer); + await this._pc.setRemoteDescription(offer); + const answer = await this._pc.createAnswer(); + logger.debug('restartIce() | calling pc.setLocalDescription() [answer:%o]', answer); + await this._pc.setLocalDescription(answer); + } + } + async getTransportStats() { + return this._pc.getStats(); + } + async send({ track, encodings, codecOptions, codec }) { + this._assertSendDirection(); + logger.debug('send() [kind:%s, track.id:%s]', track.kind, track.id); + const sendingRtpParameters = utils.clone(this._sendingRtpParametersByKind[track.kind], {}); + // This may throw. + sendingRtpParameters.codecs = ortc.reduceCodecs(sendingRtpParameters.codecs, codec); + const sendingRemoteRtpParameters = utils.clone( + this._sendingRemoteRtpParametersByKind[track.kind], + {}, + ); + // This may throw. + sendingRemoteRtpParameters.codecs = ortc.reduceCodecs(sendingRemoteRtpParameters.codecs, codec); + const mediaSectionIdx = this._remoteSdp.getNextMediaSectionIdx(); + const transceiver = this._pc.addTransceiver(track, { + direction: 'sendonly', + streams: [this._sendStream], + }); + let offer = await this._pc.createOffer(); + let localSdpObject = sdpTransform.parse(offer.sdp); + let offerMediaObject; + if (!this._transportReady) + await this._setupTransport({ localDtlsRole: 'server', localSdpObject }); + if (encodings && encodings.length > 1) { + logger.debug('send() | enabling legacy simulcast'); + localSdpObject = sdpTransform.parse(offer.sdp); + offerMediaObject = localSdpObject.media[mediaSectionIdx.idx]; + sdpUnifiedPlanUtils.addLegacySimulcast({ + offerMediaObject, + numStreams: encodings.length, + }); + offer = { type: 'offer', sdp: sdpTransform.write(localSdpObject) }; + } + // Special case for VP9 with SVC. + let hackVp9Svc = false; + const layers = scalabilityModes_1.parse((encodings || [{}])[0].scalabilityMode); + if ( + encodings && + encodings.length === 1 && + layers.spatialLayers > 1 && + sendingRtpParameters.codecs[0].mimeType.toLowerCase() === 'video/vp9' + ) { + logger.debug('send() | enabling legacy simulcast for VP9 SVC'); + hackVp9Svc = true; + localSdpObject = sdpTransform.parse(offer.sdp); + offerMediaObject = localSdpObject.media[mediaSectionIdx.idx]; + sdpUnifiedPlanUtils.addLegacySimulcast({ + offerMediaObject, + numStreams: layers.spatialLayers, + }); + offer = { type: 'offer', sdp: sdpTransform.write(localSdpObject) }; + } + logger.debug('send() | calling pc.setLocalDescription() [offer:%o]', offer); + await this._pc.setLocalDescription(offer); + // If encodings are given, apply them now. + if (encodings) { + logger.debug('send() | applying given encodings'); + const parameters = transceiver.sender.getParameters(); + for (let idx = 0; idx < (parameters.encodings || []).length; ++idx) { + const encoding = parameters.encodings[idx]; + const desiredEncoding = encodings[idx]; + // Should not happen but just in case. + if (!desiredEncoding) break; + parameters.encodings[idx] = Object.assign(encoding, desiredEncoding); + } + await transceiver.sender.setParameters(parameters); + } + // We can now get the transceiver.mid. + const localId = transceiver.mid; + // Set MID. + sendingRtpParameters.mid = localId; + localSdpObject = sdpTransform.parse(this._pc.localDescription.sdp); + offerMediaObject = localSdpObject.media[mediaSectionIdx.idx]; + // Set RTCP CNAME. + sendingRtpParameters.rtcp.cname = sdpCommonUtils.getCname({ offerMediaObject }); + // Set RTP encodings. + sendingRtpParameters.encodings = sdpUnifiedPlanUtils.getRtpEncodings({ offerMediaObject }); + // Complete encodings with given values. + if (encodings) { + for (let idx = 0; idx < sendingRtpParameters.encodings.length; ++idx) { + if (encodings[idx]) Object.assign(sendingRtpParameters.encodings[idx], encodings[idx]); + } + } + // Hack for VP9 SVC. + if (hackVp9Svc) { + sendingRtpParameters.encodings = [sendingRtpParameters.encodings[0]]; + } + // If VP8 or H264 and there is effective simulcast, add scalabilityMode to + // each encoding. + if ( + sendingRtpParameters.encodings.length > 1 && + (sendingRtpParameters.codecs[0].mimeType.toLowerCase() === 'video/vp8' || + sendingRtpParameters.codecs[0].mimeType.toLowerCase() === 'video/h264') + ) { + for (const encoding of sendingRtpParameters.encodings) { + encoding.scalabilityMode = 'S1T3'; + } + } + this._remoteSdp.send({ + offerMediaObject, + reuseMid: mediaSectionIdx.reuseMid, + offerRtpParameters: sendingRtpParameters, + answerRtpParameters: sendingRemoteRtpParameters, + codecOptions, + }); + const answer = { type: 'answer', sdp: this._remoteSdp.getSdp() }; + logger.debug('send() | calling pc.setRemoteDescription() [answer:%o]', answer); + await this._pc.setRemoteDescription(answer); + // Store in the map. + this._mapMidTransceiver.set(localId, transceiver); + return { + localId, + rtpParameters: sendingRtpParameters, + rtpSender: transceiver.sender, + }; + } + async stopSending(localId) { + this._assertSendDirection(); + logger.debug('stopSending() [localId:%s]', localId); + const transceiver = this._mapMidTransceiver.get(localId); + if (!transceiver) throw new Error('associated RTCRtpTransceiver not found'); + transceiver.sender.replaceTrack(null); + this._pc.removeTrack(transceiver.sender); + this._remoteSdp.closeMediaSection(transceiver.mid); + const offer = await this._pc.createOffer(); + logger.debug('stopSending() | calling pc.setLocalDescription() [offer:%o]', offer); + await this._pc.setLocalDescription(offer); + const answer = { type: 'answer', sdp: this._remoteSdp.getSdp() }; + logger.debug('stopSending() | calling pc.setRemoteDescription() [answer:%o]', answer); + await this._pc.setRemoteDescription(answer); + } + async replaceTrack(localId, track) { + this._assertSendDirection(); + if (track) { + logger.debug('replaceTrack() [localId:%s, track.id:%s]', localId, track.id); + } else { + logger.debug('replaceTrack() [localId:%s, no track]', localId); + } + const transceiver = this._mapMidTransceiver.get(localId); + if (!transceiver) throw new Error('associated RTCRtpTransceiver not found'); + await transceiver.sender.replaceTrack(track); + } + async setMaxSpatialLayer(localId, spatialLayer) { + this._assertSendDirection(); + logger.debug('setMaxSpatialLayer() [localId:%s, spatialLayer:%s]', localId, spatialLayer); + const transceiver = this._mapMidTransceiver.get(localId); + if (!transceiver) throw new Error('associated RTCRtpTransceiver not found'); + const parameters = transceiver.sender.getParameters(); + parameters.encodings.forEach((encoding, idx) => { + if (idx <= spatialLayer) encoding.active = true; + else encoding.active = false; + }); + await transceiver.sender.setParameters(parameters); + } + async setRtpEncodingParameters(localId, params) { + this._assertSendDirection(); + logger.debug('setRtpEncodingParameters() [localId:%s, params:%o]', localId, params); + const transceiver = this._mapMidTransceiver.get(localId); + if (!transceiver) throw new Error('associated RTCRtpTransceiver not found'); + const parameters = transceiver.sender.getParameters(); + parameters.encodings.forEach((encoding, idx) => { + parameters.encodings[idx] = Object.assign(Object.assign({}, encoding), params); + }); + await transceiver.sender.setParameters(parameters); + } + async getSenderStats(localId) { + this._assertSendDirection(); + const transceiver = this._mapMidTransceiver.get(localId); + if (!transceiver) throw new Error('associated RTCRtpTransceiver not found'); + return transceiver.sender.getStats(); + } + async sendDataChannel({ ordered, maxPacketLifeTime, maxRetransmits, label, protocol }) { + this._assertSendDirection(); + const options = { + negotiated: true, + id: this._nextSendSctpStreamId, + ordered, + maxPacketLifeTime, + maxRetransmitTime: maxPacketLifeTime, + maxRetransmits, + protocol, + }; + logger.debug('sendDataChannel() [options:%o]', options); + const dataChannel = this._pc.createDataChannel(label, options); + // Increase next id. + this._nextSendSctpStreamId = ++this._nextSendSctpStreamId % SCTP_NUM_STREAMS.MIS; + // If this is the first DataChannel we need to create the SDP answer with + // m=application section. + if (!this._hasDataChannelMediaSection) { + const offer = await this._pc.createOffer(); + const localSdpObject = sdpTransform.parse(offer.sdp); + const offerMediaObject = localSdpObject.media.find((m) => m.type === 'application'); + if (!this._transportReady) + await this._setupTransport({ localDtlsRole: 'server', localSdpObject }); + logger.debug('sendDataChannel() | calling pc.setLocalDescription() [offer:%o]', offer); + await this._pc.setLocalDescription(offer); + this._remoteSdp.sendSctpAssociation({ offerMediaObject }); + const answer = { type: 'answer', sdp: this._remoteSdp.getSdp() }; + logger.debug('sendDataChannel() | calling pc.setRemoteDescription() [answer:%o]', answer); + await this._pc.setRemoteDescription(answer); + this._hasDataChannelMediaSection = true; + } + const sctpStreamParameters = { + streamId: options.id, + ordered: options.ordered, + maxPacketLifeTime: options.maxPacketLifeTime, + maxRetransmits: options.maxRetransmits, + }; + return { dataChannel, sctpStreamParameters }; + } + async receive({ trackId, kind, rtpParameters }) { + this._assertRecvDirection(); + logger.debug('receive() [trackId:%s, kind:%s]', trackId, kind); + const localId = rtpParameters.mid || String(this._mapMidTransceiver.size); + this._remoteSdp.receive({ + mid: localId, + kind, + offerRtpParameters: rtpParameters, + streamId: rtpParameters.rtcp.cname, + trackId, + }); + const offer = { type: 'offer', sdp: this._remoteSdp.getSdp() }; + logger.debug('receive() | calling pc.setRemoteDescription() [offer:%o]', offer); + await this._pc.setRemoteDescription(offer); + let answer = await this._pc.createAnswer(); + const localSdpObject = sdpTransform.parse(answer.sdp); + const answerMediaObject = localSdpObject.media.find((m) => String(m.mid) === localId); + // May need to modify codec parameters in the answer based on codec + // parameters in the offer. + sdpCommonUtils.applyCodecParameters({ + offerRtpParameters: rtpParameters, + answerMediaObject, + }); + answer = { type: 'answer', sdp: sdpTransform.write(localSdpObject) }; + if (!this._transportReady) + await this._setupTransport({ localDtlsRole: 'client', localSdpObject }); + logger.debug('receive() | calling pc.setLocalDescription() [answer:%o]', answer); + await this._pc.setLocalDescription(answer); + const transceiver = this._pc.getTransceivers().find((t) => t.mid === localId); + if (!transceiver) throw new Error('new RTCRtpTransceiver not found'); + // Store in the map. + this._mapMidTransceiver.set(localId, transceiver); + return { + localId, + track: transceiver.receiver.track, + rtpReceiver: transceiver.receiver, + }; + } + async stopReceiving(localId) { + this._assertRecvDirection(); + logger.debug('stopReceiving() [localId:%s]', localId); + const transceiver = this._mapMidTransceiver.get(localId); + if (!transceiver) throw new Error('associated RTCRtpTransceiver not found'); + this._remoteSdp.closeMediaSection(transceiver.mid); + const offer = { type: 'offer', sdp: this._remoteSdp.getSdp() }; + logger.debug('stopReceiving() | calling pc.setRemoteDescription() [offer:%o]', offer); + await this._pc.setRemoteDescription(offer); + const answer = await this._pc.createAnswer(); + logger.debug('stopReceiving() | calling pc.setLocalDescription() [answer:%o]', answer); + await this._pc.setLocalDescription(answer); + } + async getReceiverStats(localId) { + this._assertRecvDirection(); + const transceiver = this._mapMidTransceiver.get(localId); + if (!transceiver) throw new Error('associated RTCRtpTransceiver not found'); + return transceiver.receiver.getStats(); + } + async receiveDataChannel({ sctpStreamParameters, label, protocol }) { + this._assertRecvDirection(); + const { streamId, ordered, maxPacketLifeTime, maxRetransmits } = sctpStreamParameters; + const options = { + negotiated: true, + id: streamId, + ordered, + maxPacketLifeTime, + maxRetransmitTime: maxPacketLifeTime, + maxRetransmits, + protocol, + }; + logger.debug('receiveDataChannel() [options:%o]', options); + const dataChannel = this._pc.createDataChannel(label, options); + // If this is the first DataChannel we need to create the SDP offer with + // m=application section. + if (!this._hasDataChannelMediaSection) { + this._remoteSdp.receiveSctpAssociation(); + const offer = { type: 'offer', sdp: this._remoteSdp.getSdp() }; + logger.debug('receiveDataChannel() | calling pc.setRemoteDescription() [offer:%o]', offer); + await this._pc.setRemoteDescription(offer); + const answer = await this._pc.createAnswer(); + if (!this._transportReady) { + const localSdpObject = sdpTransform.parse(answer.sdp); + await this._setupTransport({ localDtlsRole: 'client', localSdpObject }); + } + logger.debug( + 'receiveDataChannel() | calling pc.setRemoteDescription() [answer:%o]', + answer, + ); + await this._pc.setLocalDescription(answer); + this._hasDataChannelMediaSection = true; + } + return { dataChannel }; + } + async _setupTransport({ localDtlsRole, localSdpObject }) { + if (!localSdpObject) localSdpObject = sdpTransform.parse(this._pc.localDescription.sdp); + // Get our local DTLS parameters. + const dtlsParameters = sdpCommonUtils.extractDtlsParameters({ sdpObject: localSdpObject }); + // Set our DTLS role. + dtlsParameters.role = localDtlsRole; + // Update the remote DTLS role in the SDP. + this._remoteSdp.updateDtlsRole(localDtlsRole === 'client' ? 'server' : 'client'); + // Need to tell the remote transport about our parameters. + await this.safeEmitAsPromise('@connect', { dtlsParameters }); + this._transportReady = true; + } + _assertSendDirection() { + if (this._direction !== 'send') { + throw new Error('method can just be called for handlers with "send" direction'); + } + } + _assertRecvDirection() { + if (this._direction !== 'recv') { + throw new Error('method can just be called for handlers with "recv" direction'); + } + } + } + exports.Chrome70 = Chrome70; + }, + { + '../Logger': 13, + '../ortc': 36, + '../scalabilityModes': 37, + '../utils': 39, + './HandlerInterface': 25, + './sdp/RemoteSdp': 31, + './sdp/commonUtils': 32, + './sdp/unifiedPlanUtils': 34, + 'sdp-transform': 44, + }, + ], + 22: [ + function (require, module, exports) { + 'use strict'; + var __createBinding = + (this && this.__createBinding) || + (Object.create + ? function (o, m, k, k2) { + if (k2 === undefined) k2 = k; + Object.defineProperty(o, k2, { + enumerable: true, + get: function () { + return m[k]; + }, + }); + } + : function (o, m, k, k2) { + if (k2 === undefined) k2 = k; + o[k2] = m[k]; + }); + var __setModuleDefault = + (this && this.__setModuleDefault) || + (Object.create + ? function (o, v) { + Object.defineProperty(o, 'default', { enumerable: true, value: v }); + } + : function (o, v) { + o['default'] = v; + }); + var __importStar = + (this && this.__importStar) || + function (mod) { + if (mod && mod.__esModule) return mod; + var result = {}; + if (mod != null) + for (var k in mod) + if (k !== 'default' && Object.hasOwnProperty.call(mod, k)) + __createBinding(result, mod, k); + __setModuleDefault(result, mod); + return result; + }; + Object.defineProperty(exports, '__esModule', { value: true }); + exports.Chrome74 = void 0; + const sdpTransform = __importStar(require('sdp-transform')); + const Logger_1 = require('../Logger'); + const utils = __importStar(require('../utils')); + const ortc = __importStar(require('../ortc')); + const sdpCommonUtils = __importStar(require('./sdp/commonUtils')); + const sdpUnifiedPlanUtils = __importStar(require('./sdp/unifiedPlanUtils')); + const HandlerInterface_1 = require('./HandlerInterface'); + const RemoteSdp_1 = require('./sdp/RemoteSdp'); + const scalabilityModes_1 = require('../scalabilityModes'); + const logger = new Logger_1.Logger('Chrome74'); + const SCTP_NUM_STREAMS = { OS: 1024, MIS: 1024 }; + class Chrome74 extends HandlerInterface_1.HandlerInterface { + constructor() { + super(); + // Map of RTCTransceivers indexed by MID. + this._mapMidTransceiver = new Map(); + // Local stream for sending. + this._sendStream = new MediaStream(); + // Whether a DataChannel m=application section has been created. + this._hasDataChannelMediaSection = false; + // Sending DataChannel id value counter. Incremented for each new DataChannel. + this._nextSendSctpStreamId = 0; + // Got transport local and remote parameters. + this._transportReady = false; + } + /** + * Creates a factory function. + */ + static createFactory() { + return () => new Chrome74(); + } + get name() { + return 'Chrome74'; + } + close() { + logger.debug('close()'); + // Close RTCPeerConnection. + if (this._pc) { + try { + this._pc.close(); + } catch (error) {} + } + } + async getNativeRtpCapabilities() { + logger.debug('getNativeRtpCapabilities()'); + const pc = new RTCPeerConnection({ + iceServers: [], + iceTransportPolicy: 'all', + bundlePolicy: 'max-bundle', + rtcpMuxPolicy: 'require', + sdpSemantics: 'unified-plan', + }); + try { + pc.addTransceiver('audio'); + pc.addTransceiver('video'); + const offer = await pc.createOffer(); + try { + pc.close(); + } catch (error) {} + const sdpObject = sdpTransform.parse(offer.sdp); + const nativeRtpCapabilities = sdpCommonUtils.extractRtpCapabilities({ sdpObject }); + return nativeRtpCapabilities; + } catch (error) { + try { + pc.close(); + } catch (error2) {} + throw error; + } + } + async getNativeSctpCapabilities() { + logger.debug('getNativeSctpCapabilities()'); + return { + numStreams: SCTP_NUM_STREAMS, + }; + } + run({ + direction, + iceParameters, + iceCandidates, + dtlsParameters, + sctpParameters, + iceServers, + iceTransportPolicy, + additionalSettings, + proprietaryConstraints, + extendedRtpCapabilities, + }) { + logger.debug('run()'); + this._direction = direction; + this._remoteSdp = new RemoteSdp_1.RemoteSdp({ + iceParameters, + iceCandidates, + dtlsParameters, + sctpParameters, + }); + this._sendingRtpParametersByKind = { + audio: ortc.getSendingRtpParameters('audio', extendedRtpCapabilities), + video: ortc.getSendingRtpParameters('video', extendedRtpCapabilities), + }; + this._sendingRemoteRtpParametersByKind = { + audio: ortc.getSendingRemoteRtpParameters('audio', extendedRtpCapabilities), + video: ortc.getSendingRemoteRtpParameters('video', extendedRtpCapabilities), + }; + this._pc = new RTCPeerConnection( + Object.assign( + { + iceServers: iceServers || [], + iceTransportPolicy: iceTransportPolicy || 'all', + bundlePolicy: 'max-bundle', + rtcpMuxPolicy: 'require', + sdpSemantics: 'unified-plan', + }, + additionalSettings, + ), + proprietaryConstraints, + ); + // Handle RTCPeerConnection connection status. + this._pc.addEventListener('iceconnectionstatechange', () => { + switch (this._pc.iceConnectionState) { + case 'checking': + this.emit('@connectionstatechange', 'connecting'); + break; + case 'connected': + case 'completed': + this.emit('@connectionstatechange', 'connected'); + break; + case 'failed': + this.emit('@connectionstatechange', 'failed'); + break; + case 'disconnected': + this.emit('@connectionstatechange', 'disconnected'); + break; + case 'closed': + this.emit('@connectionstatechange', 'closed'); + break; + } + }); + } + async updateIceServers(iceServers) { + logger.debug('updateIceServers()'); + const configuration = this._pc.getConfiguration(); + configuration.iceServers = iceServers; + this._pc.setConfiguration(configuration); + } + async restartIce(iceParameters) { + logger.debug('restartIce()'); + // Provide the remote SDP handler with new remote ICE parameters. + this._remoteSdp.updateIceParameters(iceParameters); + if (!this._transportReady) return; + if (this._direction === 'send') { + const offer = await this._pc.createOffer({ iceRestart: true }); + logger.debug('restartIce() | calling pc.setLocalDescription() [offer:%o]', offer); + await this._pc.setLocalDescription(offer); + const answer = { type: 'answer', sdp: this._remoteSdp.getSdp() }; + logger.debug('restartIce() | calling pc.setRemoteDescription() [answer:%o]', answer); + await this._pc.setRemoteDescription(answer); + } else { + const offer = { type: 'offer', sdp: this._remoteSdp.getSdp() }; + logger.debug('restartIce() | calling pc.setRemoteDescription() [offer:%o]', offer); + await this._pc.setRemoteDescription(offer); + const answer = await this._pc.createAnswer(); + logger.debug('restartIce() | calling pc.setLocalDescription() [answer:%o]', answer); + await this._pc.setLocalDescription(answer); + } + } + async getTransportStats() { + return this._pc.getStats(); + } + async send({ track, encodings, codecOptions, codec }) { + this._assertSendDirection(); + logger.debug('send() [kind:%s, track.id:%s]', track.kind, track.id); + if (encodings && encodings.length > 1) { + encodings.forEach((encoding, idx) => { + encoding.rid = `r${idx}`; + }); + } + const sendingRtpParameters = utils.clone(this._sendingRtpParametersByKind[track.kind], {}); + // This may throw. + sendingRtpParameters.codecs = ortc.reduceCodecs(sendingRtpParameters.codecs, codec); + const sendingRemoteRtpParameters = utils.clone( + this._sendingRemoteRtpParametersByKind[track.kind], + {}, + ); + // This may throw. + sendingRemoteRtpParameters.codecs = ortc.reduceCodecs(sendingRemoteRtpParameters.codecs, codec); + const mediaSectionIdx = this._remoteSdp.getNextMediaSectionIdx(); + const transceiver = this._pc.addTransceiver(track, { + direction: 'sendonly', + streams: [this._sendStream], + sendEncodings: encodings, + }); + let offer = await this._pc.createOffer(); + let localSdpObject = sdpTransform.parse(offer.sdp); + let offerMediaObject; + if (!this._transportReady) + await this._setupTransport({ localDtlsRole: 'server', localSdpObject }); + // Special case for VP9 with SVC. + let hackVp9Svc = false; + const layers = scalabilityModes_1.parse((encodings || [{}])[0].scalabilityMode); + if ( + encodings && + encodings.length === 1 && + layers.spatialLayers > 1 && + sendingRtpParameters.codecs[0].mimeType.toLowerCase() === 'video/vp9' + ) { + logger.debug('send() | enabling legacy simulcast for VP9 SVC'); + hackVp9Svc = true; + localSdpObject = sdpTransform.parse(offer.sdp); + offerMediaObject = localSdpObject.media[mediaSectionIdx.idx]; + sdpUnifiedPlanUtils.addLegacySimulcast({ + offerMediaObject, + numStreams: layers.spatialLayers, + }); + offer = { type: 'offer', sdp: sdpTransform.write(localSdpObject) }; + } + logger.debug('send() | calling pc.setLocalDescription() [offer:%o]', offer); + await this._pc.setLocalDescription(offer); + // We can now get the transceiver.mid. + const localId = transceiver.mid; + // Set MID. + sendingRtpParameters.mid = localId; + localSdpObject = sdpTransform.parse(this._pc.localDescription.sdp); + offerMediaObject = localSdpObject.media[mediaSectionIdx.idx]; + // Set RTCP CNAME. + sendingRtpParameters.rtcp.cname = sdpCommonUtils.getCname({ offerMediaObject }); + // Set RTP encodings by parsing the SDP offer if no encodings are given. + if (!encodings) { + sendingRtpParameters.encodings = sdpUnifiedPlanUtils.getRtpEncodings({ offerMediaObject }); + } + // Set RTP encodings by parsing the SDP offer and complete them with given + // one if just a single encoding has been given. + else if (encodings.length === 1) { + let newEncodings = sdpUnifiedPlanUtils.getRtpEncodings({ offerMediaObject }); + Object.assign(newEncodings[0], encodings[0]); + // Hack for VP9 SVC. + if (hackVp9Svc) newEncodings = [newEncodings[0]]; + sendingRtpParameters.encodings = newEncodings; + } + // Otherwise if more than 1 encoding are given use them verbatim. + else { + sendingRtpParameters.encodings = encodings; + } + // If VP8 or H264 and there is effective simulcast, add scalabilityMode to + // each encoding. + if ( + sendingRtpParameters.encodings.length > 1 && + (sendingRtpParameters.codecs[0].mimeType.toLowerCase() === 'video/vp8' || + sendingRtpParameters.codecs[0].mimeType.toLowerCase() === 'video/h264') + ) { + for (const encoding of sendingRtpParameters.encodings) { + encoding.scalabilityMode = 'S1T3'; + } + } + this._remoteSdp.send({ + offerMediaObject, + reuseMid: mediaSectionIdx.reuseMid, + offerRtpParameters: sendingRtpParameters, + answerRtpParameters: sendingRemoteRtpParameters, + codecOptions, + extmapAllowMixed: true, + }); + const answer = { type: 'answer', sdp: this._remoteSdp.getSdp() }; + logger.debug('send() | calling pc.setRemoteDescription() [answer:%o]', answer); + await this._pc.setRemoteDescription(answer); + // Store in the map. + this._mapMidTransceiver.set(localId, transceiver); + return { + localId, + rtpParameters: sendingRtpParameters, + rtpSender: transceiver.sender, + }; + } + async stopSending(localId) { + this._assertSendDirection(); + logger.debug('stopSending() [localId:%s]', localId); + const transceiver = this._mapMidTransceiver.get(localId); + if (!transceiver) throw new Error('associated RTCRtpTransceiver not found'); + transceiver.sender.replaceTrack(null); + this._pc.removeTrack(transceiver.sender); + this._remoteSdp.closeMediaSection(transceiver.mid); + const offer = await this._pc.createOffer(); + logger.debug('stopSending() | calling pc.setLocalDescription() [offer:%o]', offer); + await this._pc.setLocalDescription(offer); + const answer = { type: 'answer', sdp: this._remoteSdp.getSdp() }; + logger.debug('stopSending() | calling pc.setRemoteDescription() [answer:%o]', answer); + await this._pc.setRemoteDescription(answer); + } + async replaceTrack(localId, track) { + this._assertSendDirection(); + if (track) { + logger.debug('replaceTrack() [localId:%s, track.id:%s]', localId, track.id); + } else { + logger.debug('replaceTrack() [localId:%s, no track]', localId); + } + const transceiver = this._mapMidTransceiver.get(localId); + if (!transceiver) throw new Error('associated RTCRtpTransceiver not found'); + await transceiver.sender.replaceTrack(track); + } + async setMaxSpatialLayer(localId, spatialLayer) { + this._assertSendDirection(); + logger.debug('setMaxSpatialLayer() [localId:%s, spatialLayer:%s]', localId, spatialLayer); + const transceiver = this._mapMidTransceiver.get(localId); + if (!transceiver) throw new Error('associated RTCRtpTransceiver not found'); + const parameters = transceiver.sender.getParameters(); + parameters.encodings.forEach((encoding, idx) => { + if (idx <= spatialLayer) encoding.active = true; + else encoding.active = false; + }); + await transceiver.sender.setParameters(parameters); + } + async setRtpEncodingParameters(localId, params) { + this._assertSendDirection(); + logger.debug('setRtpEncodingParameters() [localId:%s, params:%o]', localId, params); + const transceiver = this._mapMidTransceiver.get(localId); + if (!transceiver) throw new Error('associated RTCRtpTransceiver not found'); + const parameters = transceiver.sender.getParameters(); + parameters.encodings.forEach((encoding, idx) => { + parameters.encodings[idx] = Object.assign(Object.assign({}, encoding), params); + }); + await transceiver.sender.setParameters(parameters); + } + async getSenderStats(localId) { + this._assertSendDirection(); + const transceiver = this._mapMidTransceiver.get(localId); + if (!transceiver) throw new Error('associated RTCRtpTransceiver not found'); + return transceiver.sender.getStats(); + } + async sendDataChannel({ ordered, maxPacketLifeTime, maxRetransmits, label, protocol }) { + this._assertSendDirection(); + const options = { + negotiated: true, + id: this._nextSendSctpStreamId, + ordered, + maxPacketLifeTime, + maxRetransmits, + protocol, + }; + logger.debug('sendDataChannel() [options:%o]', options); + const dataChannel = this._pc.createDataChannel(label, options); + // Increase next id. + this._nextSendSctpStreamId = ++this._nextSendSctpStreamId % SCTP_NUM_STREAMS.MIS; + // If this is the first DataChannel we need to create the SDP answer with + // m=application section. + if (!this._hasDataChannelMediaSection) { + const offer = await this._pc.createOffer(); + const localSdpObject = sdpTransform.parse(offer.sdp); + const offerMediaObject = localSdpObject.media.find((m) => m.type === 'application'); + if (!this._transportReady) + await this._setupTransport({ localDtlsRole: 'server', localSdpObject }); + logger.debug('sendDataChannel() | calling pc.setLocalDescription() [offer:%o]', offer); + await this._pc.setLocalDescription(offer); + this._remoteSdp.sendSctpAssociation({ offerMediaObject }); + const answer = { type: 'answer', sdp: this._remoteSdp.getSdp() }; + logger.debug('sendDataChannel() | calling pc.setRemoteDescription() [answer:%o]', answer); + await this._pc.setRemoteDescription(answer); + this._hasDataChannelMediaSection = true; + } + const sctpStreamParameters = { + streamId: options.id, + ordered: options.ordered, + maxPacketLifeTime: options.maxPacketLifeTime, + maxRetransmits: options.maxRetransmits, + }; + return { dataChannel, sctpStreamParameters }; + } + async receive({ trackId, kind, rtpParameters }) { + this._assertRecvDirection(); + logger.debug('receive() [trackId:%s, kind:%s]', trackId, kind); + const localId = rtpParameters.mid || String(this._mapMidTransceiver.size); + this._remoteSdp.receive({ + mid: localId, + kind, + offerRtpParameters: rtpParameters, + streamId: rtpParameters.rtcp.cname, + trackId, + }); + const offer = { type: 'offer', sdp: this._remoteSdp.getSdp() }; + logger.debug('receive() | calling pc.setRemoteDescription() [offer:%o]', offer); + await this._pc.setRemoteDescription(offer); + let answer = await this._pc.createAnswer(); + const localSdpObject = sdpTransform.parse(answer.sdp); + const answerMediaObject = localSdpObject.media.find((m) => String(m.mid) === localId); + // May need to modify codec parameters in the answer based on codec + // parameters in the offer. + sdpCommonUtils.applyCodecParameters({ + offerRtpParameters: rtpParameters, + answerMediaObject, + }); + answer = { type: 'answer', sdp: sdpTransform.write(localSdpObject) }; + if (!this._transportReady) + await this._setupTransport({ localDtlsRole: 'client', localSdpObject }); + logger.debug('receive() | calling pc.setLocalDescription() [answer:%o]', answer); + await this._pc.setLocalDescription(answer); + const transceiver = this._pc.getTransceivers().find((t) => t.mid === localId); + if (!transceiver) throw new Error('new RTCRtpTransceiver not found'); + // Store in the map. + this._mapMidTransceiver.set(localId, transceiver); + return { + localId, + track: transceiver.receiver.track, + rtpReceiver: transceiver.receiver, + }; + } + async stopReceiving(localId) { + this._assertRecvDirection(); + logger.debug('stopReceiving() [localId:%s]', localId); + const transceiver = this._mapMidTransceiver.get(localId); + if (!transceiver) throw new Error('associated RTCRtpTransceiver not found'); + this._remoteSdp.closeMediaSection(transceiver.mid); + const offer = { type: 'offer', sdp: this._remoteSdp.getSdp() }; + logger.debug('stopReceiving() | calling pc.setRemoteDescription() [offer:%o]', offer); + await this._pc.setRemoteDescription(offer); + const answer = await this._pc.createAnswer(); + logger.debug('stopReceiving() | calling pc.setLocalDescription() [answer:%o]', answer); + await this._pc.setLocalDescription(answer); + } + async getReceiverStats(localId) { + this._assertRecvDirection(); + const transceiver = this._mapMidTransceiver.get(localId); + if (!transceiver) throw new Error('associated RTCRtpTransceiver not found'); + return transceiver.receiver.getStats(); + } + async receiveDataChannel({ sctpStreamParameters, label, protocol }) { + this._assertRecvDirection(); + const { streamId, ordered, maxPacketLifeTime, maxRetransmits } = sctpStreamParameters; + const options = { + negotiated: true, + id: streamId, + ordered, + maxPacketLifeTime, + maxRetransmits, + protocol, + }; + logger.debug('receiveDataChannel() [options:%o]', options); + const dataChannel = this._pc.createDataChannel(label, options); + // If this is the first DataChannel we need to create the SDP offer with + // m=application section. + if (!this._hasDataChannelMediaSection) { + this._remoteSdp.receiveSctpAssociation(); + const offer = { type: 'offer', sdp: this._remoteSdp.getSdp() }; + logger.debug('receiveDataChannel() | calling pc.setRemoteDescription() [offer:%o]', offer); + await this._pc.setRemoteDescription(offer); + const answer = await this._pc.createAnswer(); + if (!this._transportReady) { + const localSdpObject = sdpTransform.parse(answer.sdp); + await this._setupTransport({ localDtlsRole: 'client', localSdpObject }); + } + logger.debug( + 'receiveDataChannel() | calling pc.setRemoteDescription() [answer:%o]', + answer, + ); + await this._pc.setLocalDescription(answer); + this._hasDataChannelMediaSection = true; + } + return { dataChannel }; + } + async _setupTransport({ localDtlsRole, localSdpObject }) { + if (!localSdpObject) localSdpObject = sdpTransform.parse(this._pc.localDescription.sdp); + // Get our local DTLS parameters. + const dtlsParameters = sdpCommonUtils.extractDtlsParameters({ sdpObject: localSdpObject }); + // Set our DTLS role. + dtlsParameters.role = localDtlsRole; + // Update the remote DTLS role in the SDP. + this._remoteSdp.updateDtlsRole(localDtlsRole === 'client' ? 'server' : 'client'); + // Need to tell the remote transport about our parameters. + await this.safeEmitAsPromise('@connect', { dtlsParameters }); + this._transportReady = true; + } + _assertSendDirection() { + if (this._direction !== 'send') { + throw new Error('method can just be called for handlers with "send" direction'); + } + } + _assertRecvDirection() { + if (this._direction !== 'recv') { + throw new Error('method can just be called for handlers with "recv" direction'); + } + } + } + exports.Chrome74 = Chrome74; + }, + { + '../Logger': 13, + '../ortc': 36, + '../scalabilityModes': 37, + '../utils': 39, + './HandlerInterface': 25, + './sdp/RemoteSdp': 31, + './sdp/commonUtils': 32, + './sdp/unifiedPlanUtils': 34, + 'sdp-transform': 44, + }, + ], + 23: [ + function (require, module, exports) { + 'use strict'; + var __createBinding = + (this && this.__createBinding) || + (Object.create + ? function (o, m, k, k2) { + if (k2 === undefined) k2 = k; + Object.defineProperty(o, k2, { + enumerable: true, + get: function () { + return m[k]; + }, + }); + } + : function (o, m, k, k2) { + if (k2 === undefined) k2 = k; + o[k2] = m[k]; + }); + var __setModuleDefault = + (this && this.__setModuleDefault) || + (Object.create + ? function (o, v) { + Object.defineProperty(o, 'default', { enumerable: true, value: v }); + } + : function (o, v) { + o['default'] = v; + }); + var __importStar = + (this && this.__importStar) || + function (mod) { + if (mod && mod.__esModule) return mod; + var result = {}; + if (mod != null) + for (var k in mod) + if (k !== 'default' && Object.hasOwnProperty.call(mod, k)) + __createBinding(result, mod, k); + __setModuleDefault(result, mod); + return result; + }; + Object.defineProperty(exports, '__esModule', { value: true }); + exports.Edge11 = void 0; + const Logger_1 = require('../Logger'); + const errors_1 = require('../errors'); + const utils = __importStar(require('../utils')); + const ortc = __importStar(require('../ortc')); + const edgeUtils = __importStar(require('./ortc/edgeUtils')); + const HandlerInterface_1 = require('./HandlerInterface'); + const logger = new Logger_1.Logger('Edge11'); + class Edge11 extends HandlerInterface_1.HandlerInterface { + constructor() { + super(); + // Map of RTCRtpSenders indexed by id. + this._rtpSenders = new Map(); + // Map of RTCRtpReceivers indexed by id. + this._rtpReceivers = new Map(); + // Next localId for sending tracks. + this._nextSendLocalId = 0; + // Got transport local and remote parameters. + this._transportReady = false; + } + /** + * Creates a factory function. + */ + static createFactory() { + return () => new Edge11(); + } + get name() { + return 'Edge11'; + } + close() { + logger.debug('close()'); + // Close the ICE gatherer. + // NOTE: Not yet implemented by Edge. + try { + this._iceGatherer.close(); + } catch (error) {} + // Close the ICE transport. + try { + this._iceTransport.stop(); + } catch (error) {} + // Close the DTLS transport. + try { + this._dtlsTransport.stop(); + } catch (error) {} + // Close RTCRtpSenders. + for (const rtpSender of this._rtpSenders.values()) { + try { + rtpSender.stop(); + } catch (error) {} + } + // Close RTCRtpReceivers. + for (const rtpReceiver of this._rtpReceivers.values()) { + try { + rtpReceiver.stop(); + } catch (error) {} + } + } + async getNativeRtpCapabilities() { + logger.debug('getNativeRtpCapabilities()'); + return edgeUtils.getCapabilities(); + } + async getNativeSctpCapabilities() { + logger.debug('getNativeSctpCapabilities()'); + return { + numStreams: { OS: 0, MIS: 0 }, + }; + } + run({ + direction, // eslint-disable-line @typescript-eslint/no-unused-vars + iceParameters, + iceCandidates, + dtlsParameters, + sctpParameters, // eslint-disable-line @typescript-eslint/no-unused-vars + iceServers, + iceTransportPolicy, + additionalSettings, // eslint-disable-line @typescript-eslint/no-unused-vars + proprietaryConstraints, // eslint-disable-line @typescript-eslint/no-unused-vars + extendedRtpCapabilities, + }) { + logger.debug('run()'); + this._sendingRtpParametersByKind = { + audio: ortc.getSendingRtpParameters('audio', extendedRtpCapabilities), + video: ortc.getSendingRtpParameters('video', extendedRtpCapabilities), + }; + this._remoteIceParameters = iceParameters; + this._remoteIceCandidates = iceCandidates; + this._remoteDtlsParameters = dtlsParameters; + this._cname = `CNAME-${utils.generateRandomNumber()}`; + this._setIceGatherer({ iceServers, iceTransportPolicy }); + this._setIceTransport(); + this._setDtlsTransport(); + } + // eslint-disable-next-line @typescript-eslint/no-unused-vars + async updateIceServers(iceServers) { + // NOTE: Edge 11 does not implement iceGatherer.gater(). + throw new errors_1.UnsupportedError('not supported'); + } + async restartIce(iceParameters) { + logger.debug('restartIce()'); + this._remoteIceParameters = iceParameters; + if (!this._transportReady) return; + logger.debug('restartIce() | calling iceTransport.start()'); + this._iceTransport.start(this._iceGatherer, iceParameters, 'controlling'); + for (const candidate of this._remoteIceCandidates) { + this._iceTransport.addRemoteCandidate(candidate); + } + this._iceTransport.addRemoteCandidate({}); + } + async getTransportStats() { + return this._iceTransport.getStats(); + } + async send( + // eslint-disable-next-line @typescript-eslint/no-unused-vars + { track, encodings, codecOptions, codec }, + ) { + logger.debug('send() [kind:%s, track.id:%s]', track.kind, track.id); + if (!this._transportReady) await this._setupTransport({ localDtlsRole: 'server' }); + logger.debug('send() | calling new RTCRtpSender()'); + const rtpSender = new RTCRtpSender(track, this._dtlsTransport); + const rtpParameters = utils.clone(this._sendingRtpParametersByKind[track.kind], {}); + rtpParameters.codecs = ortc.reduceCodecs(rtpParameters.codecs, codec); + const useRtx = rtpParameters.codecs.some((_codec) => /.+\/rtx$/i.test(_codec.mimeType)); + if (!encodings) encodings = [{}]; + for (const encoding of encodings) { + encoding.ssrc = utils.generateRandomNumber(); + if (useRtx) encoding.rtx = { ssrc: utils.generateRandomNumber() }; + } + rtpParameters.encodings = encodings; + // Fill RTCRtpParameters.rtcp. + rtpParameters.rtcp = { + cname: this._cname, + reducedSize: true, + mux: true, + }; + // NOTE: Convert our standard RTCRtpParameters into those that Edge + // expects. + const edgeRtpParameters = edgeUtils.mangleRtpParameters(rtpParameters); + logger.debug('send() | calling rtpSender.send() [params:%o]', edgeRtpParameters); + await rtpSender.send(edgeRtpParameters); + const localId = String(this._nextSendLocalId); + this._nextSendLocalId++; + // Store it. + this._rtpSenders.set(localId, rtpSender); + return { localId, rtpParameters, rtpSender }; + } + async stopSending(localId) { + logger.debug('stopSending() [localId:%s]', localId); + const rtpSender = this._rtpSenders.get(localId); + if (!rtpSender) throw new Error('RTCRtpSender not found'); + this._rtpSenders.delete(localId); + try { + logger.debug('stopSending() | calling rtpSender.stop()'); + rtpSender.stop(); + } catch (error) { + logger.warn('stopSending() | rtpSender.stop() failed:%o', error); + throw error; + } + } + async replaceTrack(localId, track) { + if (track) { + logger.debug('replaceTrack() [localId:%s, track.id:%s]', localId, track.id); + } else { + logger.debug('replaceTrack() [localId:%s, no track]', localId); + } + const rtpSender = this._rtpSenders.get(localId); + if (!rtpSender) throw new Error('RTCRtpSender not found'); + rtpSender.setTrack(track); + } + async setMaxSpatialLayer(localId, spatialLayer) { + logger.debug('setMaxSpatialLayer() [localId:%s, spatialLayer:%s]', localId, spatialLayer); + const rtpSender = this._rtpSenders.get(localId); + if (!rtpSender) throw new Error('RTCRtpSender not found'); + const parameters = rtpSender.getParameters(); + parameters.encodings.forEach((encoding, idx) => { + if (idx <= spatialLayer) encoding.active = true; + else encoding.active = false; + }); + await rtpSender.setParameters(parameters); + } + async setRtpEncodingParameters(localId, params) { + logger.debug('setRtpEncodingParameters() [localId:%s, params:%o]', localId, params); + const rtpSender = this._rtpSenders.get(localId); + if (!rtpSender) throw new Error('RTCRtpSender not found'); + const parameters = rtpSender.getParameters(); + parameters.encodings.forEach((encoding, idx) => { + parameters.encodings[idx] = Object.assign(Object.assign({}, encoding), params); + }); + await rtpSender.setParameters(parameters); + } + async getSenderStats(localId) { + const rtpSender = this._rtpSenders.get(localId); + if (!rtpSender) throw new Error('RTCRtpSender not found'); + return rtpSender.getStats(); + } + async sendDataChannel( + // eslint-disable-next-line @typescript-eslint/no-unused-vars + options, + ) { + throw new errors_1.UnsupportedError('not implemented'); + } + async receive({ trackId, kind, rtpParameters }) { + logger.debug('receive() [trackId:%s, kind:%s]', trackId, kind); + if (!this._transportReady) await this._setupTransport({ localDtlsRole: 'server' }); + logger.debug('receive() | calling new RTCRtpReceiver()'); + const rtpReceiver = new RTCRtpReceiver(this._dtlsTransport, kind); + rtpReceiver.addEventListener('error', (event) => { + logger.error('rtpReceiver "error" event [event:%o]', event); + }); + // NOTE: Convert our standard RTCRtpParameters into those that Edge + // expects. + const edgeRtpParameters = edgeUtils.mangleRtpParameters(rtpParameters); + logger.debug('receive() | calling rtpReceiver.receive() [params:%o]', edgeRtpParameters); + await rtpReceiver.receive(edgeRtpParameters); + const localId = trackId; + // Store it. + this._rtpReceivers.set(localId, rtpReceiver); + return { + localId, + track: rtpReceiver.track, + rtpReceiver, + }; + } + async stopReceiving(localId) { + logger.debug('stopReceiving() [localId:%s]', localId); + const rtpReceiver = this._rtpReceivers.get(localId); + if (!rtpReceiver) throw new Error('RTCRtpReceiver not found'); + this._rtpReceivers.delete(localId); + try { + logger.debug('stopReceiving() | calling rtpReceiver.stop()'); + rtpReceiver.stop(); + } catch (error) { + logger.warn('stopReceiving() | rtpReceiver.stop() failed:%o', error); + } + } + async getReceiverStats(localId) { + const rtpReceiver = this._rtpReceivers.get(localId); + if (!rtpReceiver) throw new Error('RTCRtpReceiver not found'); + return rtpReceiver.getStats(); + } + async receiveDataChannel( + // eslint-disable-next-line @typescript-eslint/no-unused-vars + options, + ) { + throw new errors_1.UnsupportedError('not implemented'); + } + _setIceGatherer({ iceServers, iceTransportPolicy }) { + const iceGatherer = new RTCIceGatherer({ + iceServers: iceServers || [], + gatherPolicy: iceTransportPolicy || 'all', + }); + iceGatherer.addEventListener('error', (event) => { + logger.error('iceGatherer "error" event [event:%o]', event); + }); + // NOTE: Not yet implemented by Edge, which starts gathering automatically. + try { + iceGatherer.gather(); + } catch (error) { + logger.debug('_setIceGatherer() | iceGatherer.gather() failed: %s', error.toString()); + } + this._iceGatherer = iceGatherer; + } + _setIceTransport() { + const iceTransport = new RTCIceTransport(this._iceGatherer); + // NOTE: Not yet implemented by Edge. + iceTransport.addEventListener('statechange', () => { + switch (iceTransport.state) { + case 'checking': + this.emit('@connectionstatechange', 'connecting'); + break; + case 'connected': + case 'completed': + this.emit('@connectionstatechange', 'connected'); + break; + case 'failed': + this.emit('@connectionstatechange', 'failed'); + break; + case 'disconnected': + this.emit('@connectionstatechange', 'disconnected'); + break; + case 'closed': + this.emit('@connectionstatechange', 'closed'); + break; + } + }); + // NOTE: Not standard, but implemented by Edge. + iceTransport.addEventListener('icestatechange', () => { + switch (iceTransport.state) { + case 'checking': + this.emit('@connectionstatechange', 'connecting'); + break; + case 'connected': + case 'completed': + this.emit('@connectionstatechange', 'connected'); + break; + case 'failed': + this.emit('@connectionstatechange', 'failed'); + break; + case 'disconnected': + this.emit('@connectionstatechange', 'disconnected'); + break; + case 'closed': + this.emit('@connectionstatechange', 'closed'); + break; + } + }); + iceTransport.addEventListener('candidatepairchange', (event) => { + logger.debug('iceTransport "candidatepairchange" event [pair:%o]', event.pair); + }); + this._iceTransport = iceTransport; + } + _setDtlsTransport() { + const dtlsTransport = new RTCDtlsTransport(this._iceTransport); + // NOTE: Not yet implemented by Edge. + dtlsTransport.addEventListener('statechange', () => { + logger.debug('dtlsTransport "statechange" event [state:%s]', dtlsTransport.state); + }); + // NOTE: Not standard, but implemented by Edge. + dtlsTransport.addEventListener('dtlsstatechange', () => { + logger.debug('dtlsTransport "dtlsstatechange" event [state:%s]', dtlsTransport.state); + if (dtlsTransport.state === 'closed') this.emit('@connectionstatechange', 'closed'); + }); + dtlsTransport.addEventListener('error', (event) => { + logger.error('dtlsTransport "error" event [event:%o]', event); + }); + this._dtlsTransport = dtlsTransport; + } + async _setupTransport({ localDtlsRole }) { + logger.debug('_setupTransport()'); + // Get our local DTLS parameters. + const dtlsParameters = this._dtlsTransport.getLocalParameters(); + dtlsParameters.role = localDtlsRole; + // Need to tell the remote transport about our parameters. + await this.safeEmitAsPromise('@connect', { dtlsParameters }); + // Start the RTCIceTransport. + this._iceTransport.start(this._iceGatherer, this._remoteIceParameters, 'controlling'); + // Add remote ICE candidates. + for (const candidate of this._remoteIceCandidates) { + this._iceTransport.addRemoteCandidate(candidate); + } + // Also signal a 'complete' candidate as per spec. + // NOTE: It should be {complete: true} but Edge prefers {}. + // NOTE: If we don't signal end of candidates, the Edge RTCIceTransport + // won't enter the 'completed' state. + this._iceTransport.addRemoteCandidate({}); + // NOTE: Edge does not like SHA less than 256. + this._remoteDtlsParameters.fingerprints = this._remoteDtlsParameters.fingerprints.filter( + (fingerprint) => { + return ( + fingerprint.algorithm === 'sha-256' || + fingerprint.algorithm === 'sha-384' || + fingerprint.algorithm === 'sha-512' + ); + }, + ); + // Start the RTCDtlsTransport. + this._dtlsTransport.start(this._remoteDtlsParameters); + this._transportReady = true; + } + } + exports.Edge11 = Edge11; + }, + { + '../Logger': 13, + '../errors': 18, + '../ortc': 36, + '../utils': 39, + './HandlerInterface': 25, + './ortc/edgeUtils': 29, + }, + ], + 24: [ + function (require, module, exports) { + 'use strict'; + var __createBinding = + (this && this.__createBinding) || + (Object.create + ? function (o, m, k, k2) { + if (k2 === undefined) k2 = k; + Object.defineProperty(o, k2, { + enumerable: true, + get: function () { + return m[k]; + }, + }); + } + : function (o, m, k, k2) { + if (k2 === undefined) k2 = k; + o[k2] = m[k]; + }); + var __setModuleDefault = + (this && this.__setModuleDefault) || + (Object.create + ? function (o, v) { + Object.defineProperty(o, 'default', { enumerable: true, value: v }); + } + : function (o, v) { + o['default'] = v; + }); + var __importStar = + (this && this.__importStar) || + function (mod) { + if (mod && mod.__esModule) return mod; + var result = {}; + if (mod != null) + for (var k in mod) + if (k !== 'default' && Object.hasOwnProperty.call(mod, k)) + __createBinding(result, mod, k); + __setModuleDefault(result, mod); + return result; + }; + Object.defineProperty(exports, '__esModule', { value: true }); + exports.Firefox60 = void 0; + const sdpTransform = __importStar(require('sdp-transform')); + const Logger_1 = require('../Logger'); + const errors_1 = require('../errors'); + const utils = __importStar(require('../utils')); + const ortc = __importStar(require('../ortc')); + const sdpCommonUtils = __importStar(require('./sdp/commonUtils')); + const sdpUnifiedPlanUtils = __importStar(require('./sdp/unifiedPlanUtils')); + const HandlerInterface_1 = require('./HandlerInterface'); + const RemoteSdp_1 = require('./sdp/RemoteSdp'); + const logger = new Logger_1.Logger('Firefox60'); + const SCTP_NUM_STREAMS = { OS: 16, MIS: 2048 }; + class Firefox60 extends HandlerInterface_1.HandlerInterface { + constructor() { + super(); + // Map of RTCTransceivers indexed by MID. + this._mapMidTransceiver = new Map(); + // Local stream for sending. + this._sendStream = new MediaStream(); + // Whether a DataChannel m=application section has been created. + this._hasDataChannelMediaSection = false; + // Sending DataChannel id value counter. Incremented for each new DataChannel. + this._nextSendSctpStreamId = 0; + // Got transport local and remote parameters. + this._transportReady = false; + } + /** + * Creates a factory function. + */ + static createFactory() { + return () => new Firefox60(); + } + get name() { + return 'Firefox60'; + } + close() { + logger.debug('close()'); + // Close RTCPeerConnection. + if (this._pc) { + try { + this._pc.close(); + } catch (error) {} + } + } + async getNativeRtpCapabilities() { + logger.debug('getNativeRtpCapabilities()'); + const pc = new RTCPeerConnection({ + iceServers: [], + iceTransportPolicy: 'all', + bundlePolicy: 'max-bundle', + rtcpMuxPolicy: 'require', + }); + // NOTE: We need to add a real video track to get the RID extension mapping. + const canvas = document.createElement('canvas'); + // NOTE: Otherwise Firefox fails in next line. + canvas.getContext('2d'); + const fakeStream = canvas.captureStream(); + const fakeVideoTrack = fakeStream.getVideoTracks()[0]; + try { + pc.addTransceiver('audio', { direction: 'sendrecv' }); + const videoTransceiver = pc.addTransceiver(fakeVideoTrack, { direction: 'sendrecv' }); + const parameters = videoTransceiver.sender.getParameters(); + const encodings = [ + { rid: 'r0', maxBitrate: 100000 }, + { rid: 'r1', maxBitrate: 500000 }, + ]; + parameters.encodings = encodings; + await videoTransceiver.sender.setParameters(parameters); + const offer = await pc.createOffer(); + try { + canvas.remove(); + } catch (error) {} + try { + fakeVideoTrack.stop(); + } catch (error) {} + try { + pc.close(); + } catch (error) {} + const sdpObject = sdpTransform.parse(offer.sdp); + const nativeRtpCapabilities = sdpCommonUtils.extractRtpCapabilities({ sdpObject }); + return nativeRtpCapabilities; + } catch (error) { + try { + canvas.remove(); + } catch (error2) {} + try { + fakeVideoTrack.stop(); + } catch (error2) {} + try { + pc.close(); + } catch (error2) {} + throw error; + } + } + async getNativeSctpCapabilities() { + logger.debug('getNativeSctpCapabilities()'); + return { + numStreams: SCTP_NUM_STREAMS, + }; + } + run({ + direction, + iceParameters, + iceCandidates, + dtlsParameters, + sctpParameters, + iceServers, + iceTransportPolicy, + additionalSettings, + proprietaryConstraints, + extendedRtpCapabilities, + }) { + logger.debug('run()'); + this._direction = direction; + this._remoteSdp = new RemoteSdp_1.RemoteSdp({ + iceParameters, + iceCandidates, + dtlsParameters, + sctpParameters, + }); + this._sendingRtpParametersByKind = { + audio: ortc.getSendingRtpParameters('audio', extendedRtpCapabilities), + video: ortc.getSendingRtpParameters('video', extendedRtpCapabilities), + }; + this._sendingRemoteRtpParametersByKind = { + audio: ortc.getSendingRemoteRtpParameters('audio', extendedRtpCapabilities), + video: ortc.getSendingRemoteRtpParameters('video', extendedRtpCapabilities), + }; + this._pc = new RTCPeerConnection( + Object.assign( + { + iceServers: iceServers || [], + iceTransportPolicy: iceTransportPolicy || 'all', + bundlePolicy: 'max-bundle', + rtcpMuxPolicy: 'require', + }, + additionalSettings, + ), + proprietaryConstraints, + ); + // Handle RTCPeerConnection connection status. + this._pc.addEventListener('iceconnectionstatechange', () => { + switch (this._pc.iceConnectionState) { + case 'checking': + this.emit('@connectionstatechange', 'connecting'); + break; + case 'connected': + case 'completed': + this.emit('@connectionstatechange', 'connected'); + break; + case 'failed': + this.emit('@connectionstatechange', 'failed'); + break; + case 'disconnected': + this.emit('@connectionstatechange', 'disconnected'); + break; + case 'closed': + this.emit('@connectionstatechange', 'closed'); + break; + } + }); + } + // eslint-disable-next-line @typescript-eslint/no-unused-vars + async updateIceServers(iceServers) { + // NOTE: Firefox does not implement pc.setConfiguration(). + throw new errors_1.UnsupportedError('not supported'); + } + async restartIce(iceParameters) { + logger.debug('restartIce()'); + // Provide the remote SDP handler with new remote ICE parameters. + this._remoteSdp.updateIceParameters(iceParameters); + if (!this._transportReady) return; + if (this._direction === 'send') { + const offer = await this._pc.createOffer({ iceRestart: true }); + logger.debug('restartIce() | calling pc.setLocalDescription() [offer:%o]', offer); + await this._pc.setLocalDescription(offer); + const answer = { type: 'answer', sdp: this._remoteSdp.getSdp() }; + logger.debug('restartIce() | calling pc.setRemoteDescription() [answer:%o]', answer); + await this._pc.setRemoteDescription(answer); + } else { + const offer = { type: 'offer', sdp: this._remoteSdp.getSdp() }; + logger.debug('restartIce() | calling pc.setRemoteDescription() [offer:%o]', offer); + await this._pc.setRemoteDescription(offer); + const answer = await this._pc.createAnswer(); + logger.debug('restartIce() | calling pc.setLocalDescription() [answer:%o]', answer); + await this._pc.setLocalDescription(answer); + } + } + async getTransportStats() { + return this._pc.getStats(); + } + async send({ track, encodings, codecOptions, codec }) { + this._assertSendDirection(); + logger.debug('send() [kind:%s, track.id:%s]', track.kind, track.id); + if (encodings) { + encodings = utils.clone(encodings, []); + if (encodings.length > 1) { + encodings.forEach((encoding, idx) => { + encoding.rid = `r${idx}`; + }); + // Clone the encodings and reverse them because Firefox likes them + // from high to low. + encodings.reverse(); + } + } + const sendingRtpParameters = utils.clone(this._sendingRtpParametersByKind[track.kind], {}); + // This may throw. + sendingRtpParameters.codecs = ortc.reduceCodecs(sendingRtpParameters.codecs, codec); + const sendingRemoteRtpParameters = utils.clone( + this._sendingRemoteRtpParametersByKind[track.kind], + {}, + ); + // This may throw. + sendingRemoteRtpParameters.codecs = ortc.reduceCodecs(sendingRemoteRtpParameters.codecs, codec); + // NOTE: Firefox fails sometimes to properly anticipate the closed media + // section that it should use, so don't reuse closed media sections. + // https://github.com/versatica/mediasoup-client/issues/104 + // + // const mediaSectionIdx = this._remoteSdp!.getNextMediaSectionIdx(); + const transceiver = this._pc.addTransceiver(track, { + direction: 'sendonly', + streams: [this._sendStream], + }); + // NOTE: This is not spec compliants. Encodings should be given in addTransceiver + // second argument, but Firefox does not support it. + if (encodings) { + const parameters = transceiver.sender.getParameters(); + parameters.encodings = encodings; + await transceiver.sender.setParameters(parameters); + } + const offer = await this._pc.createOffer(); + let localSdpObject = sdpTransform.parse(offer.sdp); + // In Firefox use DTLS role client even if we are the "offerer" since + // Firefox does not respect ICE-Lite. + if (!this._transportReady) + await this._setupTransport({ localDtlsRole: 'client', localSdpObject }); + logger.debug('send() | calling pc.setLocalDescription() [offer:%o]', offer); + await this._pc.setLocalDescription(offer); + // We can now get the transceiver.mid. + const localId = transceiver.mid; + // Set MID. + sendingRtpParameters.mid = localId; + localSdpObject = sdpTransform.parse(this._pc.localDescription.sdp); + const offerMediaObject = localSdpObject.media[localSdpObject.media.length - 1]; + // Set RTCP CNAME. + sendingRtpParameters.rtcp.cname = sdpCommonUtils.getCname({ offerMediaObject }); + // Set RTP encodings by parsing the SDP offer if no encodings are given. + if (!encodings) { + sendingRtpParameters.encodings = sdpUnifiedPlanUtils.getRtpEncodings({ offerMediaObject }); + } + // Set RTP encodings by parsing the SDP offer and complete them with given + // one if just a single encoding has been given. + else if (encodings.length === 1) { + const newEncodings = sdpUnifiedPlanUtils.getRtpEncodings({ offerMediaObject }); + Object.assign(newEncodings[0], encodings[0]); + sendingRtpParameters.encodings = newEncodings; + } + // Otherwise if more than 1 encoding are given use them verbatim (but + // reverse them back since we reversed them above to satisfy Firefox). + else { + sendingRtpParameters.encodings = encodings.reverse(); + } + // If VP8 or H264 and there is effective simulcast, add scalabilityMode to + // each encoding. + if ( + sendingRtpParameters.encodings.length > 1 && + (sendingRtpParameters.codecs[0].mimeType.toLowerCase() === 'video/vp8' || + sendingRtpParameters.codecs[0].mimeType.toLowerCase() === 'video/h264') + ) { + for (const encoding of sendingRtpParameters.encodings) { + encoding.scalabilityMode = 'S1T3'; + } + } + this._remoteSdp.send({ + offerMediaObject, + offerRtpParameters: sendingRtpParameters, + answerRtpParameters: sendingRemoteRtpParameters, + codecOptions, + extmapAllowMixed: true, + }); + const answer = { type: 'answer', sdp: this._remoteSdp.getSdp() }; + logger.debug('send() | calling pc.setRemoteDescription() [answer:%o]', answer); + await this._pc.setRemoteDescription(answer); + // Store in the map. + this._mapMidTransceiver.set(localId, transceiver); + return { + localId, + rtpParameters: sendingRtpParameters, + rtpSender: transceiver.sender, + }; + } + async stopSending(localId) { + logger.debug('stopSending() [localId:%s]', localId); + const transceiver = this._mapMidTransceiver.get(localId); + if (!transceiver) throw new Error('associated transceiver not found'); + transceiver.sender.replaceTrack(null); + this._pc.removeTrack(transceiver.sender); + // NOTE: Cannot use closeMediaSection() due to the the note above in send() + // method. + // this._remoteSdp!.closeMediaSection(transceiver.mid); + this._remoteSdp.disableMediaSection(transceiver.mid); + const offer = await this._pc.createOffer(); + logger.debug('stopSending() | calling pc.setLocalDescription() [offer:%o]', offer); + await this._pc.setLocalDescription(offer); + const answer = { type: 'answer', sdp: this._remoteSdp.getSdp() }; + logger.debug('stopSending() | calling pc.setRemoteDescription() [answer:%o]', answer); + await this._pc.setRemoteDescription(answer); + } + async replaceTrack(localId, track) { + this._assertSendDirection(); + if (track) { + logger.debug('replaceTrack() [localId:%s, track.id:%s]', localId, track.id); + } else { + logger.debug('replaceTrack() [localId:%s, no track]', localId); + } + const transceiver = this._mapMidTransceiver.get(localId); + if (!transceiver) throw new Error('associated RTCRtpTransceiver not found'); + await transceiver.sender.replaceTrack(track); + } + async setMaxSpatialLayer(localId, spatialLayer) { + this._assertSendDirection(); + logger.debug('setMaxSpatialLayer() [localId:%s, spatialLayer:%s]', localId, spatialLayer); + const transceiver = this._mapMidTransceiver.get(localId); + if (!transceiver) throw new Error('associated transceiver not found'); + const parameters = transceiver.sender.getParameters(); + // NOTE: We require encodings given from low to high, however Firefox + // requires them in reverse order, so do magic here. + spatialLayer = parameters.encodings.length - 1 - spatialLayer; + parameters.encodings.forEach((encoding, idx) => { + if (idx >= spatialLayer) encoding.active = true; + else encoding.active = false; + }); + await transceiver.sender.setParameters(parameters); + } + async setRtpEncodingParameters(localId, params) { + this._assertSendDirection(); + logger.debug('setRtpEncodingParameters() [localId:%s, params:%o]', localId, params); + const transceiver = this._mapMidTransceiver.get(localId); + if (!transceiver) throw new Error('associated RTCRtpTransceiver not found'); + const parameters = transceiver.sender.getParameters(); + parameters.encodings.forEach((encoding, idx) => { + parameters.encodings[idx] = Object.assign(Object.assign({}, encoding), params); + }); + await transceiver.sender.setParameters(parameters); + } + async getSenderStats(localId) { + this._assertSendDirection(); + const transceiver = this._mapMidTransceiver.get(localId); + if (!transceiver) throw new Error('associated RTCRtpTransceiver not found'); + return transceiver.sender.getStats(); + } + async sendDataChannel({ ordered, maxPacketLifeTime, maxRetransmits, label, protocol }) { + this._assertSendDirection(); + const options = { + negotiated: true, + id: this._nextSendSctpStreamId, + ordered, + maxPacketLifeTime, + maxRetransmits, + protocol, + }; + logger.debug('sendDataChannel() [options:%o]', options); + const dataChannel = this._pc.createDataChannel(label, options); + // Increase next id. + this._nextSendSctpStreamId = ++this._nextSendSctpStreamId % SCTP_NUM_STREAMS.MIS; + // If this is the first DataChannel we need to create the SDP answer with + // m=application section. + if (!this._hasDataChannelMediaSection) { + const offer = await this._pc.createOffer(); + const localSdpObject = sdpTransform.parse(offer.sdp); + const offerMediaObject = localSdpObject.media.find((m) => m.type === 'application'); + if (!this._transportReady) + await this._setupTransport({ localDtlsRole: 'server', localSdpObject }); + logger.debug('sendDataChannel() | calling pc.setLocalDescription() [offer:%o]', offer); + await this._pc.setLocalDescription(offer); + this._remoteSdp.sendSctpAssociation({ offerMediaObject }); + const answer = { type: 'answer', sdp: this._remoteSdp.getSdp() }; + logger.debug('sendDataChannel() | calling pc.setRemoteDescription() [answer:%o]', answer); + await this._pc.setRemoteDescription(answer); + this._hasDataChannelMediaSection = true; + } + const sctpStreamParameters = { + streamId: options.id, + ordered: options.ordered, + maxPacketLifeTime: options.maxPacketLifeTime, + maxRetransmits: options.maxRetransmits, + }; + return { dataChannel, sctpStreamParameters }; + } + async receive({ trackId, kind, rtpParameters }) { + this._assertRecvDirection(); + logger.debug('receive() [trackId:%s, kind:%s]', trackId, kind); + const localId = rtpParameters.mid || String(this._mapMidTransceiver.size); + this._remoteSdp.receive({ + mid: localId, + kind, + offerRtpParameters: rtpParameters, + streamId: rtpParameters.rtcp.cname, + trackId, + }); + const offer = { type: 'offer', sdp: this._remoteSdp.getSdp() }; + logger.debug('receive() | calling pc.setRemoteDescription() [offer:%o]', offer); + await this._pc.setRemoteDescription(offer); + let answer = await this._pc.createAnswer(); + const localSdpObject = sdpTransform.parse(answer.sdp); + const answerMediaObject = localSdpObject.media.find((m) => String(m.mid) === localId); + // May need to modify codec parameters in the answer based on codec + // parameters in the offer. + sdpCommonUtils.applyCodecParameters({ + offerRtpParameters: rtpParameters, + answerMediaObject, + }); + answer = { type: 'answer', sdp: sdpTransform.write(localSdpObject) }; + if (!this._transportReady) + await this._setupTransport({ localDtlsRole: 'client', localSdpObject }); + logger.debug('receive() | calling pc.setLocalDescription() [answer:%o]', answer); + await this._pc.setLocalDescription(answer); + const transceiver = this._pc.getTransceivers().find((t) => t.mid === localId); + if (!transceiver) throw new Error('new RTCRtpTransceiver not found'); + // Store in the map. + this._mapMidTransceiver.set(localId, transceiver); + return { + localId, + track: transceiver.receiver.track, + rtpReceiver: transceiver.receiver, + }; + } + async stopReceiving(localId) { + this._assertRecvDirection(); + logger.debug('stopReceiving() [localId:%s]', localId); + const transceiver = this._mapMidTransceiver.get(localId); + if (!transceiver) throw new Error('associated RTCRtpTransceiver not found'); + this._remoteSdp.closeMediaSection(transceiver.mid); + const offer = { type: 'offer', sdp: this._remoteSdp.getSdp() }; + logger.debug('stopReceiving() | calling pc.setRemoteDescription() [offer:%o]', offer); + await this._pc.setRemoteDescription(offer); + const answer = await this._pc.createAnswer(); + logger.debug('stopReceiving() | calling pc.setLocalDescription() [answer:%o]', answer); + await this._pc.setLocalDescription(answer); + } + async getReceiverStats(localId) { + this._assertRecvDirection(); + const transceiver = this._mapMidTransceiver.get(localId); + if (!transceiver) throw new Error('associated RTCRtpTransceiver not found'); + return transceiver.receiver.getStats(); + } + async receiveDataChannel({ sctpStreamParameters, label, protocol }) { + this._assertRecvDirection(); + const { streamId, ordered, maxPacketLifeTime, maxRetransmits } = sctpStreamParameters; + const options = { + negotiated: true, + id: streamId, + ordered, + maxPacketLifeTime, + maxRetransmits, + protocol, + }; + logger.debug('receiveDataChannel() [options:%o]', options); + const dataChannel = this._pc.createDataChannel(label, options); + // If this is the first DataChannel we need to create the SDP offer with + // m=application section. + if (!this._hasDataChannelMediaSection) { + this._remoteSdp.receiveSctpAssociation(); + const offer = { type: 'offer', sdp: this._remoteSdp.getSdp() }; + logger.debug('receiveDataChannel() | calling pc.setRemoteDescription() [offer:%o]', offer); + await this._pc.setRemoteDescription(offer); + const answer = await this._pc.createAnswer(); + if (!this._transportReady) { + const localSdpObject = sdpTransform.parse(answer.sdp); + await this._setupTransport({ localDtlsRole: 'client', localSdpObject }); + } + logger.debug( + 'receiveDataChannel() | calling pc.setRemoteDescription() [answer:%o]', + answer, + ); + await this._pc.setLocalDescription(answer); + this._hasDataChannelMediaSection = true; + } + return { dataChannel }; + } + async _setupTransport({ localDtlsRole, localSdpObject }) { + if (!localSdpObject) localSdpObject = sdpTransform.parse(this._pc.localDescription.sdp); + // Get our local DTLS parameters. + const dtlsParameters = sdpCommonUtils.extractDtlsParameters({ sdpObject: localSdpObject }); + // Set our DTLS role. + dtlsParameters.role = localDtlsRole; + // Update the remote DTLS role in the SDP. + this._remoteSdp.updateDtlsRole(localDtlsRole === 'client' ? 'server' : 'client'); + // Need to tell the remote transport about our parameters. + await this.safeEmitAsPromise('@connect', { dtlsParameters }); + this._transportReady = true; + } + _assertSendDirection() { + if (this._direction !== 'send') { + throw new Error('method can just be called for handlers with "send" direction'); + } + } + _assertRecvDirection() { + if (this._direction !== 'recv') { + throw new Error('method can just be called for handlers with "recv" direction'); + } + } + } + exports.Firefox60 = Firefox60; + }, + { + '../Logger': 13, + '../errors': 18, + '../ortc': 36, + '../utils': 39, + './HandlerInterface': 25, + './sdp/RemoteSdp': 31, + './sdp/commonUtils': 32, + './sdp/unifiedPlanUtils': 34, + 'sdp-transform': 44, + }, + ], + 25: [ + function (require, module, exports) { + 'use strict'; + Object.defineProperty(exports, '__esModule', { value: true }); + exports.HandlerInterface = void 0; + const EnhancedEventEmitter_1 = require('../EnhancedEventEmitter'); + class HandlerInterface extends EnhancedEventEmitter_1.EnhancedEventEmitter { + /** + * @emits @connect - ( + * { dtlsParameters: DtlsParameters }, + * callback: Function, + * errback: Function + * ) + * @emits @connectionstatechange - (connectionState: ConnectionState) + */ + constructor() { + super(); + } + } + exports.HandlerInterface = HandlerInterface; + }, + { '../EnhancedEventEmitter': 12 }, + ], + 26: [ + function (require, module, exports) { + 'use strict'; + var __createBinding = + (this && this.__createBinding) || + (Object.create + ? function (o, m, k, k2) { + if (k2 === undefined) k2 = k; + Object.defineProperty(o, k2, { + enumerable: true, + get: function () { + return m[k]; + }, + }); + } + : function (o, m, k, k2) { + if (k2 === undefined) k2 = k; + o[k2] = m[k]; + }); + var __setModuleDefault = + (this && this.__setModuleDefault) || + (Object.create + ? function (o, v) { + Object.defineProperty(o, 'default', { enumerable: true, value: v }); + } + : function (o, v) { + o['default'] = v; + }); + var __importStar = + (this && this.__importStar) || + function (mod) { + if (mod && mod.__esModule) return mod; + var result = {}; + if (mod != null) + for (var k in mod) + if (k !== 'default' && Object.hasOwnProperty.call(mod, k)) + __createBinding(result, mod, k); + __setModuleDefault(result, mod); + return result; + }; + Object.defineProperty(exports, '__esModule', { value: true }); + exports.ReactNative = void 0; + const sdpTransform = __importStar(require('sdp-transform')); + const Logger_1 = require('../Logger'); + const errors_1 = require('../errors'); + const utils = __importStar(require('../utils')); + const ortc = __importStar(require('../ortc')); + const sdpCommonUtils = __importStar(require('./sdp/commonUtils')); + const sdpPlanBUtils = __importStar(require('./sdp/planBUtils')); + const HandlerInterface_1 = require('./HandlerInterface'); + const RemoteSdp_1 = require('./sdp/RemoteSdp'); + const logger = new Logger_1.Logger('ReactNative'); + const SCTP_NUM_STREAMS = { OS: 1024, MIS: 1024 }; + class ReactNative extends HandlerInterface_1.HandlerInterface { + constructor() { + super(); + // Local stream for sending. + this._sendStream = new MediaStream(); + // Map of sending MediaStreamTracks indexed by localId. + this._mapSendLocalIdTrack = new Map(); + // Next sending localId. + this._nextSendLocalId = 0; + // Map of MID, RTP parameters and RTCRtpReceiver indexed by local id. + // Value is an Object with mid, rtpParameters and rtpReceiver. + this._mapRecvLocalIdInfo = new Map(); + // Whether a DataChannel m=application section has been created. + this._hasDataChannelMediaSection = false; + // Sending DataChannel id value counter. Incremented for each new DataChannel. + this._nextSendSctpStreamId = 0; + // Got transport local and remote parameters. + this._transportReady = false; + } + /** + * Creates a factory function. + */ + static createFactory() { + return () => new ReactNative(); + } + get name() { + return 'ReactNative'; + } + close() { + logger.debug('close()'); + // Free/dispose native stream and tracks. + // @ts-ignore (proprietary API in react-native-webrtc). + this._sendStream.release(); + // Close RTCPeerConnection. + if (this._pc) { + try { + this._pc.close(); + } catch (error) {} + } + } + async getNativeRtpCapabilities() { + logger.debug('getNativeRtpCapabilities()'); + const pc = new RTCPeerConnection({ + iceServers: [], + iceTransportPolicy: 'all', + bundlePolicy: 'max-bundle', + rtcpMuxPolicy: 'require', + sdpSemantics: 'plan-b', + }); + try { + const offer = await pc.createOffer({ + offerToReceiveAudio: true, + offerToReceiveVideo: true, + }); + try { + pc.close(); + } catch (error) {} + const sdpObject = sdpTransform.parse(offer.sdp); + const nativeRtpCapabilities = sdpCommonUtils.extractRtpCapabilities({ sdpObject }); + return nativeRtpCapabilities; + } catch (error) { + try { + pc.close(); + } catch (error2) {} + throw error; + } + } + async getNativeSctpCapabilities() { + logger.debug('getNativeSctpCapabilities()'); + return { + numStreams: SCTP_NUM_STREAMS, + }; + } + run({ + direction, + iceParameters, + iceCandidates, + dtlsParameters, + sctpParameters, + iceServers, + iceTransportPolicy, + additionalSettings, + proprietaryConstraints, + extendedRtpCapabilities, + }) { + logger.debug('run()'); + this._direction = direction; + this._remoteSdp = new RemoteSdp_1.RemoteSdp({ + iceParameters, + iceCandidates, + dtlsParameters, + sctpParameters, + planB: true, + }); + this._sendingRtpParametersByKind = { + audio: ortc.getSendingRtpParameters('audio', extendedRtpCapabilities), + video: ortc.getSendingRtpParameters('video', extendedRtpCapabilities), + }; + this._sendingRemoteRtpParametersByKind = { + audio: ortc.getSendingRemoteRtpParameters('audio', extendedRtpCapabilities), + video: ortc.getSendingRemoteRtpParameters('video', extendedRtpCapabilities), + }; + this._pc = new RTCPeerConnection( + Object.assign( + { + iceServers: iceServers || [], + iceTransportPolicy: iceTransportPolicy || 'all', + bundlePolicy: 'max-bundle', + rtcpMuxPolicy: 'require', + sdpSemantics: 'plan-b', + }, + additionalSettings, + ), + proprietaryConstraints, + ); + // Handle RTCPeerConnection connection status. + this._pc.addEventListener('iceconnectionstatechange', () => { + switch (this._pc.iceConnectionState) { + case 'checking': + this.emit('@connectionstatechange', 'connecting'); + break; + case 'connected': + case 'completed': + this.emit('@connectionstatechange', 'connected'); + break; + case 'failed': + this.emit('@connectionstatechange', 'failed'); + break; + case 'disconnected': + this.emit('@connectionstatechange', 'disconnected'); + break; + case 'closed': + this.emit('@connectionstatechange', 'closed'); + break; + } + }); + } + async updateIceServers(iceServers) { + logger.debug('updateIceServers()'); + const configuration = this._pc.getConfiguration(); + configuration.iceServers = iceServers; + this._pc.setConfiguration(configuration); + } + async restartIce(iceParameters) { + logger.debug('restartIce()'); + // Provide the remote SDP handler with new remote ICE parameters. + this._remoteSdp.updateIceParameters(iceParameters); + if (!this._transportReady) return; + if (this._direction === 'send') { + const offer = await this._pc.createOffer({ iceRestart: true }); + logger.debug('restartIce() | calling pc.setLocalDescription() [offer:%o]', offer); + await this._pc.setLocalDescription(offer); + const answer = { type: 'answer', sdp: this._remoteSdp.getSdp() }; + logger.debug('restartIce() | calling pc.setRemoteDescription() [answer:%o]', answer); + await this._pc.setRemoteDescription(answer); + } else { + const offer = { type: 'offer', sdp: this._remoteSdp.getSdp() }; + logger.debug('restartIce() | calling pc.setRemoteDescription() [offer:%o]', offer); + await this._pc.setRemoteDescription(offer); + const answer = await this._pc.createAnswer(); + logger.debug('restartIce() | calling pc.setLocalDescription() [answer:%o]', answer); + await this._pc.setLocalDescription(answer); + } + } + async getTransportStats() { + return this._pc.getStats(); + } + async send({ track, encodings, codecOptions, codec }) { + this._assertSendDirection(); + logger.debug('send() [kind:%s, track.id:%s]', track.kind, track.id); + if (codec) { + logger.warn('send() | codec selection is not available in %s handler', this.name); + } + this._sendStream.addTrack(track); + this._pc.addStream(this._sendStream); + let offer = await this._pc.createOffer(); + let localSdpObject = sdpTransform.parse(offer.sdp); + let offerMediaObject; + const sendingRtpParameters = utils.clone(this._sendingRtpParametersByKind[track.kind], {}); + sendingRtpParameters.codecs = ortc.reduceCodecs(sendingRtpParameters.codecs); + const sendingRemoteRtpParameters = utils.clone( + this._sendingRemoteRtpParametersByKind[track.kind], + {}, + ); + sendingRemoteRtpParameters.codecs = ortc.reduceCodecs(sendingRemoteRtpParameters.codecs); + if (!this._transportReady) + await this._setupTransport({ localDtlsRole: 'server', localSdpObject }); + if (track.kind === 'video' && encodings && encodings.length > 1) { + logger.debug('send() | enabling simulcast'); + localSdpObject = sdpTransform.parse(offer.sdp); + offerMediaObject = localSdpObject.media.find((m) => m.type === 'video'); + sdpPlanBUtils.addLegacySimulcast({ + offerMediaObject, + track, + numStreams: encodings.length, + }); + offer = { type: 'offer', sdp: sdpTransform.write(localSdpObject) }; + } + logger.debug('send() | calling pc.setLocalDescription() [offer:%o]', offer); + await this._pc.setLocalDescription(offer); + localSdpObject = sdpTransform.parse(this._pc.localDescription.sdp); + offerMediaObject = localSdpObject.media.find((m) => m.type === track.kind); + // Set RTCP CNAME. + sendingRtpParameters.rtcp.cname = sdpCommonUtils.getCname({ offerMediaObject }); + // Set RTP encodings. + sendingRtpParameters.encodings = sdpPlanBUtils.getRtpEncodings({ offerMediaObject, track }); + // Complete encodings with given values. + if (encodings) { + for (let idx = 0; idx < sendingRtpParameters.encodings.length; ++idx) { + if (encodings[idx]) Object.assign(sendingRtpParameters.encodings[idx], encodings[idx]); + } + } + // If VP8 or H264 and there is effective simulcast, add scalabilityMode to + // each encoding. + if ( + sendingRtpParameters.encodings.length > 1 && + (sendingRtpParameters.codecs[0].mimeType.toLowerCase() === 'video/vp8' || + sendingRtpParameters.codecs[0].mimeType.toLowerCase() === 'video/h264') + ) { + for (const encoding of sendingRtpParameters.encodings) { + encoding.scalabilityMode = 'S1T3'; + } + } + this._remoteSdp.send({ + offerMediaObject, + offerRtpParameters: sendingRtpParameters, + answerRtpParameters: sendingRemoteRtpParameters, + codecOptions, + }); + const answer = { type: 'answer', sdp: this._remoteSdp.getSdp() }; + logger.debug('send() | calling pc.setRemoteDescription() [answer:%o]', answer); + await this._pc.setRemoteDescription(answer); + const localId = String(this._nextSendLocalId); + this._nextSendLocalId++; + // Insert into the map. + this._mapSendLocalIdTrack.set(localId, track); + return { + localId: localId, + rtpParameters: sendingRtpParameters, + }; + } + async stopSending(localId) { + this._assertSendDirection(); + logger.debug('stopSending() [localId:%s]', localId); + const track = this._mapSendLocalIdTrack.get(localId); + if (!track) throw new Error('track not found'); + this._mapSendLocalIdTrack.delete(localId); + this._sendStream.removeTrack(track); + this._pc.addStream(this._sendStream); + const offer = await this._pc.createOffer(); + logger.debug('stopSending() | calling pc.setLocalDescription() [offer:%o]', offer); + try { + await this._pc.setLocalDescription(offer); + } catch (error) { + // NOTE: If there are no sending tracks, setLocalDescription() will fail with + // "Failed to create channels". If so, ignore it. + if (this._sendStream.getTracks().length === 0) { + logger.warn( + 'stopSending() | ignoring expected error due no sending tracks: %s', + error.toString(), + ); + return; + } + throw error; + } + if (this._pc.signalingState === 'stable') return; + const answer = { type: 'answer', sdp: this._remoteSdp.getSdp() }; + logger.debug('stopSending() | calling pc.setRemoteDescription() [answer:%o]', answer); + await this._pc.setRemoteDescription(answer); + } + async replaceTrack( + // eslint-disable-next-line @typescript-eslint/no-unused-vars + localId, + track, + ) { + throw new errors_1.UnsupportedError('not implemented'); + } + // eslint-disable-next-line @typescript-eslint/no-unused-vars + async setMaxSpatialLayer(localId, spatialLayer) { + throw new errors_1.UnsupportedError('not implemented'); + } + // eslint-disable-next-line @typescript-eslint/no-unused-vars + async setRtpEncodingParameters(localId, params) { + throw new errors_1.UnsupportedError('not implemented'); + } + // eslint-disable-next-line @typescript-eslint/no-unused-vars + async getSenderStats(localId) { + throw new errors_1.UnsupportedError('not implemented'); + } + async sendDataChannel({ ordered, maxPacketLifeTime, maxRetransmits, label, protocol }) { + this._assertSendDirection(); + const options = { + negotiated: true, + id: this._nextSendSctpStreamId, + ordered, + maxPacketLifeTime, + maxRetransmitTime: maxPacketLifeTime, + maxRetransmits, + protocol, + }; + logger.debug('sendDataChannel() [options:%o]', options); + const dataChannel = this._pc.createDataChannel(label, options); + // Increase next id. + this._nextSendSctpStreamId = ++this._nextSendSctpStreamId % SCTP_NUM_STREAMS.MIS; + // If this is the first DataChannel we need to create the SDP answer with + // m=application section. + if (!this._hasDataChannelMediaSection) { + const offer = await this._pc.createOffer(); + const localSdpObject = sdpTransform.parse(offer.sdp); + const offerMediaObject = localSdpObject.media.find((m) => m.type === 'application'); + if (!this._transportReady) + await this._setupTransport({ localDtlsRole: 'server', localSdpObject }); + logger.debug('sendDataChannel() | calling pc.setLocalDescription() [offer:%o]', offer); + await this._pc.setLocalDescription(offer); + this._remoteSdp.sendSctpAssociation({ offerMediaObject }); + const answer = { type: 'answer', sdp: this._remoteSdp.getSdp() }; + logger.debug('sendDataChannel() | calling pc.setRemoteDescription() [answer:%o]', answer); + await this._pc.setRemoteDescription(answer); + this._hasDataChannelMediaSection = true; + } + const sctpStreamParameters = { + streamId: options.id, + ordered: options.ordered, + maxPacketLifeTime: options.maxPacketLifeTime, + maxRetransmits: options.maxRetransmits, + }; + return { dataChannel, sctpStreamParameters }; + } + async receive({ trackId, kind, rtpParameters }) { + this._assertRecvDirection(); + logger.debug('receive() [trackId:%s, kind:%s]', trackId, kind); + const localId = trackId; + const mid = kind; + let streamId = rtpParameters.rtcp.cname; + // NOTE: In React-Native we cannot reuse the same remote MediaStream for new + // remote tracks. This is because react-native-webrtc does not react on new + // tracks generated within already existing streams, so force the streamId + // to be different. + logger.debug( + 'receive() | forcing a random remote streamId to avoid well known bug in react-native-webrtc', + ); + streamId += `-hack-${utils.generateRandomNumber()}`; + this._remoteSdp.receive({ + mid, + kind, + offerRtpParameters: rtpParameters, + streamId, + trackId, + }); + const offer = { type: 'offer', sdp: this._remoteSdp.getSdp() }; + logger.debug('receive() | calling pc.setRemoteDescription() [offer:%o]', offer); + await this._pc.setRemoteDescription(offer); + let answer = await this._pc.createAnswer(); + const localSdpObject = sdpTransform.parse(answer.sdp); + const answerMediaObject = localSdpObject.media.find((m) => String(m.mid) === mid); + // May need to modify codec parameters in the answer based on codec + // parameters in the offer. + sdpCommonUtils.applyCodecParameters({ + offerRtpParameters: rtpParameters, + answerMediaObject, + }); + answer = { type: 'answer', sdp: sdpTransform.write(localSdpObject) }; + if (!this._transportReady) + await this._setupTransport({ localDtlsRole: 'client', localSdpObject }); + logger.debug('receive() | calling pc.setLocalDescription() [answer:%o]', answer); + await this._pc.setLocalDescription(answer); + const stream = this._pc.getRemoteStreams().find((s) => s.id === streamId); + const track = stream.getTrackById(localId); + if (!track) throw new Error('remote track not found'); + // Insert into the map. + this._mapRecvLocalIdInfo.set(localId, { mid, rtpParameters }); + return { localId, track }; + } + async stopReceiving(localId) { + this._assertRecvDirection(); + logger.debug('stopReceiving() [localId:%s]', localId); + const { mid, rtpParameters } = this._mapRecvLocalIdInfo.get(localId) || {}; + // Remove from the map. + this._mapRecvLocalIdInfo.delete(localId); + this._remoteSdp.planBStopReceiving({ mid: mid, offerRtpParameters: rtpParameters }); + const offer = { type: 'offer', sdp: this._remoteSdp.getSdp() }; + logger.debug('stopReceiving() | calling pc.setRemoteDescription() [offer:%o]', offer); + await this._pc.setRemoteDescription(offer); + const answer = await this._pc.createAnswer(); + logger.debug('stopReceiving() | calling pc.setLocalDescription() [answer:%o]', answer); + await this._pc.setLocalDescription(answer); + } + // eslint-disable-next-line @typescript-eslint/no-unused-vars + async getReceiverStats(localId) { + throw new errors_1.UnsupportedError('not implemented'); + } + async receiveDataChannel({ sctpStreamParameters, label, protocol }) { + this._assertRecvDirection(); + const { streamId, ordered, maxPacketLifeTime, maxRetransmits } = sctpStreamParameters; + const options = { + negotiated: true, + id: streamId, + ordered, + maxPacketLifeTime, + maxRetransmitTime: maxPacketLifeTime, + maxRetransmits, + protocol, + }; + logger.debug('receiveDataChannel() [options:%o]', options); + const dataChannel = this._pc.createDataChannel(label, options); + // If this is the first DataChannel we need to create the SDP offer with + // m=application section. + if (!this._hasDataChannelMediaSection) { + this._remoteSdp.receiveSctpAssociation({ oldDataChannelSpec: true }); + const offer = { type: 'offer', sdp: this._remoteSdp.getSdp() }; + logger.debug('receiveDataChannel() | calling pc.setRemoteDescription() [offer:%o]', offer); + await this._pc.setRemoteDescription(offer); + const answer = await this._pc.createAnswer(); + if (!this._transportReady) { + const localSdpObject = sdpTransform.parse(answer.sdp); + await this._setupTransport({ localDtlsRole: 'client', localSdpObject }); + } + logger.debug( + 'receiveDataChannel() | calling pc.setRemoteDescription() [answer:%o]', + answer, + ); + await this._pc.setLocalDescription(answer); + this._hasDataChannelMediaSection = true; + } + return { dataChannel }; + } + async _setupTransport({ localDtlsRole, localSdpObject }) { + if (!localSdpObject) localSdpObject = sdpTransform.parse(this._pc.localDescription.sdp); + // Get our local DTLS parameters. + const dtlsParameters = sdpCommonUtils.extractDtlsParameters({ sdpObject: localSdpObject }); + // Set our DTLS role. + dtlsParameters.role = localDtlsRole; + // Update the remote DTLS role in the SDP. + this._remoteSdp.updateDtlsRole(localDtlsRole === 'client' ? 'server' : 'client'); + // Need to tell the remote transport about our parameters. + await this.safeEmitAsPromise('@connect', { dtlsParameters }); + this._transportReady = true; + } + _assertSendDirection() { + if (this._direction !== 'send') { + throw new Error('method can just be called for handlers with "send" direction'); + } + } + _assertRecvDirection() { + if (this._direction !== 'recv') { + throw new Error('method can just be called for handlers with "recv" direction'); + } + } + } + exports.ReactNative = ReactNative; + }, + { + '../Logger': 13, + '../errors': 18, + '../ortc': 36, + '../utils': 39, + './HandlerInterface': 25, + './sdp/RemoteSdp': 31, + './sdp/commonUtils': 32, + './sdp/planBUtils': 33, + 'sdp-transform': 44, + }, + ], + 27: [ + function (require, module, exports) { + 'use strict'; + var __createBinding = + (this && this.__createBinding) || + (Object.create + ? function (o, m, k, k2) { + if (k2 === undefined) k2 = k; + Object.defineProperty(o, k2, { + enumerable: true, + get: function () { + return m[k]; + }, + }); + } + : function (o, m, k, k2) { + if (k2 === undefined) k2 = k; + o[k2] = m[k]; + }); + var __setModuleDefault = + (this && this.__setModuleDefault) || + (Object.create + ? function (o, v) { + Object.defineProperty(o, 'default', { enumerable: true, value: v }); + } + : function (o, v) { + o['default'] = v; + }); + var __importStar = + (this && this.__importStar) || + function (mod) { + if (mod && mod.__esModule) return mod; + var result = {}; + if (mod != null) + for (var k in mod) + if (k !== 'default' && Object.hasOwnProperty.call(mod, k)) + __createBinding(result, mod, k); + __setModuleDefault(result, mod); + return result; + }; + Object.defineProperty(exports, '__esModule', { value: true }); + exports.Safari11 = void 0; + const sdpTransform = __importStar(require('sdp-transform')); + const Logger_1 = require('../Logger'); + const utils = __importStar(require('../utils')); + const ortc = __importStar(require('../ortc')); + const sdpCommonUtils = __importStar(require('./sdp/commonUtils')); + const sdpPlanBUtils = __importStar(require('./sdp/planBUtils')); + const HandlerInterface_1 = require('./HandlerInterface'); + const RemoteSdp_1 = require('./sdp/RemoteSdp'); + const logger = new Logger_1.Logger('Safari11'); + const SCTP_NUM_STREAMS = { OS: 1024, MIS: 1024 }; + class Safari11 extends HandlerInterface_1.HandlerInterface { + constructor() { + super(); + // Local stream for sending. + this._sendStream = new MediaStream(); + // Map of RTCRtpSender indexed by localId. + this._mapSendLocalIdRtpSender = new Map(); + // Next sending localId. + this._nextSendLocalId = 0; + // Map of MID, RTP parameters and RTCRtpReceiver indexed by local id. + // Value is an Object with mid, rtpParameters and rtpReceiver. + this._mapRecvLocalIdInfo = new Map(); + // Whether a DataChannel m=application section has been created. + this._hasDataChannelMediaSection = false; + // Sending DataChannel id value counter. Incremented for each new DataChannel. + this._nextSendSctpStreamId = 0; + // Got transport local and remote parameters. + this._transportReady = false; + } + /** + * Creates a factory function. + */ + static createFactory() { + return () => new Safari11(); + } + get name() { + return 'Safari11'; + } + close() { + logger.debug('close()'); + // Close RTCPeerConnection. + if (this._pc) { + try { + this._pc.close(); + } catch (error) {} + } + } + async getNativeRtpCapabilities() { + logger.debug('getNativeRtpCapabilities()'); + const pc = new RTCPeerConnection({ + iceServers: [], + iceTransportPolicy: 'all', + bundlePolicy: 'max-bundle', + rtcpMuxPolicy: 'require', + sdpSemantics: 'plan-b', + }); + try { + const offer = await pc.createOffer({ + offerToReceiveAudio: true, + offerToReceiveVideo: true, + }); + try { + pc.close(); + } catch (error) {} + const sdpObject = sdpTransform.parse(offer.sdp); + const nativeRtpCapabilities = sdpCommonUtils.extractRtpCapabilities({ sdpObject }); + return nativeRtpCapabilities; + } catch (error) { + try { + pc.close(); + } catch (error2) {} + throw error; + } + } + async getNativeSctpCapabilities() { + logger.debug('getNativeSctpCapabilities()'); + return { + numStreams: SCTP_NUM_STREAMS, + }; + } + run({ + direction, + iceParameters, + iceCandidates, + dtlsParameters, + sctpParameters, + iceServers, + iceTransportPolicy, + additionalSettings, + proprietaryConstraints, + extendedRtpCapabilities, + }) { + logger.debug('run()'); + this._direction = direction; + this._remoteSdp = new RemoteSdp_1.RemoteSdp({ + iceParameters, + iceCandidates, + dtlsParameters, + sctpParameters, + planB: true, + }); + this._sendingRtpParametersByKind = { + audio: ortc.getSendingRtpParameters('audio', extendedRtpCapabilities), + video: ortc.getSendingRtpParameters('video', extendedRtpCapabilities), + }; + this._sendingRemoteRtpParametersByKind = { + audio: ortc.getSendingRemoteRtpParameters('audio', extendedRtpCapabilities), + video: ortc.getSendingRemoteRtpParameters('video', extendedRtpCapabilities), + }; + this._pc = new RTCPeerConnection( + Object.assign( + { + iceServers: iceServers || [], + iceTransportPolicy: iceTransportPolicy || 'all', + bundlePolicy: 'max-bundle', + rtcpMuxPolicy: 'require', + }, + additionalSettings, + ), + proprietaryConstraints, + ); + // Handle RTCPeerConnection connection status. + this._pc.addEventListener('iceconnectionstatechange', () => { + switch (this._pc.iceConnectionState) { + case 'checking': + this.emit('@connectionstatechange', 'connecting'); + break; + case 'connected': + case 'completed': + this.emit('@connectionstatechange', 'connected'); + break; + case 'failed': + this.emit('@connectionstatechange', 'failed'); + break; + case 'disconnected': + this.emit('@connectionstatechange', 'disconnected'); + break; + case 'closed': + this.emit('@connectionstatechange', 'closed'); + break; + } + }); + } + async updateIceServers(iceServers) { + logger.debug('updateIceServers()'); + const configuration = this._pc.getConfiguration(); + configuration.iceServers = iceServers; + this._pc.setConfiguration(configuration); + } + async restartIce(iceParameters) { + logger.debug('restartIce()'); + // Provide the remote SDP handler with new remote ICE parameters. + this._remoteSdp.updateIceParameters(iceParameters); + if (!this._transportReady) return; + if (this._direction === 'send') { + const offer = await this._pc.createOffer({ iceRestart: true }); + logger.debug('restartIce() | calling pc.setLocalDescription() [offer:%o]', offer); + await this._pc.setLocalDescription(offer); + const answer = { type: 'answer', sdp: this._remoteSdp.getSdp() }; + logger.debug('restartIce() | calling pc.setRemoteDescription() [answer:%o]', answer); + await this._pc.setRemoteDescription(answer); + } else { + const offer = { type: 'offer', sdp: this._remoteSdp.getSdp() }; + logger.debug('restartIce() | calling pc.setRemoteDescription() [offer:%o]', offer); + await this._pc.setRemoteDescription(offer); + const answer = await this._pc.createAnswer(); + logger.debug('restartIce() | calling pc.setLocalDescription() [answer:%o]', answer); + await this._pc.setLocalDescription(answer); + } + } + async getTransportStats() { + return this._pc.getStats(); + } + async send({ track, encodings, codecOptions, codec }) { + this._assertSendDirection(); + logger.debug('send() [kind:%s, track.id:%s]', track.kind, track.id); + if (codec) { + logger.warn('send() | codec selection is not available in %s handler', this.name); + } + this._sendStream.addTrack(track); + this._pc.addTrack(track, this._sendStream); + let offer = await this._pc.createOffer(); + let localSdpObject = sdpTransform.parse(offer.sdp); + let offerMediaObject; + const sendingRtpParameters = utils.clone(this._sendingRtpParametersByKind[track.kind], {}); + sendingRtpParameters.codecs = ortc.reduceCodecs(sendingRtpParameters.codecs); + const sendingRemoteRtpParameters = utils.clone( + this._sendingRemoteRtpParametersByKind[track.kind], + {}, + ); + sendingRemoteRtpParameters.codecs = ortc.reduceCodecs(sendingRemoteRtpParameters.codecs); + if (!this._transportReady) + await this._setupTransport({ localDtlsRole: 'server', localSdpObject }); + if (track.kind === 'video' && encodings && encodings.length > 1) { + logger.debug('send() | enabling simulcast'); + localSdpObject = sdpTransform.parse(offer.sdp); + offerMediaObject = localSdpObject.media.find((m) => m.type === 'video'); + sdpPlanBUtils.addLegacySimulcast({ + offerMediaObject, + track, + numStreams: encodings.length, + }); + offer = { type: 'offer', sdp: sdpTransform.write(localSdpObject) }; + } + logger.debug('send() | calling pc.setLocalDescription() [offer:%o]', offer); + await this._pc.setLocalDescription(offer); + localSdpObject = sdpTransform.parse(this._pc.localDescription.sdp); + offerMediaObject = localSdpObject.media.find((m) => m.type === track.kind); + // Set RTCP CNAME. + sendingRtpParameters.rtcp.cname = sdpCommonUtils.getCname({ offerMediaObject }); + // Set RTP encodings. + sendingRtpParameters.encodings = sdpPlanBUtils.getRtpEncodings({ offerMediaObject, track }); + // Complete encodings with given values. + if (encodings) { + for (let idx = 0; idx < sendingRtpParameters.encodings.length; ++idx) { + if (encodings[idx]) Object.assign(sendingRtpParameters.encodings[idx], encodings[idx]); + } + } + // If VP8 and there is effective simulcast, add scalabilityMode to each + // encoding. + if ( + sendingRtpParameters.encodings.length > 1 && + sendingRtpParameters.codecs[0].mimeType.toLowerCase() === 'video/vp8' + ) { + for (const encoding of sendingRtpParameters.encodings) { + encoding.scalabilityMode = 'S1T3'; + } + } + this._remoteSdp.send({ + offerMediaObject, + offerRtpParameters: sendingRtpParameters, + answerRtpParameters: sendingRemoteRtpParameters, + codecOptions, + }); + const answer = { type: 'answer', sdp: this._remoteSdp.getSdp() }; + logger.debug('send() | calling pc.setRemoteDescription() [answer:%o]', answer); + await this._pc.setRemoteDescription(answer); + const localId = String(this._nextSendLocalId); + this._nextSendLocalId++; + const rtpSender = this._pc.getSenders().find((s) => s.track === track); + // Insert into the map. + this._mapSendLocalIdRtpSender.set(localId, rtpSender); + return { + localId: localId, + rtpParameters: sendingRtpParameters, + rtpSender, + }; + } + async stopSending(localId) { + this._assertSendDirection(); + const rtpSender = this._mapSendLocalIdRtpSender.get(localId); + if (!rtpSender) throw new Error('associated RTCRtpSender not found'); + if (rtpSender.track) this._sendStream.removeTrack(rtpSender.track); + this._mapSendLocalIdRtpSender.delete(localId); + const offer = await this._pc.createOffer(); + logger.debug('stopSending() | calling pc.setLocalDescription() [offer:%o]', offer); + try { + await this._pc.setLocalDescription(offer); + } catch (error) { + // NOTE: If there are no sending tracks, setLocalDescription() will fail with + // "Failed to create channels". If so, ignore it. + if (this._sendStream.getTracks().length === 0) { + logger.warn( + 'stopSending() | ignoring expected error due no sending tracks: %s', + error.toString(), + ); + return; + } + throw error; + } + if (this._pc.signalingState === 'stable') return; + const answer = { type: 'answer', sdp: this._remoteSdp.getSdp() }; + logger.debug('stopSending() | calling pc.setRemoteDescription() [answer:%o]', answer); + await this._pc.setRemoteDescription(answer); + } + async replaceTrack(localId, track) { + this._assertSendDirection(); + if (track) { + logger.debug('replaceTrack() [localId:%s, track.id:%s]', localId, track.id); + } else { + logger.debug('replaceTrack() [localId:%s, no track]', localId); + } + const rtpSender = this._mapSendLocalIdRtpSender.get(localId); + if (!rtpSender) throw new Error('associated RTCRtpSender not found'); + const oldTrack = rtpSender.track; + await rtpSender.replaceTrack(track); + // Remove the old track from the local stream. + if (oldTrack) this._sendStream.removeTrack(oldTrack); + // Add the new track to the local stream. + if (track) this._sendStream.addTrack(track); + } + async setMaxSpatialLayer(localId, spatialLayer) { + this._assertSendDirection(); + logger.debug('setMaxSpatialLayer() [localId:%s, spatialLayer:%s]', localId, spatialLayer); + const rtpSender = this._mapSendLocalIdRtpSender.get(localId); + if (!rtpSender) throw new Error('associated RTCRtpSender not found'); + const parameters = rtpSender.getParameters(); + parameters.encodings.forEach((encoding, idx) => { + if (idx <= spatialLayer) encoding.active = true; + else encoding.active = false; + }); + await rtpSender.setParameters(parameters); + } + async setRtpEncodingParameters(localId, params) { + this._assertSendDirection(); + logger.debug('setRtpEncodingParameters() [localId:%s, params:%o]', localId, params); + const rtpSender = this._mapSendLocalIdRtpSender.get(localId); + if (!rtpSender) throw new Error('associated RTCRtpSender not found'); + const parameters = rtpSender.getParameters(); + parameters.encodings.forEach((encoding, idx) => { + parameters.encodings[idx] = Object.assign(Object.assign({}, encoding), params); + }); + await rtpSender.setParameters(parameters); + } + async getSenderStats(localId) { + this._assertSendDirection(); + const rtpSender = this._mapSendLocalIdRtpSender.get(localId); + if (!rtpSender) throw new Error('associated RTCRtpSender not found'); + return rtpSender.getStats(); + } + async sendDataChannel({ ordered, maxPacketLifeTime, maxRetransmits, label, protocol }) { + this._assertSendDirection(); + const options = { + negotiated: true, + id: this._nextSendSctpStreamId, + ordered, + maxPacketLifeTime, + maxRetransmits, + protocol, + }; + logger.debug('sendDataChannel() [options:%o]', options); + const dataChannel = this._pc.createDataChannel(label, options); + // Increase next id. + this._nextSendSctpStreamId = ++this._nextSendSctpStreamId % SCTP_NUM_STREAMS.MIS; + // If this is the first DataChannel we need to create the SDP answer with + // m=application section. + if (!this._hasDataChannelMediaSection) { + const offer = await this._pc.createOffer(); + const localSdpObject = sdpTransform.parse(offer.sdp); + const offerMediaObject = localSdpObject.media.find((m) => m.type === 'application'); + if (!this._transportReady) + await this._setupTransport({ localDtlsRole: 'server', localSdpObject }); + logger.debug('sendDataChannel() | calling pc.setLocalDescription() [offer:%o]', offer); + await this._pc.setLocalDescription(offer); + this._remoteSdp.sendSctpAssociation({ offerMediaObject }); + const answer = { type: 'answer', sdp: this._remoteSdp.getSdp() }; + logger.debug('sendDataChannel() | calling pc.setRemoteDescription() [answer:%o]', answer); + await this._pc.setRemoteDescription(answer); + this._hasDataChannelMediaSection = true; + } + const sctpStreamParameters = { + streamId: options.id, + ordered: options.ordered, + maxPacketLifeTime: options.maxPacketLifeTime, + maxRetransmits: options.maxRetransmits, + }; + return { dataChannel, sctpStreamParameters }; + } + async receive({ trackId, kind, rtpParameters }) { + this._assertRecvDirection(); + logger.debug('receive() [trackId:%s, kind:%s]', trackId, kind); + const localId = trackId; + const mid = kind; + this._remoteSdp.receive({ + mid, + kind, + offerRtpParameters: rtpParameters, + streamId: rtpParameters.rtcp.cname, + trackId, + }); + const offer = { type: 'offer', sdp: this._remoteSdp.getSdp() }; + logger.debug('receive() | calling pc.setRemoteDescription() [offer:%o]', offer); + await this._pc.setRemoteDescription(offer); + let answer = await this._pc.createAnswer(); + const localSdpObject = sdpTransform.parse(answer.sdp); + const answerMediaObject = localSdpObject.media.find((m) => String(m.mid) === mid); + // May need to modify codec parameters in the answer based on codec + // parameters in the offer. + sdpCommonUtils.applyCodecParameters({ + offerRtpParameters: rtpParameters, + answerMediaObject, + }); + answer = { type: 'answer', sdp: sdpTransform.write(localSdpObject) }; + if (!this._transportReady) + await this._setupTransport({ localDtlsRole: 'client', localSdpObject }); + logger.debug('receive() | calling pc.setLocalDescription() [answer:%o]', answer); + await this._pc.setLocalDescription(answer); + const rtpReceiver = this._pc.getReceivers().find((r) => r.track && r.track.id === localId); + if (!rtpReceiver) throw new Error('new RTCRtpReceiver not'); + // Insert into the map. + this._mapRecvLocalIdInfo.set(localId, { mid, rtpParameters, rtpReceiver }); + return { + localId, + track: rtpReceiver.track, + rtpReceiver, + }; + } + async stopReceiving(localId) { + this._assertRecvDirection(); + logger.debug('stopReceiving() [localId:%s]', localId); + const { mid, rtpParameters } = this._mapRecvLocalIdInfo.get(localId) || {}; + // Remove from the map. + this._mapRecvLocalIdInfo.delete(localId); + this._remoteSdp.planBStopReceiving({ mid: mid, offerRtpParameters: rtpParameters }); + const offer = { type: 'offer', sdp: this._remoteSdp.getSdp() }; + logger.debug('stopReceiving() | calling pc.setRemoteDescription() [offer:%o]', offer); + await this._pc.setRemoteDescription(offer); + const answer = await this._pc.createAnswer(); + logger.debug('stopReceiving() | calling pc.setLocalDescription() [answer:%o]', answer); + await this._pc.setLocalDescription(answer); + } + async getReceiverStats(localId) { + this._assertRecvDirection(); + const { rtpReceiver } = this._mapRecvLocalIdInfo.get(localId) || {}; + if (!rtpReceiver) throw new Error('associated RTCRtpReceiver not found'); + return rtpReceiver.getStats(); + } + async receiveDataChannel({ sctpStreamParameters, label, protocol }) { + this._assertRecvDirection(); + const { streamId, ordered, maxPacketLifeTime, maxRetransmits } = sctpStreamParameters; + const options = { + negotiated: true, + id: streamId, + ordered, + maxPacketLifeTime, + maxRetransmits, + protocol, + }; + logger.debug('receiveDataChannel() [options:%o]', options); + const dataChannel = this._pc.createDataChannel(label, options); + // If this is the first DataChannel we need to create the SDP offer with + // m=application section. + if (!this._hasDataChannelMediaSection) { + this._remoteSdp.receiveSctpAssociation({ oldDataChannelSpec: true }); + const offer = { type: 'offer', sdp: this._remoteSdp.getSdp() }; + logger.debug('receiveDataChannel() | calling pc.setRemoteDescription() [offer:%o]', offer); + await this._pc.setRemoteDescription(offer); + const answer = await this._pc.createAnswer(); + if (!this._transportReady) { + const localSdpObject = sdpTransform.parse(answer.sdp); + await this._setupTransport({ localDtlsRole: 'client', localSdpObject }); + } + logger.debug( + 'receiveDataChannel() | calling pc.setRemoteDescription() [answer:%o]', + answer, + ); + await this._pc.setLocalDescription(answer); + this._hasDataChannelMediaSection = true; + } + return { dataChannel }; + } + async _setupTransport({ localDtlsRole, localSdpObject }) { + if (!localSdpObject) localSdpObject = sdpTransform.parse(this._pc.localDescription.sdp); + // Get our local DTLS parameters. + const dtlsParameters = sdpCommonUtils.extractDtlsParameters({ sdpObject: localSdpObject }); + // Set our DTLS role. + dtlsParameters.role = localDtlsRole; + // Update the remote DTLS role in the SDP. + this._remoteSdp.updateDtlsRole(localDtlsRole === 'client' ? 'server' : 'client'); + // Need to tell the remote transport about our parameters. + await this.safeEmitAsPromise('@connect', { dtlsParameters }); + this._transportReady = true; + } + _assertSendDirection() { + if (this._direction !== 'send') { + throw new Error('method can just be called for handlers with "send" direction'); + } + } + _assertRecvDirection() { + if (this._direction !== 'recv') { + throw new Error('method can just be called for handlers with "recv" direction'); + } + } + } + exports.Safari11 = Safari11; + }, + { + '../Logger': 13, + '../ortc': 36, + '../utils': 39, + './HandlerInterface': 25, + './sdp/RemoteSdp': 31, + './sdp/commonUtils': 32, + './sdp/planBUtils': 33, + 'sdp-transform': 44, + }, + ], + 28: [ + function (require, module, exports) { + 'use strict'; + var __createBinding = + (this && this.__createBinding) || + (Object.create + ? function (o, m, k, k2) { + if (k2 === undefined) k2 = k; + Object.defineProperty(o, k2, { + enumerable: true, + get: function () { + return m[k]; + }, + }); + } + : function (o, m, k, k2) { + if (k2 === undefined) k2 = k; + o[k2] = m[k]; + }); + var __setModuleDefault = + (this && this.__setModuleDefault) || + (Object.create + ? function (o, v) { + Object.defineProperty(o, 'default', { enumerable: true, value: v }); + } + : function (o, v) { + o['default'] = v; + }); + var __importStar = + (this && this.__importStar) || + function (mod) { + if (mod && mod.__esModule) return mod; + var result = {}; + if (mod != null) + for (var k in mod) + if (k !== 'default' && Object.hasOwnProperty.call(mod, k)) + __createBinding(result, mod, k); + __setModuleDefault(result, mod); + return result; + }; + Object.defineProperty(exports, '__esModule', { value: true }); + exports.Safari12 = void 0; + const sdpTransform = __importStar(require('sdp-transform')); + const Logger_1 = require('../Logger'); + const utils = __importStar(require('../utils')); + const ortc = __importStar(require('../ortc')); + const sdpCommonUtils = __importStar(require('./sdp/commonUtils')); + const sdpUnifiedPlanUtils = __importStar(require('./sdp/unifiedPlanUtils')); + const HandlerInterface_1 = require('./HandlerInterface'); + const RemoteSdp_1 = require('./sdp/RemoteSdp'); + const logger = new Logger_1.Logger('Safari12'); + const SCTP_NUM_STREAMS = { OS: 1024, MIS: 1024 }; + class Safari12 extends HandlerInterface_1.HandlerInterface { + constructor() { + super(); + // Map of RTCTransceivers indexed by MID. + this._mapMidTransceiver = new Map(); + // Local stream for sending. + this._sendStream = new MediaStream(); + // Whether a DataChannel m=application section has been created. + this._hasDataChannelMediaSection = false; + // Sending DataChannel id value counter. Incremented for each new DataChannel. + this._nextSendSctpStreamId = 0; + // Got transport local and remote parameters. + this._transportReady = false; + } + /** + * Creates a factory function. + */ + static createFactory() { + return () => new Safari12(); + } + get name() { + return 'Safari12'; + } + close() { + logger.debug('close()'); + // Close RTCPeerConnection. + if (this._pc) { + try { + this._pc.close(); + } catch (error) {} + } + } + async getNativeRtpCapabilities() { + logger.debug('getNativeRtpCapabilities()'); + const pc = new RTCPeerConnection({ + iceServers: [], + iceTransportPolicy: 'all', + bundlePolicy: 'max-bundle', + rtcpMuxPolicy: 'require', + }); + try { + pc.addTransceiver('audio'); + pc.addTransceiver('video'); + const offer = await pc.createOffer(); + try { + pc.close(); + } catch (error) {} + const sdpObject = sdpTransform.parse(offer.sdp); + const nativeRtpCapabilities = sdpCommonUtils.extractRtpCapabilities({ sdpObject }); + return nativeRtpCapabilities; + } catch (error) { + try { + pc.close(); + } catch (error2) {} + throw error; + } + } + async getNativeSctpCapabilities() { + logger.debug('getNativeSctpCapabilities()'); + return { + numStreams: SCTP_NUM_STREAMS, + }; + } + run({ + direction, + iceParameters, + iceCandidates, + dtlsParameters, + sctpParameters, + iceServers, + iceTransportPolicy, + additionalSettings, + proprietaryConstraints, + extendedRtpCapabilities, + }) { + logger.debug('run()'); + this._direction = direction; + this._remoteSdp = new RemoteSdp_1.RemoteSdp({ + iceParameters, + iceCandidates, + dtlsParameters, + sctpParameters, + }); + this._sendingRtpParametersByKind = { + audio: ortc.getSendingRtpParameters('audio', extendedRtpCapabilities), + video: ortc.getSendingRtpParameters('video', extendedRtpCapabilities), + }; + this._sendingRemoteRtpParametersByKind = { + audio: ortc.getSendingRemoteRtpParameters('audio', extendedRtpCapabilities), + video: ortc.getSendingRemoteRtpParameters('video', extendedRtpCapabilities), + }; + this._pc = new RTCPeerConnection( + Object.assign( + { + iceServers: iceServers || [], + iceTransportPolicy: iceTransportPolicy || 'all', + bundlePolicy: 'max-bundle', + rtcpMuxPolicy: 'require', + }, + additionalSettings, + ), + proprietaryConstraints, + ); + // Handle RTCPeerConnection connection status. + this._pc.addEventListener('iceconnectionstatechange', () => { + switch (this._pc.iceConnectionState) { + case 'checking': + this.emit('@connectionstatechange', 'connecting'); + break; + case 'connected': + case 'completed': + this.emit('@connectionstatechange', 'connected'); + break; + case 'failed': + this.emit('@connectionstatechange', 'failed'); + break; + case 'disconnected': + this.emit('@connectionstatechange', 'disconnected'); + break; + case 'closed': + this.emit('@connectionstatechange', 'closed'); + break; + } + }); + } + async updateIceServers(iceServers) { + logger.debug('updateIceServers()'); + const configuration = this._pc.getConfiguration(); + configuration.iceServers = iceServers; + this._pc.setConfiguration(configuration); + } + async restartIce(iceParameters) { + logger.debug('restartIce()'); + // Provide the remote SDP handler with new remote ICE parameters. + this._remoteSdp.updateIceParameters(iceParameters); + if (!this._transportReady) return; + if (this._direction === 'send') { + const offer = await this._pc.createOffer({ iceRestart: true }); + logger.debug('restartIce() | calling pc.setLocalDescription() [offer:%o]', offer); + await this._pc.setLocalDescription(offer); + const answer = { type: 'answer', sdp: this._remoteSdp.getSdp() }; + logger.debug('restartIce() | calling pc.setRemoteDescription() [answer:%o]', answer); + await this._pc.setRemoteDescription(answer); + } else { + const offer = { type: 'offer', sdp: this._remoteSdp.getSdp() }; + logger.debug('restartIce() | calling pc.setRemoteDescription() [offer:%o]', offer); + await this._pc.setRemoteDescription(offer); + const answer = await this._pc.createAnswer(); + logger.debug('restartIce() | calling pc.setLocalDescription() [answer:%o]', answer); + await this._pc.setLocalDescription(answer); + } + } + async getTransportStats() { + return this._pc.getStats(); + } + async send({ track, encodings, codecOptions, codec }) { + this._assertSendDirection(); + logger.debug('send() [kind:%s, track.id:%s]', track.kind, track.id); + const sendingRtpParameters = utils.clone(this._sendingRtpParametersByKind[track.kind], {}); + // This may throw. + sendingRtpParameters.codecs = ortc.reduceCodecs(sendingRtpParameters.codecs, codec); + const sendingRemoteRtpParameters = utils.clone( + this._sendingRemoteRtpParametersByKind[track.kind], + {}, + ); + // This may throw. + sendingRemoteRtpParameters.codecs = ortc.reduceCodecs(sendingRemoteRtpParameters.codecs, codec); + const mediaSectionIdx = this._remoteSdp.getNextMediaSectionIdx(); + const transceiver = this._pc.addTransceiver(track, { + direction: 'sendonly', + streams: [this._sendStream], + }); + let offer = await this._pc.createOffer(); + let localSdpObject = sdpTransform.parse(offer.sdp); + let offerMediaObject; + if (!this._transportReady) + await this._setupTransport({ localDtlsRole: 'server', localSdpObject }); + if (encodings && encodings.length > 1) { + logger.debug('send() | enabling legacy simulcast'); + localSdpObject = sdpTransform.parse(offer.sdp); + offerMediaObject = localSdpObject.media[mediaSectionIdx.idx]; + sdpUnifiedPlanUtils.addLegacySimulcast({ + offerMediaObject, + numStreams: encodings.length, + }); + offer = { type: 'offer', sdp: sdpTransform.write(localSdpObject) }; + } + logger.debug('send() | calling pc.setLocalDescription() [offer:%o]', offer); + await this._pc.setLocalDescription(offer); + // We can now get the transceiver.mid. + const localId = transceiver.mid; + // Set MID. + sendingRtpParameters.mid = localId; + localSdpObject = sdpTransform.parse(this._pc.localDescription.sdp); + offerMediaObject = localSdpObject.media[mediaSectionIdx.idx]; + // Set RTCP CNAME. + sendingRtpParameters.rtcp.cname = sdpCommonUtils.getCname({ offerMediaObject }); + // Set RTP encodings. + sendingRtpParameters.encodings = sdpUnifiedPlanUtils.getRtpEncodings({ offerMediaObject }); + // Complete encodings with given values. + if (encodings) { + for (let idx = 0; idx < sendingRtpParameters.encodings.length; ++idx) { + if (encodings[idx]) Object.assign(sendingRtpParameters.encodings[idx], encodings[idx]); + } + } + // If VP8 or H264 and there is effective simulcast, add scalabilityMode to + // each encoding. + if ( + sendingRtpParameters.encodings.length > 1 && + (sendingRtpParameters.codecs[0].mimeType.toLowerCase() === 'video/vp8' || + sendingRtpParameters.codecs[0].mimeType.toLowerCase() === 'video/h264') + ) { + for (const encoding of sendingRtpParameters.encodings) { + encoding.scalabilityMode = 'S1T3'; + } + } + this._remoteSdp.send({ + offerMediaObject, + reuseMid: mediaSectionIdx.reuseMid, + offerRtpParameters: sendingRtpParameters, + answerRtpParameters: sendingRemoteRtpParameters, + codecOptions, + }); + const answer = { type: 'answer', sdp: this._remoteSdp.getSdp() }; + logger.debug('send() | calling pc.setRemoteDescription() [answer:%o]', answer); + await this._pc.setRemoteDescription(answer); + // Store in the map. + this._mapMidTransceiver.set(localId, transceiver); + return { + localId, + rtpParameters: sendingRtpParameters, + rtpSender: transceiver.sender, + }; + } + async stopSending(localId) { + this._assertSendDirection(); + logger.debug('stopSending() [localId:%s]', localId); + const transceiver = this._mapMidTransceiver.get(localId); + if (!transceiver) throw new Error('associated RTCRtpTransceiver not found'); + transceiver.sender.replaceTrack(null); + this._pc.removeTrack(transceiver.sender); + this._remoteSdp.closeMediaSection(transceiver.mid); + const offer = await this._pc.createOffer(); + logger.debug('stopSending() | calling pc.setLocalDescription() [offer:%o]', offer); + await this._pc.setLocalDescription(offer); + const answer = { type: 'answer', sdp: this._remoteSdp.getSdp() }; + logger.debug('stopSending() | calling pc.setRemoteDescription() [answer:%o]', answer); + await this._pc.setRemoteDescription(answer); + } + async replaceTrack(localId, track) { + this._assertSendDirection(); + if (track) { + logger.debug('replaceTrack() [localId:%s, track.id:%s]', localId, track.id); + } else { + logger.debug('replaceTrack() [localId:%s, no track]', localId); + } + const transceiver = this._mapMidTransceiver.get(localId); + if (!transceiver) throw new Error('associated RTCRtpTransceiver not found'); + await transceiver.sender.replaceTrack(track); + } + async setMaxSpatialLayer(localId, spatialLayer) { + this._assertSendDirection(); + logger.debug('setMaxSpatialLayer() [localId:%s, spatialLayer:%s]', localId, spatialLayer); + const transceiver = this._mapMidTransceiver.get(localId); + if (!transceiver) throw new Error('associated RTCRtpTransceiver not found'); + const parameters = transceiver.sender.getParameters(); + parameters.encodings.forEach((encoding, idx) => { + if (idx <= spatialLayer) encoding.active = true; + else encoding.active = false; + }); + await transceiver.sender.setParameters(parameters); + } + async setRtpEncodingParameters(localId, params) { + this._assertSendDirection(); + logger.debug('setRtpEncodingParameters() [localId:%s, params:%o]', localId, params); + const transceiver = this._mapMidTransceiver.get(localId); + if (!transceiver) throw new Error('associated RTCRtpTransceiver not found'); + const parameters = transceiver.sender.getParameters(); + parameters.encodings.forEach((encoding, idx) => { + parameters.encodings[idx] = Object.assign(Object.assign({}, encoding), params); + }); + await transceiver.sender.setParameters(parameters); + } + async getSenderStats(localId) { + this._assertSendDirection(); + const transceiver = this._mapMidTransceiver.get(localId); + if (!transceiver) throw new Error('associated RTCRtpTransceiver not found'); + return transceiver.sender.getStats(); + } + async sendDataChannel({ ordered, maxPacketLifeTime, maxRetransmits, label, protocol }) { + this._assertSendDirection(); + const options = { + negotiated: true, + id: this._nextSendSctpStreamId, + ordered, + maxPacketLifeTime, + maxRetransmits, + protocol, + }; + logger.debug('sendDataChannel() [options:%o]', options); + const dataChannel = this._pc.createDataChannel(label, options); + // Increase next id. + this._nextSendSctpStreamId = ++this._nextSendSctpStreamId % SCTP_NUM_STREAMS.MIS; + // If this is the first DataChannel we need to create the SDP answer with + // m=application section. + if (!this._hasDataChannelMediaSection) { + const offer = await this._pc.createOffer(); + const localSdpObject = sdpTransform.parse(offer.sdp); + const offerMediaObject = localSdpObject.media.find((m) => m.type === 'application'); + if (!this._transportReady) + await this._setupTransport({ localDtlsRole: 'server', localSdpObject }); + logger.debug('sendDataChannel() | calling pc.setLocalDescription() [offer:%o]', offer); + await this._pc.setLocalDescription(offer); + this._remoteSdp.sendSctpAssociation({ offerMediaObject }); + const answer = { type: 'answer', sdp: this._remoteSdp.getSdp() }; + logger.debug('sendDataChannel() | calling pc.setRemoteDescription() [answer:%o]', answer); + await this._pc.setRemoteDescription(answer); + this._hasDataChannelMediaSection = true; + } + const sctpStreamParameters = { + streamId: options.id, + ordered: options.ordered, + maxPacketLifeTime: options.maxPacketLifeTime, + maxRetransmits: options.maxRetransmits, + }; + return { dataChannel, sctpStreamParameters }; + } + async receive({ trackId, kind, rtpParameters }) { + this._assertRecvDirection(); + logger.debug('receive() [trackId:%s, kind:%s]', trackId, kind); + const localId = rtpParameters.mid || String(this._mapMidTransceiver.size); + this._remoteSdp.receive({ + mid: localId, + kind, + offerRtpParameters: rtpParameters, + streamId: rtpParameters.rtcp.cname, + trackId, + }); + const offer = { type: 'offer', sdp: this._remoteSdp.getSdp() }; + logger.debug('receive() | calling pc.setRemoteDescription() [offer:%o]', offer); + await this._pc.setRemoteDescription(offer); + let answer = await this._pc.createAnswer(); + const localSdpObject = sdpTransform.parse(answer.sdp); + const answerMediaObject = localSdpObject.media.find((m) => String(m.mid) === localId); + // May need to modify codec parameters in the answer based on codec + // parameters in the offer. + sdpCommonUtils.applyCodecParameters({ + offerRtpParameters: rtpParameters, + answerMediaObject, + }); + answer = { type: 'answer', sdp: sdpTransform.write(localSdpObject) }; + if (!this._transportReady) + await this._setupTransport({ localDtlsRole: 'client', localSdpObject }); + logger.debug('receive() | calling pc.setLocalDescription() [answer:%o]', answer); + await this._pc.setLocalDescription(answer); + const transceiver = this._pc.getTransceivers().find((t) => t.mid === localId); + if (!transceiver) throw new Error('new RTCRtpTransceiver not found'); + // Store in the map. + this._mapMidTransceiver.set(localId, transceiver); + return { + localId, + track: transceiver.receiver.track, + rtpReceiver: transceiver.receiver, + }; + } + async stopReceiving(localId) { + this._assertRecvDirection(); + logger.debug('stopReceiving() [localId:%s]', localId); + const transceiver = this._mapMidTransceiver.get(localId); + if (!transceiver) throw new Error('associated RTCRtpTransceiver not found'); + this._remoteSdp.closeMediaSection(transceiver.mid); + const offer = { type: 'offer', sdp: this._remoteSdp.getSdp() }; + logger.debug('stopReceiving() | calling pc.setRemoteDescription() [offer:%o]', offer); + await this._pc.setRemoteDescription(offer); + const answer = await this._pc.createAnswer(); + logger.debug('stopReceiving() | calling pc.setLocalDescription() [answer:%o]', answer); + await this._pc.setLocalDescription(answer); + } + async getReceiverStats(localId) { + this._assertRecvDirection(); + const transceiver = this._mapMidTransceiver.get(localId); + if (!transceiver) throw new Error('associated RTCRtpTransceiver not found'); + return transceiver.receiver.getStats(); + } + async receiveDataChannel({ sctpStreamParameters, label, protocol }) { + this._assertRecvDirection(); + const { streamId, ordered, maxPacketLifeTime, maxRetransmits } = sctpStreamParameters; + const options = { + negotiated: true, + id: streamId, + ordered, + maxPacketLifeTime, + maxRetransmits, + protocol, + }; + logger.debug('receiveDataChannel() [options:%o]', options); + const dataChannel = this._pc.createDataChannel(label, options); + // If this is the first DataChannel we need to create the SDP offer with + // m=application section. + if (!this._hasDataChannelMediaSection) { + this._remoteSdp.receiveSctpAssociation(); + const offer = { type: 'offer', sdp: this._remoteSdp.getSdp() }; + logger.debug('receiveDataChannel() | calling pc.setRemoteDescription() [offer:%o]', offer); + await this._pc.setRemoteDescription(offer); + const answer = await this._pc.createAnswer(); + if (!this._transportReady) { + const localSdpObject = sdpTransform.parse(answer.sdp); + await this._setupTransport({ localDtlsRole: 'client', localSdpObject }); + } + logger.debug( + 'receiveDataChannel() | calling pc.setRemoteDescription() [answer:%o]', + answer, + ); + await this._pc.setLocalDescription(answer); + this._hasDataChannelMediaSection = true; + } + return { dataChannel }; + } + async _setupTransport({ localDtlsRole, localSdpObject }) { + if (!localSdpObject) localSdpObject = sdpTransform.parse(this._pc.localDescription.sdp); + // Get our local DTLS parameters. + const dtlsParameters = sdpCommonUtils.extractDtlsParameters({ sdpObject: localSdpObject }); + // Set our DTLS role. + dtlsParameters.role = localDtlsRole; + // Update the remote DTLS role in the SDP. + this._remoteSdp.updateDtlsRole(localDtlsRole === 'client' ? 'server' : 'client'); + // Need to tell the remote transport about our parameters. + await this.safeEmitAsPromise('@connect', { dtlsParameters }); + this._transportReady = true; + } + _assertSendDirection() { + if (this._direction !== 'send') { + throw new Error('method can just be called for handlers with "send" direction'); + } + } + _assertRecvDirection() { + if (this._direction !== 'recv') { + throw new Error('method can just be called for handlers with "recv" direction'); + } + } + } + exports.Safari12 = Safari12; + }, + { + '../Logger': 13, + '../ortc': 36, + '../utils': 39, + './HandlerInterface': 25, + './sdp/RemoteSdp': 31, + './sdp/commonUtils': 32, + './sdp/unifiedPlanUtils': 34, + 'sdp-transform': 44, + }, + ], + 29: [ + function (require, module, exports) { + 'use strict'; + var __createBinding = + (this && this.__createBinding) || + (Object.create + ? function (o, m, k, k2) { + if (k2 === undefined) k2 = k; + Object.defineProperty(o, k2, { + enumerable: true, + get: function () { + return m[k]; + }, + }); + } + : function (o, m, k, k2) { + if (k2 === undefined) k2 = k; + o[k2] = m[k]; + }); + var __setModuleDefault = + (this && this.__setModuleDefault) || + (Object.create + ? function (o, v) { + Object.defineProperty(o, 'default', { enumerable: true, value: v }); + } + : function (o, v) { + o['default'] = v; + }); + var __importStar = + (this && this.__importStar) || + function (mod) { + if (mod && mod.__esModule) return mod; + var result = {}; + if (mod != null) + for (var k in mod) + if (k !== 'default' && Object.hasOwnProperty.call(mod, k)) + __createBinding(result, mod, k); + __setModuleDefault(result, mod); + return result; + }; + Object.defineProperty(exports, '__esModule', { value: true }); + exports.mangleRtpParameters = exports.getCapabilities = void 0; + const utils = __importStar(require('../../utils')); + /** + * Normalize ORTC based Edge's RTCRtpReceiver.getCapabilities() to produce a full + * compliant ORTC RTCRtpCapabilities. + */ + function getCapabilities() { + const nativeCaps = RTCRtpReceiver.getCapabilities(); + const caps = utils.clone(nativeCaps, {}); + for (const codec of caps.codecs) { + // Rename numChannels to channels. + codec.channels = codec.numChannels; + delete codec.numChannels; + // Add mimeType. + codec.mimeType = codec.mimeType || `${codec.kind}/${codec.name}`; + // NOTE: Edge sets some numeric parameters as string rather than number. Fix them. + if (codec.parameters) { + const parameters = codec.parameters; + if (parameters.apt) parameters.apt = Number(parameters.apt); + if (parameters['packetization-mode']) + parameters['packetization-mode'] = Number(parameters['packetization-mode']); + } + // Delete emty parameter String in rtcpFeedback. + for (const feedback of codec.rtcpFeedback || []) { + if (!feedback.parameter) feedback.parameter = ''; + } + } + return caps; + } + exports.getCapabilities = getCapabilities; + /** + * Generate RTCRtpParameters as ORTC based Edge likes. + */ + function mangleRtpParameters(rtpParameters) { + const params = utils.clone(rtpParameters, {}); + // Rename mid to muxId. + if (params.mid) { + params.muxId = params.mid; + delete params.mid; + } + for (const codec of params.codecs) { + // Rename channels to numChannels. + if (codec.channels) { + codec.numChannels = codec.channels; + delete codec.channels; + } + // Add codec.name (requried by Edge). + if (codec.mimeType && !codec.name) codec.name = codec.mimeType.split('/')[1]; + // Remove mimeType. + delete codec.mimeType; + } + return params; + } + exports.mangleRtpParameters = mangleRtpParameters; + }, + { '../../utils': 39 }, + ], + 30: [ + function (require, module, exports) { + 'use strict'; + var __createBinding = + (this && this.__createBinding) || + (Object.create + ? function (o, m, k, k2) { + if (k2 === undefined) k2 = k; + Object.defineProperty(o, k2, { + enumerable: true, + get: function () { + return m[k]; + }, + }); + } + : function (o, m, k, k2) { + if (k2 === undefined) k2 = k; + o[k2] = m[k]; + }); + var __setModuleDefault = + (this && this.__setModuleDefault) || + (Object.create + ? function (o, v) { + Object.defineProperty(o, 'default', { enumerable: true, value: v }); + } + : function (o, v) { + o['default'] = v; + }); + var __importStar = + (this && this.__importStar) || + function (mod) { + if (mod && mod.__esModule) return mod; + var result = {}; + if (mod != null) + for (var k in mod) + if (k !== 'default' && Object.hasOwnProperty.call(mod, k)) + __createBinding(result, mod, k); + __setModuleDefault(result, mod); + return result; + }; + Object.defineProperty(exports, '__esModule', { value: true }); + exports.OfferMediaSection = exports.AnswerMediaSection = exports.MediaSection = void 0; + const utils = __importStar(require('../../utils')); + class MediaSection { + constructor({ iceParameters, iceCandidates, dtlsParameters, planB = false }) { + this._mediaObject = {}; + this._planB = planB; + if (iceParameters) { + this.setIceParameters(iceParameters); + } + if (iceCandidates) { + this._mediaObject.candidates = []; + for (const candidate of iceCandidates) { + const candidateObject = {}; + // mediasoup does mandates rtcp-mux so candidates component is always + // RTP (1). + candidateObject.component = 1; + candidateObject.foundation = candidate.foundation; + candidateObject.ip = candidate.ip; + candidateObject.port = candidate.port; + candidateObject.priority = candidate.priority; + candidateObject.transport = candidate.protocol; + candidateObject.type = candidate.type; + if (candidate.tcpType) candidateObject.tcptype = candidate.tcpType; + this._mediaObject.candidates.push(candidateObject); + } + this._mediaObject.endOfCandidates = 'end-of-candidates'; + this._mediaObject.iceOptions = 'renomination'; + } + if (dtlsParameters) { + this.setDtlsRole(dtlsParameters.role); + } + } + get mid() { + return String(this._mediaObject.mid); + } + get closed() { + return this._mediaObject.port === 0; + } + getObject() { + return this._mediaObject; + } + setIceParameters(iceParameters) { + this._mediaObject.iceUfrag = iceParameters.usernameFragment; + this._mediaObject.icePwd = iceParameters.password; + } + disable() { + this._mediaObject.direction = 'inactive'; + delete this._mediaObject.ext; + delete this._mediaObject.ssrcs; + delete this._mediaObject.ssrcGroups; + delete this._mediaObject.simulcast; + delete this._mediaObject.simulcast_03; + delete this._mediaObject.rids; + } + close() { + this._mediaObject.direction = 'inactive'; + this._mediaObject.port = 0; + delete this._mediaObject.ext; + delete this._mediaObject.ssrcs; + delete this._mediaObject.ssrcGroups; + delete this._mediaObject.simulcast; + delete this._mediaObject.simulcast_03; + delete this._mediaObject.rids; + delete this._mediaObject.extmapAllowMixed; + } + } + exports.MediaSection = MediaSection; + class AnswerMediaSection extends MediaSection { + constructor({ + iceParameters, + iceCandidates, + dtlsParameters, + sctpParameters, + plainRtpParameters, + planB = false, + offerMediaObject, + offerRtpParameters, + answerRtpParameters, + codecOptions, + extmapAllowMixed = false, + }) { + super({ iceParameters, iceCandidates, dtlsParameters, planB }); + this._mediaObject.mid = String(offerMediaObject.mid); + this._mediaObject.type = offerMediaObject.type; + this._mediaObject.protocol = offerMediaObject.protocol; + if (!plainRtpParameters) { + this._mediaObject.connection = { ip: '127.0.0.1', version: 4 }; + this._mediaObject.port = 7; + } else { + this._mediaObject.connection = { + ip: plainRtpParameters.ip, + version: plainRtpParameters.ipVersion, + }; + this._mediaObject.port = plainRtpParameters.port; + } + switch (offerMediaObject.type) { + case 'audio': + case 'video': { + this._mediaObject.direction = 'recvonly'; + this._mediaObject.rtp = []; + this._mediaObject.rtcpFb = []; + this._mediaObject.fmtp = []; + for (const codec of answerRtpParameters.codecs) { + const rtp = { + payload: codec.payloadType, + codec: getCodecName(codec), + rate: codec.clockRate, + }; + if (codec.channels > 1) rtp.encoding = codec.channels; + this._mediaObject.rtp.push(rtp); + const codecParameters = utils.clone(codec.parameters, {}); + if (codecOptions) { + const { + opusStereo, + opusFec, + opusDtx, + opusMaxPlaybackRate, + opusMaxAverageBitrate, + opusPtime, + videoGoogleStartBitrate, + videoGoogleMaxBitrate, + videoGoogleMinBitrate, + } = codecOptions; + const offerCodec = offerRtpParameters.codecs.find( + (c) => c.payloadType === codec.payloadType, + ); + switch (codec.mimeType.toLowerCase()) { + case 'audio/opus': { + if (opusStereo !== undefined) { + offerCodec.parameters['sprop-stereo'] = opusStereo ? 1 : 0; + codecParameters.stereo = opusStereo ? 1 : 0; + } + if (opusFec !== undefined) { + offerCodec.parameters.useinbandfec = opusFec ? 1 : 0; + codecParameters.useinbandfec = opusFec ? 1 : 0; + } + if (opusDtx !== undefined) { + offerCodec.parameters.usedtx = opusDtx ? 1 : 0; + codecParameters.usedtx = opusDtx ? 1 : 0; + } + if (opusMaxPlaybackRate !== undefined) { + codecParameters.maxplaybackrate = opusMaxPlaybackRate; + } + if (opusMaxAverageBitrate !== undefined) { + codecParameters.maxaveragebitrate = opusMaxAverageBitrate; + } + if (opusPtime !== undefined) { + offerCodec.parameters.ptime = opusPtime; + codecParameters.ptime = opusPtime; + } + break; + } + case 'video/vp8': + case 'video/vp9': + case 'video/h264': + case 'video/h265': { + if (videoGoogleStartBitrate !== undefined) + codecParameters['x-google-start-bitrate'] = videoGoogleStartBitrate; + if (videoGoogleMaxBitrate !== undefined) + codecParameters['x-google-max-bitrate'] = videoGoogleMaxBitrate; + if (videoGoogleMinBitrate !== undefined) + codecParameters['x-google-min-bitrate'] = videoGoogleMinBitrate; + break; + } + } + } + const fmtp = { + payload: codec.payloadType, + config: '', + }; + for (const key of Object.keys(codecParameters)) { + if (fmtp.config) fmtp.config += ';'; + fmtp.config += `${key}=${codecParameters[key]}`; + } + if (fmtp.config) this._mediaObject.fmtp.push(fmtp); + for (const fb of codec.rtcpFeedback) { + this._mediaObject.rtcpFb.push({ + payload: codec.payloadType, + type: fb.type, + subtype: fb.parameter, + }); + } + } + this._mediaObject.payloads = answerRtpParameters.codecs + .map((codec) => codec.payloadType) + .join(' '); + this._mediaObject.ext = []; + for (const ext of answerRtpParameters.headerExtensions) { + // Don't add a header extension if not present in the offer. + const found = (offerMediaObject.ext || []).some( + (localExt) => localExt.uri === ext.uri, + ); + if (!found) continue; + this._mediaObject.ext.push({ + uri: ext.uri, + value: ext.id, + }); + } + // Allow both 1 byte and 2 bytes length header extensions. + if (extmapAllowMixed && offerMediaObject.extmapAllowMixed === 'extmap-allow-mixed') { + this._mediaObject.extmapAllowMixed = 'extmap-allow-mixed'; + } + // Simulcast. + if (offerMediaObject.simulcast) { + this._mediaObject.simulcast = { + dir1: 'recv', + list1: offerMediaObject.simulcast.list1, + }; + this._mediaObject.rids = []; + for (const rid of offerMediaObject.rids || []) { + if (rid.direction !== 'send') continue; + this._mediaObject.rids.push({ + id: rid.id, + direction: 'recv', + }); + } + } + // Simulcast (draft version 03). + else if (offerMediaObject.simulcast_03) { + // eslint-disable-next-line camelcase + this._mediaObject.simulcast_03 = { + value: offerMediaObject.simulcast_03.value.replace(/send/g, 'recv'), + }; + this._mediaObject.rids = []; + for (const rid of offerMediaObject.rids || []) { + if (rid.direction !== 'send') continue; + this._mediaObject.rids.push({ + id: rid.id, + direction: 'recv', + }); + } + } + this._mediaObject.rtcpMux = 'rtcp-mux'; + this._mediaObject.rtcpRsize = 'rtcp-rsize'; + if (this._planB && this._mediaObject.type === 'video') + this._mediaObject.xGoogleFlag = 'conference'; + break; + } + case 'application': { + // New spec. + if (typeof offerMediaObject.sctpPort === 'number') { + this._mediaObject.payloads = 'webrtc-datachannel'; + this._mediaObject.sctpPort = sctpParameters.port; + this._mediaObject.maxMessageSize = sctpParameters.maxMessageSize; + } + // Old spec. + else if (offerMediaObject.sctpmap) { + this._mediaObject.payloads = sctpParameters.port; + this._mediaObject.sctpmap = { + app: 'webrtc-datachannel', + sctpmapNumber: sctpParameters.port, + maxMessageSize: sctpParameters.maxMessageSize, + }; + } + break; + } + } + } + setDtlsRole(role) { + switch (role) { + case 'client': + this._mediaObject.setup = 'active'; + break; + case 'server': + this._mediaObject.setup = 'passive'; + break; + case 'auto': + this._mediaObject.setup = 'actpass'; + break; + } + } + } + exports.AnswerMediaSection = AnswerMediaSection; + class OfferMediaSection extends MediaSection { + constructor({ + iceParameters, + iceCandidates, + dtlsParameters, + sctpParameters, + plainRtpParameters, + planB = false, + mid, + kind, + offerRtpParameters, + streamId, + trackId, + oldDataChannelSpec = false, + }) { + super({ iceParameters, iceCandidates, dtlsParameters, planB }); + this._mediaObject.mid = String(mid); + this._mediaObject.type = kind; + if (!plainRtpParameters) { + this._mediaObject.connection = { ip: '127.0.0.1', version: 4 }; + if (!sctpParameters) this._mediaObject.protocol = 'UDP/TLS/RTP/SAVPF'; + else this._mediaObject.protocol = 'UDP/DTLS/SCTP'; + this._mediaObject.port = 7; + } else { + this._mediaObject.connection = { + ip: plainRtpParameters.ip, + version: plainRtpParameters.ipVersion, + }; + this._mediaObject.protocol = 'RTP/AVP'; + this._mediaObject.port = plainRtpParameters.port; + } + switch (kind) { + case 'audio': + case 'video': { + this._mediaObject.direction = 'sendonly'; + this._mediaObject.rtp = []; + this._mediaObject.rtcpFb = []; + this._mediaObject.fmtp = []; + if (!this._planB) this._mediaObject.msid = `${streamId || '-'} ${trackId}`; + for (const codec of offerRtpParameters.codecs) { + const rtp = { + payload: codec.payloadType, + codec: getCodecName(codec), + rate: codec.clockRate, + }; + if (codec.channels > 1) rtp.encoding = codec.channels; + this._mediaObject.rtp.push(rtp); + const fmtp = { + payload: codec.payloadType, + config: '', + }; + for (const key of Object.keys(codec.parameters)) { + if (fmtp.config) fmtp.config += ';'; + fmtp.config += `${key}=${codec.parameters[key]}`; + } + if (fmtp.config) this._mediaObject.fmtp.push(fmtp); + for (const fb of codec.rtcpFeedback) { + this._mediaObject.rtcpFb.push({ + payload: codec.payloadType, + type: fb.type, + subtype: fb.parameter, + }); + } + } + this._mediaObject.payloads = offerRtpParameters.codecs + .map((codec) => codec.payloadType) + .join(' '); + this._mediaObject.ext = []; + for (const ext of offerRtpParameters.headerExtensions) { + this._mediaObject.ext.push({ + uri: ext.uri, + value: ext.id, + }); + } + this._mediaObject.rtcpMux = 'rtcp-mux'; + this._mediaObject.rtcpRsize = 'rtcp-rsize'; + const encoding = offerRtpParameters.encodings[0]; + const ssrc = encoding.ssrc; + const rtxSsrc = encoding.rtx && encoding.rtx.ssrc ? encoding.rtx.ssrc : undefined; + this._mediaObject.ssrcs = []; + this._mediaObject.ssrcGroups = []; + if (offerRtpParameters.rtcp.cname) { + this._mediaObject.ssrcs.push({ + id: ssrc, + attribute: 'cname', + value: offerRtpParameters.rtcp.cname, + }); + } + if (this._planB) { + this._mediaObject.ssrcs.push({ + id: ssrc, + attribute: 'msid', + value: `${streamId || '-'} ${trackId}`, + }); + } + if (rtxSsrc) { + if (offerRtpParameters.rtcp.cname) { + this._mediaObject.ssrcs.push({ + id: rtxSsrc, + attribute: 'cname', + value: offerRtpParameters.rtcp.cname, + }); + } + if (this._planB) { + this._mediaObject.ssrcs.push({ + id: rtxSsrc, + attribute: 'msid', + value: `${streamId || '-'} ${trackId}`, + }); + } + // Associate original and retransmission SSRCs. + this._mediaObject.ssrcGroups.push({ + semantics: 'FID', + ssrcs: `${ssrc} ${rtxSsrc}`, + }); + } + break; + } + case 'application': { + // New spec. + if (!oldDataChannelSpec) { + this._mediaObject.payloads = 'webrtc-datachannel'; + this._mediaObject.sctpPort = sctpParameters.port; + this._mediaObject.maxMessageSize = sctpParameters.maxMessageSize; + } + // Old spec. + else { + this._mediaObject.payloads = sctpParameters.port; + this._mediaObject.sctpmap = { + app: 'webrtc-datachannel', + sctpmapNumber: sctpParameters.port, + maxMessageSize: sctpParameters.maxMessageSize, + }; + } + break; + } + } + } + // eslint-disable-next-line @typescript-eslint/no-unused-vars + setDtlsRole(role) { + // Always 'actpass'. + this._mediaObject.setup = 'actpass'; + } + planBReceive({ offerRtpParameters, streamId, trackId }) { + const encoding = offerRtpParameters.encodings[0]; + const ssrc = encoding.ssrc; + const rtxSsrc = encoding.rtx && encoding.rtx.ssrc ? encoding.rtx.ssrc : undefined; + const payloads = this._mediaObject.payloads.split(' '); + for (const codec of offerRtpParameters.codecs) { + if (payloads.includes(String(codec.payloadType))) { + continue; + } + const rtp = { + payload: codec.payloadType, + codec: getCodecName(codec), + rate: codec.clockRate, + }; + if (codec.channels > 1) rtp.encoding = codec.channels; + this._mediaObject.rtp.push(rtp); + const fmtp = { + payload: codec.payloadType, + config: '', + }; + for (const key of Object.keys(codec.parameters)) { + if (fmtp.config) fmtp.config += ';'; + fmtp.config += `${key}=${codec.parameters[key]}`; + } + if (fmtp.config) this._mediaObject.fmtp.push(fmtp); + for (const fb of codec.rtcpFeedback) { + this._mediaObject.rtcpFb.push({ + payload: codec.payloadType, + type: fb.type, + subtype: fb.parameter, + }); + } + } + this._mediaObject.payloads += ` ${offerRtpParameters.codecs + .filter((codec) => !this._mediaObject.payloads.includes(codec.payloadType)) + .map((codec) => codec.payloadType) + .join(' ')}`; + this._mediaObject.payloads = this._mediaObject.payloads.trim(); + if (offerRtpParameters.rtcp.cname) { + this._mediaObject.ssrcs.push({ + id: ssrc, + attribute: 'cname', + value: offerRtpParameters.rtcp.cname, + }); + } + this._mediaObject.ssrcs.push({ + id: ssrc, + attribute: 'msid', + value: `${streamId || '-'} ${trackId}`, + }); + if (rtxSsrc) { + if (offerRtpParameters.rtcp.cname) { + this._mediaObject.ssrcs.push({ + id: rtxSsrc, + attribute: 'cname', + value: offerRtpParameters.rtcp.cname, + }); + } + this._mediaObject.ssrcs.push({ + id: rtxSsrc, + attribute: 'msid', + value: `${streamId || '-'} ${trackId}`, + }); + // Associate original and retransmission SSRCs. + this._mediaObject.ssrcGroups.push({ + semantics: 'FID', + ssrcs: `${ssrc} ${rtxSsrc}`, + }); + } + } + planBStopReceiving({ offerRtpParameters }) { + const encoding = offerRtpParameters.encodings[0]; + const ssrc = encoding.ssrc; + const rtxSsrc = encoding.rtx && encoding.rtx.ssrc ? encoding.rtx.ssrc : undefined; + this._mediaObject.ssrcs = this._mediaObject.ssrcs.filter( + (s) => s.id !== ssrc && s.id !== rtxSsrc, + ); + if (rtxSsrc) { + this._mediaObject.ssrcGroups = this._mediaObject.ssrcGroups.filter( + (group) => group.ssrcs !== `${ssrc} ${rtxSsrc}`, + ); + } + } + } + exports.OfferMediaSection = OfferMediaSection; + function getCodecName(codec) { + const MimeTypeRegex = new RegExp('^(audio|video)/(.+)', 'i'); + const mimeTypeMatch = MimeTypeRegex.exec(codec.mimeType); + if (!mimeTypeMatch) throw new TypeError('invalid codec.mimeType'); + return mimeTypeMatch[2]; + } + }, + { '../../utils': 39 }, + ], + 31: [ + function (require, module, exports) { + 'use strict'; + var __createBinding = + (this && this.__createBinding) || + (Object.create + ? function (o, m, k, k2) { + if (k2 === undefined) k2 = k; + Object.defineProperty(o, k2, { + enumerable: true, + get: function () { + return m[k]; + }, + }); + } + : function (o, m, k, k2) { + if (k2 === undefined) k2 = k; + o[k2] = m[k]; + }); + var __setModuleDefault = + (this && this.__setModuleDefault) || + (Object.create + ? function (o, v) { + Object.defineProperty(o, 'default', { enumerable: true, value: v }); + } + : function (o, v) { + o['default'] = v; + }); + var __importStar = + (this && this.__importStar) || + function (mod) { + if (mod && mod.__esModule) return mod; + var result = {}; + if (mod != null) + for (var k in mod) + if (k !== 'default' && Object.hasOwnProperty.call(mod, k)) + __createBinding(result, mod, k); + __setModuleDefault(result, mod); + return result; + }; + Object.defineProperty(exports, '__esModule', { value: true }); + exports.RemoteSdp = void 0; + const sdpTransform = __importStar(require('sdp-transform')); + const Logger_1 = require('../../Logger'); + const MediaSection_1 = require('./MediaSection'); + const logger = new Logger_1.Logger('RemoteSdp'); + class RemoteSdp { + constructor({ + iceParameters, + iceCandidates, + dtlsParameters, + sctpParameters, + plainRtpParameters, + planB = false, + }) { + // MediaSection instances with same order as in the SDP. + this._mediaSections = []; + // MediaSection indices indexed by MID. + this._midToIndex = new Map(); + this._iceParameters = iceParameters; + this._iceCandidates = iceCandidates; + this._dtlsParameters = dtlsParameters; + this._sctpParameters = sctpParameters; + this._plainRtpParameters = plainRtpParameters; + this._planB = planB; + this._sdpObject = { + version: 0, + origin: { + address: '0.0.0.0', + ipVer: 4, + netType: 'IN', + sessionId: 10000, + sessionVersion: 0, + username: 'mediasoup-client', + }, + name: '-', + timing: { start: 0, stop: 0 }, + media: [], + }; + // If ICE parameters are given, add ICE-Lite indicator. + if (iceParameters && iceParameters.iceLite) { + this._sdpObject.icelite = 'ice-lite'; + } + // If DTLS parameters are given, assume WebRTC and BUNDLE. + if (dtlsParameters) { + this._sdpObject.msidSemantic = { semantic: 'WMS', token: '*' }; + // NOTE: We take the latest fingerprint. + const numFingerprints = this._dtlsParameters.fingerprints.length; + this._sdpObject.fingerprint = { + type: dtlsParameters.fingerprints[numFingerprints - 1].algorithm, + hash: dtlsParameters.fingerprints[numFingerprints - 1].value, + }; + this._sdpObject.groups = [{ type: 'BUNDLE', mids: '' }]; + } + // If there are plain RPT parameters, override SDP origin. + if (plainRtpParameters) { + this._sdpObject.origin.address = plainRtpParameters.ip; + this._sdpObject.origin.ipVer = plainRtpParameters.ipVersion; + } + } + updateIceParameters(iceParameters) { + logger.debug('updateIceParameters() [iceParameters:%o]', iceParameters); + this._iceParameters = iceParameters; + this._sdpObject.icelite = iceParameters.iceLite ? 'ice-lite' : undefined; + for (const mediaSection of this._mediaSections) { + mediaSection.setIceParameters(iceParameters); + } + } + updateDtlsRole(role) { + logger.debug('updateDtlsRole() [role:%s]', role); + this._dtlsParameters.role = role; + for (const mediaSection of this._mediaSections) { + mediaSection.setDtlsRole(role); + } + } + getNextMediaSectionIdx() { + // If a closed media section is found, return its index. + for (let idx = 0; idx < this._mediaSections.length; ++idx) { + const mediaSection = this._mediaSections[idx]; + if (mediaSection.closed) return { idx, reuseMid: mediaSection.mid }; + } + // If no closed media section is found, return next one. + return { idx: this._mediaSections.length }; + } + send({ + offerMediaObject, + reuseMid, + offerRtpParameters, + answerRtpParameters, + codecOptions, + extmapAllowMixed = false, + }) { + const mediaSection = new MediaSection_1.AnswerMediaSection({ + iceParameters: this._iceParameters, + iceCandidates: this._iceCandidates, + dtlsParameters: this._dtlsParameters, + plainRtpParameters: this._plainRtpParameters, + planB: this._planB, + offerMediaObject, + offerRtpParameters, + answerRtpParameters, + codecOptions, + extmapAllowMixed, + }); + // Unified-Plan with closed media section replacement. + if (reuseMid) { + this._replaceMediaSection(mediaSection, reuseMid); + } + // Unified-Plan or Plan-B with different media kind. + else if (!this._midToIndex.has(mediaSection.mid)) { + this._addMediaSection(mediaSection); + } + // Plan-B with same media kind. + else { + this._replaceMediaSection(mediaSection); + } + } + receive({ mid, kind, offerRtpParameters, streamId, trackId }) { + const idx = this._midToIndex.get(mid); + let mediaSection; + if (idx !== undefined) mediaSection = this._mediaSections[idx]; + // Unified-Plan or different media kind. + if (!mediaSection) { + mediaSection = new MediaSection_1.OfferMediaSection({ + iceParameters: this._iceParameters, + iceCandidates: this._iceCandidates, + dtlsParameters: this._dtlsParameters, + plainRtpParameters: this._plainRtpParameters, + planB: this._planB, + mid, + kind, + offerRtpParameters, + streamId, + trackId, + }); + // Let's try to recycle a closed media section (if any). + // NOTE: Yes, we can recycle a closed m=audio section with a new m=video. + const oldMediaSection = this._mediaSections.find((m) => m.closed); + if (oldMediaSection) { + this._replaceMediaSection(mediaSection, oldMediaSection.mid); + } else { + this._addMediaSection(mediaSection); + } + } + // Plan-B. + else { + mediaSection.planBReceive({ offerRtpParameters, streamId, trackId }); + this._replaceMediaSection(mediaSection); + } + } + disableMediaSection(mid) { + const idx = this._midToIndex.get(mid); + if (idx === undefined) { + throw new Error(`no media section found with mid '${mid}'`); + } + const mediaSection = this._mediaSections[idx]; + mediaSection.disable(); + } + closeMediaSection(mid) { + const idx = this._midToIndex.get(mid); + if (idx === undefined) { + throw new Error(`no media section found with mid '${mid}'`); + } + const mediaSection = this._mediaSections[idx]; + // NOTE: Closing the first m section is a pain since it invalidates the + // bundled transport, so let's avoid it. + if (mid === this._firstMid) { + logger.debug( + 'closeMediaSection() | cannot close first media section, disabling it instead [mid:%s]', + mid, + ); + this.disableMediaSection(mid); + return; + } + mediaSection.close(); + // Regenerate BUNDLE mids. + this._regenerateBundleMids(); + } + planBStopReceiving({ mid, offerRtpParameters }) { + const idx = this._midToIndex.get(mid); + if (idx === undefined) { + throw new Error(`no media section found with mid '${mid}'`); + } + const mediaSection = this._mediaSections[idx]; + mediaSection.planBStopReceiving({ offerRtpParameters }); + this._replaceMediaSection(mediaSection); + } + sendSctpAssociation({ offerMediaObject }) { + const mediaSection = new MediaSection_1.AnswerMediaSection({ + iceParameters: this._iceParameters, + iceCandidates: this._iceCandidates, + dtlsParameters: this._dtlsParameters, + sctpParameters: this._sctpParameters, + plainRtpParameters: this._plainRtpParameters, + offerMediaObject, + }); + this._addMediaSection(mediaSection); + } + receiveSctpAssociation({ oldDataChannelSpec = false } = {}) { + const mediaSection = new MediaSection_1.OfferMediaSection({ + iceParameters: this._iceParameters, + iceCandidates: this._iceCandidates, + dtlsParameters: this._dtlsParameters, + sctpParameters: this._sctpParameters, + plainRtpParameters: this._plainRtpParameters, + mid: 'datachannel', + kind: 'application', + oldDataChannelSpec, + }); + this._addMediaSection(mediaSection); + } + getSdp() { + // Increase SDP version. + this._sdpObject.origin.sessionVersion++; + return sdpTransform.write(this._sdpObject); + } + _addMediaSection(newMediaSection) { + if (!this._firstMid) this._firstMid = newMediaSection.mid; + // Add to the vector. + this._mediaSections.push(newMediaSection); + // Add to the map. + this._midToIndex.set(newMediaSection.mid, this._mediaSections.length - 1); + // Add to the SDP object. + this._sdpObject.media.push(newMediaSection.getObject()); + // Regenerate BUNDLE mids. + this._regenerateBundleMids(); + } + _replaceMediaSection(newMediaSection, reuseMid) { + // Store it in the map. + if (typeof reuseMid === 'string') { + const idx = this._midToIndex.get(reuseMid); + if (idx === undefined) { + throw new Error(`no media section found for reuseMid '${reuseMid}'`); + } + const oldMediaSection = this._mediaSections[idx]; + // Replace the index in the vector with the new media section. + this._mediaSections[idx] = newMediaSection; + // Update the map. + this._midToIndex.delete(oldMediaSection.mid); + this._midToIndex.set(newMediaSection.mid, idx); + // Update the SDP object. + this._sdpObject.media[idx] = newMediaSection.getObject(); + // Regenerate BUNDLE mids. + this._regenerateBundleMids(); + } else { + const idx = this._midToIndex.get(newMediaSection.mid); + if (idx === undefined) { + throw new Error(`no media section found with mid '${newMediaSection.mid}'`); + } + // Replace the index in the vector with the new media section. + this._mediaSections[idx] = newMediaSection; + // Update the SDP object. + this._sdpObject.media[idx] = newMediaSection.getObject(); + } + } + _regenerateBundleMids() { + if (!this._dtlsParameters) return; + this._sdpObject.groups[0].mids = this._mediaSections + .filter((mediaSection) => !mediaSection.closed) + .map((mediaSection) => mediaSection.mid) + .join(' '); + } + } + exports.RemoteSdp = RemoteSdp; + }, + { '../../Logger': 13, './MediaSection': 30, 'sdp-transform': 44 }, + ], + 32: [ + function (require, module, exports) { + 'use strict'; + var __createBinding = + (this && this.__createBinding) || + (Object.create + ? function (o, m, k, k2) { + if (k2 === undefined) k2 = k; + Object.defineProperty(o, k2, { + enumerable: true, + get: function () { + return m[k]; + }, + }); + } + : function (o, m, k, k2) { + if (k2 === undefined) k2 = k; + o[k2] = m[k]; + }); + var __setModuleDefault = + (this && this.__setModuleDefault) || + (Object.create + ? function (o, v) { + Object.defineProperty(o, 'default', { enumerable: true, value: v }); + } + : function (o, v) { + o['default'] = v; + }); + var __importStar = + (this && this.__importStar) || + function (mod) { + if (mod && mod.__esModule) return mod; + var result = {}; + if (mod != null) + for (var k in mod) + if (k !== 'default' && Object.hasOwnProperty.call(mod, k)) + __createBinding(result, mod, k); + __setModuleDefault(result, mod); + return result; + }; + Object.defineProperty(exports, '__esModule', { value: true }); + exports.applyCodecParameters = + exports.getCname = + exports.extractDtlsParameters = + exports.extractRtpCapabilities = + void 0; + const sdpTransform = __importStar(require('sdp-transform')); + function extractRtpCapabilities({ sdpObject }) { + // Map of RtpCodecParameters indexed by payload type. + const codecsMap = new Map(); + // Array of RtpHeaderExtensions. + const headerExtensions = []; + // Whether a m=audio/video section has been already found. + let gotAudio = false; + let gotVideo = false; + for (const m of sdpObject.media) { + const kind = m.type; + switch (kind) { + case 'audio': { + if (gotAudio) continue; + gotAudio = true; + break; + } + case 'video': { + if (gotVideo) continue; + gotVideo = true; + break; + } + default: { + continue; + } + } + // Get codecs. + for (const rtp of m.rtp) { + const codec = { + kind: kind, + mimeType: `${kind}/${rtp.codec}`, + preferredPayloadType: rtp.payload, + clockRate: rtp.rate, + channels: rtp.encoding, + parameters: {}, + rtcpFeedback: [], + }; + codecsMap.set(codec.preferredPayloadType, codec); + } + // Get codec parameters. + for (const fmtp of m.fmtp || []) { + const parameters = sdpTransform.parseParams(fmtp.config); + const codec = codecsMap.get(fmtp.payload); + if (!codec) continue; + // Specials case to convert parameter value to string. + if (parameters && parameters.hasOwnProperty('profile-level-id')) + parameters['profile-level-id'] = String(parameters['profile-level-id']); + codec.parameters = parameters; + } + // Get RTCP feedback for each codec. + for (const fb of m.rtcpFb || []) { + const codec = codecsMap.get(fb.payload); + if (!codec) continue; + const feedback = { + type: fb.type, + parameter: fb.subtype, + }; + if (!feedback.parameter) delete feedback.parameter; + codec.rtcpFeedback.push(feedback); + } + // Get RTP header extensions. + for (const ext of m.ext || []) { + // Ignore encrypted extensions (not yet supported in mediasoup). + if (ext['encrypt-uri']) continue; + const headerExtension = { + kind: kind, + uri: ext.uri, + preferredId: ext.value, + }; + headerExtensions.push(headerExtension); + } + } + const rtpCapabilities = { + codecs: Array.from(codecsMap.values()), + headerExtensions: headerExtensions, + }; + return rtpCapabilities; + } + exports.extractRtpCapabilities = extractRtpCapabilities; + function extractDtlsParameters({ sdpObject }) { + const mediaObject = (sdpObject.media || []).find((m) => m.iceUfrag && m.port !== 0); + if (!mediaObject) throw new Error('no active media section found'); + const fingerprint = mediaObject.fingerprint || sdpObject.fingerprint; + let role; + switch (mediaObject.setup) { + case 'active': + role = 'client'; + break; + case 'passive': + role = 'server'; + break; + case 'actpass': + role = 'auto'; + break; + } + const dtlsParameters = { + role, + fingerprints: [ + { + algorithm: fingerprint.type, + value: fingerprint.hash, + }, + ], + }; + return dtlsParameters; + } + exports.extractDtlsParameters = extractDtlsParameters; + function getCname({ offerMediaObject }) { + const ssrcCnameLine = (offerMediaObject.ssrcs || []).find((line) => line.attribute === 'cname'); + if (!ssrcCnameLine) return ''; + return ssrcCnameLine.value; + } + exports.getCname = getCname; + /** + * Apply codec parameters in the given SDP m= section answer based on the + * given RTP parameters of an offer. + */ + function applyCodecParameters({ offerRtpParameters, answerMediaObject }) { + for (const codec of offerRtpParameters.codecs) { + const mimeType = codec.mimeType.toLowerCase(); + // Avoid parsing codec parameters for unhandled codecs. + if (mimeType !== 'audio/opus') continue; + const rtp = (answerMediaObject.rtp || []).find((r) => r.payload === codec.payloadType); + if (!rtp) continue; + // Just in case. + answerMediaObject.fmtp = answerMediaObject.fmtp || []; + let fmtp = answerMediaObject.fmtp.find((f) => f.payload === codec.payloadType); + if (!fmtp) { + fmtp = { payload: codec.payloadType, config: '' }; + answerMediaObject.fmtp.push(fmtp); + } + const parameters = sdpTransform.parseParams(fmtp.config); + switch (mimeType) { + case 'audio/opus': { + const spropStereo = codec.parameters['sprop-stereo']; + if (spropStereo !== undefined) parameters.stereo = spropStereo ? 1 : 0; + break; + } + } + // Write the codec fmtp.config back. + fmtp.config = ''; + for (const key of Object.keys(parameters)) { + if (fmtp.config) fmtp.config += ';'; + fmtp.config += `${key}=${parameters[key]}`; + } + } + } + exports.applyCodecParameters = applyCodecParameters; + }, + { 'sdp-transform': 44 }, + ], + 33: [ + function (require, module, exports) { + 'use strict'; + Object.defineProperty(exports, '__esModule', { value: true }); + exports.addLegacySimulcast = exports.getRtpEncodings = void 0; + function getRtpEncodings({ offerMediaObject, track }) { + // First media SSRC (or the only one). + let firstSsrc; + const ssrcs = new Set(); + for (const line of offerMediaObject.ssrcs || []) { + if (line.attribute !== 'msid') continue; + const trackId = line.value.split(' ')[1]; + if (trackId === track.id) { + const ssrc = line.id; + ssrcs.add(ssrc); + if (!firstSsrc) firstSsrc = ssrc; + } + } + if (ssrcs.size === 0) + throw new Error(`a=ssrc line with msid information not found [track.id:${track.id}]`); + const ssrcToRtxSsrc = new Map(); + // First assume RTX is used. + for (const line of offerMediaObject.ssrcGroups || []) { + if (line.semantics !== 'FID') continue; + let [ssrc, rtxSsrc] = line.ssrcs.split(/\s+/); + ssrc = Number(ssrc); + rtxSsrc = Number(rtxSsrc); + if (ssrcs.has(ssrc)) { + // Remove both the SSRC and RTX SSRC from the set so later we know that they + // are already handled. + ssrcs.delete(ssrc); + ssrcs.delete(rtxSsrc); + // Add to the map. + ssrcToRtxSsrc.set(ssrc, rtxSsrc); + } + } + // If the set of SSRCs is not empty it means that RTX is not being used, so take + // media SSRCs from there. + for (const ssrc of ssrcs) { + // Add to the map. + ssrcToRtxSsrc.set(ssrc, null); + } + const encodings = []; + for (const [ssrc, rtxSsrc] of ssrcToRtxSsrc) { + const encoding = { ssrc }; + if (rtxSsrc) encoding.rtx = { ssrc: rtxSsrc }; + encodings.push(encoding); + } + return encodings; + } + exports.getRtpEncodings = getRtpEncodings; + /** + * Adds multi-ssrc based simulcast into the given SDP media section offer. + */ + function addLegacySimulcast({ offerMediaObject, track, numStreams }) { + if (numStreams <= 1) throw new TypeError('numStreams must be greater than 1'); + let firstSsrc; + let firstRtxSsrc; + let streamId; + // Get the SSRC. + const ssrcMsidLine = (offerMediaObject.ssrcs || []).find((line) => { + if (line.attribute !== 'msid') return false; + const trackId = line.value.split(' ')[1]; + if (trackId === track.id) { + firstSsrc = line.id; + streamId = line.value.split(' ')[0]; + return true; + } else { + return false; + } + }); + if (!ssrcMsidLine) + throw new Error(`a=ssrc line with msid information not found [track.id:${track.id}]`); + // Get the SSRC for RTX. + (offerMediaObject.ssrcGroups || []).some((line) => { + if (line.semantics !== 'FID') return false; + const ssrcs = line.ssrcs.split(/\s+/); + if (Number(ssrcs[0]) === firstSsrc) { + firstRtxSsrc = Number(ssrcs[1]); + return true; + } else { + return false; + } + }); + const ssrcCnameLine = offerMediaObject.ssrcs.find( + (line) => line.attribute === 'cname' && line.id === firstSsrc, + ); + if (!ssrcCnameLine) + throw new Error(`a=ssrc line with cname information not found [track.id:${track.id}]`); + const cname = ssrcCnameLine.value; + const ssrcs = []; + const rtxSsrcs = []; + for (let i = 0; i < numStreams; ++i) { + ssrcs.push(firstSsrc + i); + if (firstRtxSsrc) rtxSsrcs.push(firstRtxSsrc + i); + } + offerMediaObject.ssrcGroups = offerMediaObject.ssrcGroups || []; + offerMediaObject.ssrcs = offerMediaObject.ssrcs || []; + offerMediaObject.ssrcGroups.push({ + semantics: 'SIM', + ssrcs: ssrcs.join(' '), + }); + for (let i = 0; i < ssrcs.length; ++i) { + const ssrc = ssrcs[i]; + offerMediaObject.ssrcs.push({ + id: ssrc, + attribute: 'cname', + value: cname, + }); + offerMediaObject.ssrcs.push({ + id: ssrc, + attribute: 'msid', + value: `${streamId} ${track.id}`, + }); + } + for (let i = 0; i < rtxSsrcs.length; ++i) { + const ssrc = ssrcs[i]; + const rtxSsrc = rtxSsrcs[i]; + offerMediaObject.ssrcs.push({ + id: rtxSsrc, + attribute: 'cname', + value: cname, + }); + offerMediaObject.ssrcs.push({ + id: rtxSsrc, + attribute: 'msid', + value: `${streamId} ${track.id}`, + }); + offerMediaObject.ssrcGroups.push({ + semantics: 'FID', + ssrcs: `${ssrc} ${rtxSsrc}`, + }); + } + } + exports.addLegacySimulcast = addLegacySimulcast; + }, + {}, + ], + 34: [ + function (require, module, exports) { + 'use strict'; + Object.defineProperty(exports, '__esModule', { value: true }); + exports.addLegacySimulcast = exports.getRtpEncodings = void 0; + function getRtpEncodings({ offerMediaObject }) { + const ssrcs = new Set(); + for (const line of offerMediaObject.ssrcs || []) { + const ssrc = line.id; + ssrcs.add(ssrc); + } + if (ssrcs.size === 0) throw new Error('no a=ssrc lines found'); + const ssrcToRtxSsrc = new Map(); + // First assume RTX is used. + for (const line of offerMediaObject.ssrcGroups || []) { + if (line.semantics !== 'FID') continue; + let [ssrc, rtxSsrc] = line.ssrcs.split(/\s+/); + ssrc = Number(ssrc); + rtxSsrc = Number(rtxSsrc); + if (ssrcs.has(ssrc)) { + // Remove both the SSRC and RTX SSRC from the set so later we know that they + // are already handled. + ssrcs.delete(ssrc); + ssrcs.delete(rtxSsrc); + // Add to the map. + ssrcToRtxSsrc.set(ssrc, rtxSsrc); + } + } + // If the set of SSRCs is not empty it means that RTX is not being used, so take + // media SSRCs from there. + for (const ssrc of ssrcs) { + // Add to the map. + ssrcToRtxSsrc.set(ssrc, null); + } + const encodings = []; + for (const [ssrc, rtxSsrc] of ssrcToRtxSsrc) { + const encoding = { ssrc }; + if (rtxSsrc) encoding.rtx = { ssrc: rtxSsrc }; + encodings.push(encoding); + } + return encodings; + } + exports.getRtpEncodings = getRtpEncodings; + /** + * Adds multi-ssrc based simulcast into the given SDP media section offer. + */ + function addLegacySimulcast({ offerMediaObject, numStreams }) { + if (numStreams <= 1) throw new TypeError('numStreams must be greater than 1'); + // Get the SSRC. + const ssrcMsidLine = (offerMediaObject.ssrcs || []).find((line) => line.attribute === 'msid'); + if (!ssrcMsidLine) throw new Error('a=ssrc line with msid information not found'); + const [streamId, trackId] = ssrcMsidLine.value.split(' '); + const firstSsrc = ssrcMsidLine.id; + let firstRtxSsrc; + // Get the SSRC for RTX. + (offerMediaObject.ssrcGroups || []).some((line) => { + if (line.semantics !== 'FID') return false; + const ssrcs = line.ssrcs.split(/\s+/); + if (Number(ssrcs[0]) === firstSsrc) { + firstRtxSsrc = Number(ssrcs[1]); + return true; + } else { + return false; + } + }); + const ssrcCnameLine = offerMediaObject.ssrcs.find((line) => line.attribute === 'cname'); + if (!ssrcCnameLine) throw new Error('a=ssrc line with cname information not found'); + const cname = ssrcCnameLine.value; + const ssrcs = []; + const rtxSsrcs = []; + for (let i = 0; i < numStreams; ++i) { + ssrcs.push(firstSsrc + i); + if (firstRtxSsrc) rtxSsrcs.push(firstRtxSsrc + i); + } + offerMediaObject.ssrcGroups = []; + offerMediaObject.ssrcs = []; + offerMediaObject.ssrcGroups.push({ + semantics: 'SIM', + ssrcs: ssrcs.join(' '), + }); + for (let i = 0; i < ssrcs.length; ++i) { + const ssrc = ssrcs[i]; + offerMediaObject.ssrcs.push({ + id: ssrc, + attribute: 'cname', + value: cname, + }); + offerMediaObject.ssrcs.push({ + id: ssrc, + attribute: 'msid', + value: `${streamId} ${trackId}`, + }); + } + for (let i = 0; i < rtxSsrcs.length; ++i) { + const ssrc = ssrcs[i]; + const rtxSsrc = rtxSsrcs[i]; + offerMediaObject.ssrcs.push({ + id: rtxSsrc, + attribute: 'cname', + value: cname, + }); + offerMediaObject.ssrcs.push({ + id: rtxSsrc, + attribute: 'msid', + value: `${streamId} ${trackId}`, + }); + offerMediaObject.ssrcGroups.push({ + semantics: 'FID', + ssrcs: `${ssrc} ${rtxSsrc}`, + }); + } + } + exports.addLegacySimulcast = addLegacySimulcast; + }, + {}, + ], + 35: [ + function (require, module, exports) { + 'use strict'; + var __createBinding = + (this && this.__createBinding) || + (Object.create + ? function (o, m, k, k2) { + if (k2 === undefined) k2 = k; + Object.defineProperty(o, k2, { + enumerable: true, + get: function () { + return m[k]; + }, + }); + } + : function (o, m, k, k2) { + if (k2 === undefined) k2 = k; + o[k2] = m[k]; + }); + var __setModuleDefault = + (this && this.__setModuleDefault) || + (Object.create + ? function (o, v) { + Object.defineProperty(o, 'default', { enumerable: true, value: v }); + } + : function (o, v) { + o['default'] = v; + }); + var __importStar = + (this && this.__importStar) || + function (mod) { + if (mod && mod.__esModule) return mod; + var result = {}; + if (mod != null) + for (var k in mod) + if (k !== 'default' && Object.hasOwnProperty.call(mod, k)) + __createBinding(result, mod, k); + __setModuleDefault(result, mod); + return result; + }; + var __importDefault = + (this && this.__importDefault) || + function (mod) { + return mod && mod.__esModule ? mod : { default: mod }; + }; + Object.defineProperty(exports, '__esModule', { value: true }); + exports.debug = exports.detectDevice = exports.Device = exports.version = exports.types = void 0; + const debug_1 = __importDefault(require('debug')); + exports.debug = debug_1.default; + const Device_1 = require('./Device'); + Object.defineProperty(exports, 'Device', { + enumerable: true, + get: function () { + return Device_1.Device; + }, + }); + Object.defineProperty(exports, 'detectDevice', { + enumerable: true, + get: function () { + return Device_1.detectDevice; + }, + }); + const types = __importStar(require('./types')); + exports.types = types; + /** + * Expose mediasoup-client version. + */ + exports.version = '3.6.37'; + /** + * Expose parseScalabilityMode() function. + */ + var scalabilityModes_1 = require('./scalabilityModes'); + Object.defineProperty(exports, 'parseScalabilityMode', { + enumerable: true, + get: function () { + return scalabilityModes_1.parse; + }, + }); + }, + { './Device': 11, './scalabilityModes': 37, './types': 38, debug: 40 }, + ], + 36: [ + function (require, module, exports) { + 'use strict'; + var __createBinding = + (this && this.__createBinding) || + (Object.create + ? function (o, m, k, k2) { + if (k2 === undefined) k2 = k; + Object.defineProperty(o, k2, { + enumerable: true, + get: function () { + return m[k]; + }, + }); + } + : function (o, m, k, k2) { + if (k2 === undefined) k2 = k; + o[k2] = m[k]; + }); + var __setModuleDefault = + (this && this.__setModuleDefault) || + (Object.create + ? function (o, v) { + Object.defineProperty(o, 'default', { enumerable: true, value: v }); + } + : function (o, v) { + o['default'] = v; + }); + var __importStar = + (this && this.__importStar) || + function (mod) { + if (mod && mod.__esModule) return mod; + var result = {}; + if (mod != null) + for (var k in mod) + if (k !== 'default' && Object.hasOwnProperty.call(mod, k)) + __createBinding(result, mod, k); + __setModuleDefault(result, mod); + return result; + }; + Object.defineProperty(exports, '__esModule', { value: true }); + exports.canReceive = + exports.canSend = + exports.generateProbatorRtpParameters = + exports.reduceCodecs = + exports.getSendingRemoteRtpParameters = + exports.getSendingRtpParameters = + exports.getRecvRtpCapabilities = + exports.getExtendedRtpCapabilities = + exports.validateSctpStreamParameters = + exports.validateSctpParameters = + exports.validateNumSctpStreams = + exports.validateSctpCapabilities = + exports.validateRtcpParameters = + exports.validateRtpEncodingParameters = + exports.validateRtpHeaderExtensionParameters = + exports.validateRtpCodecParameters = + exports.validateRtpParameters = + exports.validateRtpHeaderExtension = + exports.validateRtcpFeedback = + exports.validateRtpCodecCapability = + exports.validateRtpCapabilities = + void 0; + const h264 = __importStar(require('h264-profile-level-id')); + const utils = __importStar(require('./utils')); + const RTP_PROBATOR_MID = 'probator'; + const RTP_PROBATOR_SSRC = 1234; + const RTP_PROBATOR_CODEC_PAYLOAD_TYPE = 127; + /** + * Validates RtpCapabilities. It may modify given data by adding missing + * fields with default values. + * It throws if invalid. + */ + function validateRtpCapabilities(caps) { + if (typeof caps !== 'object') throw new TypeError('caps is not an object'); + // codecs is optional. If unset, fill with an empty array. + if (caps.codecs && !Array.isArray(caps.codecs)) throw new TypeError('caps.codecs is not an array'); + else if (!caps.codecs) caps.codecs = []; + for (const codec of caps.codecs) { + validateRtpCodecCapability(codec); + } + // headerExtensions is optional. If unset, fill with an empty array. + if (caps.headerExtensions && !Array.isArray(caps.headerExtensions)) + throw new TypeError('caps.headerExtensions is not an array'); + else if (!caps.headerExtensions) caps.headerExtensions = []; + for (const ext of caps.headerExtensions) { + validateRtpHeaderExtension(ext); + } + } + exports.validateRtpCapabilities = validateRtpCapabilities; + /** + * Validates RtpCodecCapability. It may modify given data by adding missing + * fields with default values. + * It throws if invalid. + */ + function validateRtpCodecCapability(codec) { + const MimeTypeRegex = new RegExp('^(audio|video)/(.+)', 'i'); + if (typeof codec !== 'object') throw new TypeError('codec is not an object'); + // mimeType is mandatory. + if (!codec.mimeType || typeof codec.mimeType !== 'string') + throw new TypeError('missing codec.mimeType'); + const mimeTypeMatch = MimeTypeRegex.exec(codec.mimeType); + if (!mimeTypeMatch) throw new TypeError('invalid codec.mimeType'); + // Just override kind with media component of mimeType. + codec.kind = mimeTypeMatch[1].toLowerCase(); + // preferredPayloadType is optional. + if (codec.preferredPayloadType && typeof codec.preferredPayloadType !== 'number') + throw new TypeError('invalid codec.preferredPayloadType'); + // clockRate is mandatory. + if (typeof codec.clockRate !== 'number') throw new TypeError('missing codec.clockRate'); + // channels is optional. If unset, set it to 1 (just if audio). + if (codec.kind === 'audio') { + if (typeof codec.channels !== 'number') codec.channels = 1; + } else { + delete codec.channels; + } + // parameters is optional. If unset, set it to an empty object. + if (!codec.parameters || typeof codec.parameters !== 'object') codec.parameters = {}; + for (const key of Object.keys(codec.parameters)) { + let value = codec.parameters[key]; + if (value === undefined) { + codec.parameters[key] = ''; + value = ''; + } + if (typeof value !== 'string' && typeof value !== 'number') { + throw new TypeError(`invalid codec parameter [key:${key}s, value:${value}]`); + } + // Specific parameters validation. + if (key === 'apt') { + if (typeof value !== 'number') throw new TypeError('invalid codec apt parameter'); + } + } + // rtcpFeedback is optional. If unset, set it to an empty array. + if (!codec.rtcpFeedback || !Array.isArray(codec.rtcpFeedback)) codec.rtcpFeedback = []; + for (const fb of codec.rtcpFeedback) { + validateRtcpFeedback(fb); + } + } + exports.validateRtpCodecCapability = validateRtpCodecCapability; + /** + * Validates RtcpFeedback. It may modify given data by adding missing + * fields with default values. + * It throws if invalid. + */ + function validateRtcpFeedback(fb) { + if (typeof fb !== 'object') throw new TypeError('fb is not an object'); + // type is mandatory. + if (!fb.type || typeof fb.type !== 'string') throw new TypeError('missing fb.type'); + // parameter is optional. If unset set it to an empty string. + if (!fb.parameter || typeof fb.parameter !== 'string') fb.parameter = ''; + } + exports.validateRtcpFeedback = validateRtcpFeedback; + /** + * Validates RtpHeaderExtension. It may modify given data by adding missing + * fields with default values. + * It throws if invalid. + */ + function validateRtpHeaderExtension(ext) { + if (typeof ext !== 'object') throw new TypeError('ext is not an object'); + // kind is mandatory. + if (ext.kind !== 'audio' && ext.kind !== 'video') throw new TypeError('invalid ext.kind'); + // uri is mandatory. + if (!ext.uri || typeof ext.uri !== 'string') throw new TypeError('missing ext.uri'); + // preferredId is mandatory. + if (typeof ext.preferredId !== 'number') throw new TypeError('missing ext.preferredId'); + // preferredEncrypt is optional. If unset set it to false. + if (ext.preferredEncrypt && typeof ext.preferredEncrypt !== 'boolean') + throw new TypeError('invalid ext.preferredEncrypt'); + else if (!ext.preferredEncrypt) ext.preferredEncrypt = false; + // direction is optional. If unset set it to sendrecv. + if (ext.direction && typeof ext.direction !== 'string') + throw new TypeError('invalid ext.direction'); + else if (!ext.direction) ext.direction = 'sendrecv'; + } + exports.validateRtpHeaderExtension = validateRtpHeaderExtension; + /** + * Validates RtpParameters. It may modify given data by adding missing + * fields with default values. + * It throws if invalid. + */ + function validateRtpParameters(params) { + if (typeof params !== 'object') throw new TypeError('params is not an object'); + // mid is optional. + if (params.mid && typeof params.mid !== 'string') throw new TypeError('params.mid is not a string'); + // codecs is mandatory. + if (!Array.isArray(params.codecs)) throw new TypeError('missing params.codecs'); + for (const codec of params.codecs) { + validateRtpCodecParameters(codec); + } + // headerExtensions is optional. If unset, fill with an empty array. + if (params.headerExtensions && !Array.isArray(params.headerExtensions)) + throw new TypeError('params.headerExtensions is not an array'); + else if (!params.headerExtensions) params.headerExtensions = []; + for (const ext of params.headerExtensions) { + validateRtpHeaderExtensionParameters(ext); + } + // encodings is optional. If unset, fill with an empty array. + if (params.encodings && !Array.isArray(params.encodings)) + throw new TypeError('params.encodings is not an array'); + else if (!params.encodings) params.encodings = []; + for (const encoding of params.encodings) { + validateRtpEncodingParameters(encoding); + } + // rtcp is optional. If unset, fill with an empty object. + if (params.rtcp && typeof params.rtcp !== 'object') + throw new TypeError('params.rtcp is not an object'); + else if (!params.rtcp) params.rtcp = {}; + validateRtcpParameters(params.rtcp); + } + exports.validateRtpParameters = validateRtpParameters; + /** + * Validates RtpCodecParameters. It may modify given data by adding missing + * fields with default values. + * It throws if invalid. + */ + function validateRtpCodecParameters(codec) { + const MimeTypeRegex = new RegExp('^(audio|video)/(.+)', 'i'); + if (typeof codec !== 'object') throw new TypeError('codec is not an object'); + // mimeType is mandatory. + if (!codec.mimeType || typeof codec.mimeType !== 'string') + throw new TypeError('missing codec.mimeType'); + const mimeTypeMatch = MimeTypeRegex.exec(codec.mimeType); + if (!mimeTypeMatch) throw new TypeError('invalid codec.mimeType'); + // payloadType is mandatory. + if (typeof codec.payloadType !== 'number') throw new TypeError('missing codec.payloadType'); + // clockRate is mandatory. + if (typeof codec.clockRate !== 'number') throw new TypeError('missing codec.clockRate'); + const kind = mimeTypeMatch[1].toLowerCase(); + // channels is optional. If unset, set it to 1 (just if audio). + if (kind === 'audio') { + if (typeof codec.channels !== 'number') codec.channels = 1; + } else { + delete codec.channels; + } + // parameters is optional. If unset, set it to an empty object. + if (!codec.parameters || typeof codec.parameters !== 'object') codec.parameters = {}; + for (const key of Object.keys(codec.parameters)) { + let value = codec.parameters[key]; + if (value === undefined) { + codec.parameters[key] = ''; + value = ''; + } + if (typeof value !== 'string' && typeof value !== 'number') { + throw new TypeError(`invalid codec parameter [key:${key}s, value:${value}]`); + } + // Specific parameters validation. + if (key === 'apt') { + if (typeof value !== 'number') throw new TypeError('invalid codec apt parameter'); + } + } + // rtcpFeedback is optional. If unset, set it to an empty array. + if (!codec.rtcpFeedback || !Array.isArray(codec.rtcpFeedback)) codec.rtcpFeedback = []; + for (const fb of codec.rtcpFeedback) { + validateRtcpFeedback(fb); + } + } + exports.validateRtpCodecParameters = validateRtpCodecParameters; + /** + * Validates RtpHeaderExtensionParameteters. It may modify given data by adding missing + * fields with default values. + * It throws if invalid. + */ + function validateRtpHeaderExtensionParameters(ext) { + if (typeof ext !== 'object') throw new TypeError('ext is not an object'); + // uri is mandatory. + if (!ext.uri || typeof ext.uri !== 'string') throw new TypeError('missing ext.uri'); + // id is mandatory. + if (typeof ext.id !== 'number') throw new TypeError('missing ext.id'); + // encrypt is optional. If unset set it to false. + if (ext.encrypt && typeof ext.encrypt !== 'boolean') throw new TypeError('invalid ext.encrypt'); + else if (!ext.encrypt) ext.encrypt = false; + // parameters is optional. If unset, set it to an empty object. + if (!ext.parameters || typeof ext.parameters !== 'object') ext.parameters = {}; + for (const key of Object.keys(ext.parameters)) { + let value = ext.parameters[key]; + if (value === undefined) { + ext.parameters[key] = ''; + value = ''; + } + if (typeof value !== 'string' && typeof value !== 'number') + throw new TypeError('invalid header extension parameter'); + } + } + exports.validateRtpHeaderExtensionParameters = validateRtpHeaderExtensionParameters; + /** + * Validates RtpEncodingParameters. It may modify given data by adding missing + * fields with default values. + * It throws if invalid. + */ + function validateRtpEncodingParameters(encoding) { + if (typeof encoding !== 'object') throw new TypeError('encoding is not an object'); + // ssrc is optional. + if (encoding.ssrc && typeof encoding.ssrc !== 'number') + throw new TypeError('invalid encoding.ssrc'); + // rid is optional. + if (encoding.rid && typeof encoding.rid !== 'string') throw new TypeError('invalid encoding.rid'); + // rtx is optional. + if (encoding.rtx && typeof encoding.rtx !== 'object') { + throw new TypeError('invalid encoding.rtx'); + } else if (encoding.rtx) { + // RTX ssrc is mandatory if rtx is present. + if (typeof encoding.rtx.ssrc !== 'number') throw new TypeError('missing encoding.rtx.ssrc'); + } + // dtx is optional. If unset set it to false. + if (!encoding.dtx || typeof encoding.dtx !== 'boolean') encoding.dtx = false; + // scalabilityMode is optional. + if (encoding.scalabilityMode && typeof encoding.scalabilityMode !== 'string') + throw new TypeError('invalid encoding.scalabilityMode'); + } + exports.validateRtpEncodingParameters = validateRtpEncodingParameters; + /** + * Validates RtcpParameters. It may modify given data by adding missing + * fields with default values. + * It throws if invalid. + */ + function validateRtcpParameters(rtcp) { + if (typeof rtcp !== 'object') throw new TypeError('rtcp is not an object'); + // cname is optional. + if (rtcp.cname && typeof rtcp.cname !== 'string') throw new TypeError('invalid rtcp.cname'); + // reducedSize is optional. If unset set it to true. + if (!rtcp.reducedSize || typeof rtcp.reducedSize !== 'boolean') rtcp.reducedSize = true; + } + exports.validateRtcpParameters = validateRtcpParameters; + /** + * Validates SctpCapabilities. It may modify given data by adding missing + * fields with default values. + * It throws if invalid. + */ + function validateSctpCapabilities(caps) { + if (typeof caps !== 'object') throw new TypeError('caps is not an object'); + // numStreams is mandatory. + if (!caps.numStreams || typeof caps.numStreams !== 'object') + throw new TypeError('missing caps.numStreams'); + validateNumSctpStreams(caps.numStreams); + } + exports.validateSctpCapabilities = validateSctpCapabilities; + /** + * Validates NumSctpStreams. It may modify given data by adding missing + * fields with default values. + * It throws if invalid. + */ + function validateNumSctpStreams(numStreams) { + if (typeof numStreams !== 'object') throw new TypeError('numStreams is not an object'); + // OS is mandatory. + if (typeof numStreams.OS !== 'number') throw new TypeError('missing numStreams.OS'); + // MIS is mandatory. + if (typeof numStreams.MIS !== 'number') throw new TypeError('missing numStreams.MIS'); + } + exports.validateNumSctpStreams = validateNumSctpStreams; + /** + * Validates SctpParameters. It may modify given data by adding missing + * fields with default values. + * It throws if invalid. + */ + function validateSctpParameters(params) { + if (typeof params !== 'object') throw new TypeError('params is not an object'); + // port is mandatory. + if (typeof params.port !== 'number') throw new TypeError('missing params.port'); + // OS is mandatory. + if (typeof params.OS !== 'number') throw new TypeError('missing params.OS'); + // MIS is mandatory. + if (typeof params.MIS !== 'number') throw new TypeError('missing params.MIS'); + // maxMessageSize is mandatory. + if (typeof params.maxMessageSize !== 'number') throw new TypeError('missing params.maxMessageSize'); + } + exports.validateSctpParameters = validateSctpParameters; + /** + * Validates SctpStreamParameters. It may modify given data by adding missing + * fields with default values. + * It throws if invalid. + */ + function validateSctpStreamParameters(params) { + if (typeof params !== 'object') throw new TypeError('params is not an object'); + // streamId is mandatory. + if (typeof params.streamId !== 'number') throw new TypeError('missing params.streamId'); + // ordered is optional. + let orderedGiven = false; + if (typeof params.ordered === 'boolean') orderedGiven = true; + else params.ordered = true; + // maxPacketLifeTime is optional. + if (params.maxPacketLifeTime && typeof params.maxPacketLifeTime !== 'number') + throw new TypeError('invalid params.maxPacketLifeTime'); + // maxRetransmits is optional. + if (params.maxRetransmits && typeof params.maxRetransmits !== 'number') + throw new TypeError('invalid params.maxRetransmits'); + if (params.maxPacketLifeTime && params.maxRetransmits) + throw new TypeError('cannot provide both maxPacketLifeTime and maxRetransmits'); + if (orderedGiven && params.ordered && (params.maxPacketLifeTime || params.maxRetransmits)) { + throw new TypeError('cannot be ordered with maxPacketLifeTime or maxRetransmits'); + } else if (!orderedGiven && (params.maxPacketLifeTime || params.maxRetransmits)) { + params.ordered = false; + } + // label is optional. + if (params.label && typeof params.label !== 'string') throw new TypeError('invalid params.label'); + // protocol is optional. + if (params.protocol && typeof params.protocol !== 'string') + throw new TypeError('invalid params.protocol'); + } + exports.validateSctpStreamParameters = validateSctpStreamParameters; + /** + * Generate extended RTP capabilities for sending and receiving. + */ + function getExtendedRtpCapabilities(localCaps, remoteCaps) { + const extendedRtpCapabilities = { + codecs: [], + headerExtensions: [], + }; + // Match media codecs and keep the order preferred by remoteCaps. + for (const remoteCodec of remoteCaps.codecs || []) { + if (isRtxCodec(remoteCodec)) continue; + const matchingLocalCodec = (localCaps.codecs || []).find((localCodec) => + matchCodecs(localCodec, remoteCodec, { strict: true, modify: true }), + ); + if (!matchingLocalCodec) continue; + const extendedCodec = { + mimeType: matchingLocalCodec.mimeType, + kind: matchingLocalCodec.kind, + clockRate: matchingLocalCodec.clockRate, + channels: matchingLocalCodec.channels, + localPayloadType: matchingLocalCodec.preferredPayloadType, + localRtxPayloadType: undefined, + remotePayloadType: remoteCodec.preferredPayloadType, + remoteRtxPayloadType: undefined, + localParameters: matchingLocalCodec.parameters, + remoteParameters: remoteCodec.parameters, + rtcpFeedback: reduceRtcpFeedback(matchingLocalCodec, remoteCodec), + }; + extendedRtpCapabilities.codecs.push(extendedCodec); + } + // Match RTX codecs. + for (const extendedCodec of extendedRtpCapabilities.codecs) { + const matchingLocalRtxCodec = localCaps.codecs.find( + (localCodec) => + isRtxCodec(localCodec) && localCodec.parameters.apt === extendedCodec.localPayloadType, + ); + const matchingRemoteRtxCodec = remoteCaps.codecs.find( + (remoteCodec) => + isRtxCodec(remoteCodec) && + remoteCodec.parameters.apt === extendedCodec.remotePayloadType, + ); + if (matchingLocalRtxCodec && matchingRemoteRtxCodec) { + extendedCodec.localRtxPayloadType = matchingLocalRtxCodec.preferredPayloadType; + extendedCodec.remoteRtxPayloadType = matchingRemoteRtxCodec.preferredPayloadType; + } + } + // Match header extensions. + for (const remoteExt of remoteCaps.headerExtensions) { + const matchingLocalExt = localCaps.headerExtensions.find((localExt) => + matchHeaderExtensions(localExt, remoteExt), + ); + if (!matchingLocalExt) continue; + const extendedExt = { + kind: remoteExt.kind, + uri: remoteExt.uri, + sendId: matchingLocalExt.preferredId, + recvId: remoteExt.preferredId, + encrypt: matchingLocalExt.preferredEncrypt, + direction: 'sendrecv', + }; + switch (remoteExt.direction) { + case 'sendrecv': + extendedExt.direction = 'sendrecv'; + break; + case 'recvonly': + extendedExt.direction = 'sendonly'; + break; + case 'sendonly': + extendedExt.direction = 'recvonly'; + break; + case 'inactive': + extendedExt.direction = 'inactive'; + break; + } + extendedRtpCapabilities.headerExtensions.push(extendedExt); + } + return extendedRtpCapabilities; + } + exports.getExtendedRtpCapabilities = getExtendedRtpCapabilities; + /** + * Generate RTP capabilities for receiving media based on the given extended + * RTP capabilities. + */ + function getRecvRtpCapabilities(extendedRtpCapabilities) { + const rtpCapabilities = { + codecs: [], + headerExtensions: [], + }; + for (const extendedCodec of extendedRtpCapabilities.codecs) { + const codec = { + mimeType: extendedCodec.mimeType, + kind: extendedCodec.kind, + preferredPayloadType: extendedCodec.remotePayloadType, + clockRate: extendedCodec.clockRate, + channels: extendedCodec.channels, + parameters: extendedCodec.localParameters, + rtcpFeedback: extendedCodec.rtcpFeedback, + }; + rtpCapabilities.codecs.push(codec); + // Add RTX codec. + if (!extendedCodec.remoteRtxPayloadType) continue; + const rtxCodec = { + mimeType: `${extendedCodec.kind}/rtx`, + kind: extendedCodec.kind, + preferredPayloadType: extendedCodec.remoteRtxPayloadType, + clockRate: extendedCodec.clockRate, + parameters: { + apt: extendedCodec.remotePayloadType, + }, + rtcpFeedback: [], + }; + rtpCapabilities.codecs.push(rtxCodec); + // TODO: In the future, we need to add FEC, CN, etc, codecs. + } + for (const extendedExtension of extendedRtpCapabilities.headerExtensions) { + // Ignore RTP extensions not valid for receiving. + if (extendedExtension.direction !== 'sendrecv' && extendedExtension.direction !== 'recvonly') { + continue; + } + const ext = { + kind: extendedExtension.kind, + uri: extendedExtension.uri, + preferredId: extendedExtension.recvId, + preferredEncrypt: extendedExtension.encrypt, + direction: extendedExtension.direction, + }; + rtpCapabilities.headerExtensions.push(ext); + } + return rtpCapabilities; + } + exports.getRecvRtpCapabilities = getRecvRtpCapabilities; + /** + * Generate RTP parameters of the given kind for sending media. + * NOTE: mid, encodings and rtcp fields are left empty. + */ + function getSendingRtpParameters(kind, extendedRtpCapabilities) { + const rtpParameters = { + mid: undefined, + codecs: [], + headerExtensions: [], + encodings: [], + rtcp: {}, + }; + for (const extendedCodec of extendedRtpCapabilities.codecs) { + if (extendedCodec.kind !== kind) continue; + const codec = { + mimeType: extendedCodec.mimeType, + payloadType: extendedCodec.localPayloadType, + clockRate: extendedCodec.clockRate, + channels: extendedCodec.channels, + parameters: extendedCodec.localParameters, + rtcpFeedback: extendedCodec.rtcpFeedback, + }; + rtpParameters.codecs.push(codec); + // Add RTX codec. + if (extendedCodec.localRtxPayloadType) { + const rtxCodec = { + mimeType: `${extendedCodec.kind}/rtx`, + payloadType: extendedCodec.localRtxPayloadType, + clockRate: extendedCodec.clockRate, + parameters: { + apt: extendedCodec.localPayloadType, + }, + rtcpFeedback: [], + }; + rtpParameters.codecs.push(rtxCodec); + } + } + for (const extendedExtension of extendedRtpCapabilities.headerExtensions) { + // Ignore RTP extensions of a different kind and those not valid for sending. + if ( + (extendedExtension.kind && extendedExtension.kind !== kind) || + (extendedExtension.direction !== 'sendrecv' && extendedExtension.direction !== 'sendonly') + ) { + continue; + } + const ext = { + uri: extendedExtension.uri, + id: extendedExtension.sendId, + encrypt: extendedExtension.encrypt, + parameters: {}, + }; + rtpParameters.headerExtensions.push(ext); + } + return rtpParameters; + } + exports.getSendingRtpParameters = getSendingRtpParameters; + /** + * Generate RTP parameters of the given kind suitable for the remote SDP answer. + */ + function getSendingRemoteRtpParameters(kind, extendedRtpCapabilities) { + const rtpParameters = { + mid: undefined, + codecs: [], + headerExtensions: [], + encodings: [], + rtcp: {}, + }; + for (const extendedCodec of extendedRtpCapabilities.codecs) { + if (extendedCodec.kind !== kind) continue; + const codec = { + mimeType: extendedCodec.mimeType, + payloadType: extendedCodec.localPayloadType, + clockRate: extendedCodec.clockRate, + channels: extendedCodec.channels, + parameters: extendedCodec.remoteParameters, + rtcpFeedback: extendedCodec.rtcpFeedback, + }; + rtpParameters.codecs.push(codec); + // Add RTX codec. + if (extendedCodec.localRtxPayloadType) { + const rtxCodec = { + mimeType: `${extendedCodec.kind}/rtx`, + payloadType: extendedCodec.localRtxPayloadType, + clockRate: extendedCodec.clockRate, + parameters: { + apt: extendedCodec.localPayloadType, + }, + rtcpFeedback: [], + }; + rtpParameters.codecs.push(rtxCodec); + } + } + for (const extendedExtension of extendedRtpCapabilities.headerExtensions) { + // Ignore RTP extensions of a different kind and those not valid for sending. + if ( + (extendedExtension.kind && extendedExtension.kind !== kind) || + (extendedExtension.direction !== 'sendrecv' && extendedExtension.direction !== 'sendonly') + ) { + continue; + } + const ext = { + uri: extendedExtension.uri, + id: extendedExtension.sendId, + encrypt: extendedExtension.encrypt, + parameters: {}, + }; + rtpParameters.headerExtensions.push(ext); + } + // Reduce codecs' RTCP feedback. Use Transport-CC if available, REMB otherwise. + if ( + rtpParameters.headerExtensions.some( + (ext) => + ext.uri === 'http://www.ietf.org/id/draft-holmer-rmcat-transport-wide-cc-extensions-01', + ) + ) { + for (const codec of rtpParameters.codecs) { + codec.rtcpFeedback = (codec.rtcpFeedback || []).filter((fb) => fb.type !== 'goog-remb'); + } + } else if ( + rtpParameters.headerExtensions.some( + (ext) => ext.uri === 'http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time', + ) + ) { + for (const codec of rtpParameters.codecs) { + codec.rtcpFeedback = (codec.rtcpFeedback || []).filter((fb) => fb.type !== 'transport-cc'); + } + } else { + for (const codec of rtpParameters.codecs) { + codec.rtcpFeedback = (codec.rtcpFeedback || []).filter( + (fb) => fb.type !== 'transport-cc' && fb.type !== 'goog-remb', + ); + } + } + return rtpParameters; + } + exports.getSendingRemoteRtpParameters = getSendingRemoteRtpParameters; + /** + * Reduce given codecs by returning an array of codecs "compatible" with the + * given capability codec. If no capability codec is given, take the first + * one(s). + * + * Given codecs must be generated by ortc.getSendingRtpParameters() or + * ortc.getSendingRemoteRtpParameters(). + * + * The returned array of codecs also include a RTX codec if available. + */ + function reduceCodecs(codecs, capCodec) { + const filteredCodecs = []; + // If no capability codec is given, take the first one (and RTX). + if (!capCodec) { + filteredCodecs.push(codecs[0]); + if (isRtxCodec(codecs[1])) filteredCodecs.push(codecs[1]); + } + // Otherwise look for a compatible set of codecs. + else { + for (let idx = 0; idx < codecs.length; ++idx) { + if (matchCodecs(codecs[idx], capCodec)) { + filteredCodecs.push(codecs[idx]); + if (isRtxCodec(codecs[idx + 1])) filteredCodecs.push(codecs[idx + 1]); + break; + } + } + if (filteredCodecs.length === 0) throw new TypeError('no matching codec found'); + } + return filteredCodecs; + } + exports.reduceCodecs = reduceCodecs; + /** + * Create RTP parameters for a Consumer for the RTP probator. + */ + function generateProbatorRtpParameters(videoRtpParameters) { + // Clone given reference video RTP parameters. + videoRtpParameters = utils.clone(videoRtpParameters, {}); + // This may throw. + validateRtpParameters(videoRtpParameters); + const rtpParameters = { + mid: RTP_PROBATOR_MID, + codecs: [], + headerExtensions: [], + encodings: [{ ssrc: RTP_PROBATOR_SSRC }], + rtcp: { cname: 'probator' }, + }; + rtpParameters.codecs.push(videoRtpParameters.codecs[0]); + rtpParameters.codecs[0].payloadType = RTP_PROBATOR_CODEC_PAYLOAD_TYPE; + rtpParameters.headerExtensions = videoRtpParameters.headerExtensions; + return rtpParameters; + } + exports.generateProbatorRtpParameters = generateProbatorRtpParameters; + /** + * Whether media can be sent based on the given RTP capabilities. + */ + function canSend(kind, extendedRtpCapabilities) { + return extendedRtpCapabilities.codecs.some((codec) => codec.kind === kind); + } + exports.canSend = canSend; + /** + * Whether the given RTP parameters can be received with the given RTP + * capabilities. + */ + function canReceive(rtpParameters, extendedRtpCapabilities) { + // This may throw. + validateRtpParameters(rtpParameters); + if (rtpParameters.codecs.length === 0) return false; + const firstMediaCodec = rtpParameters.codecs[0]; + return extendedRtpCapabilities.codecs.some( + (codec) => codec.remotePayloadType === firstMediaCodec.payloadType, + ); + } + exports.canReceive = canReceive; + function isRtxCodec(codec) { + if (!codec) return false; + return /.+\/rtx$/i.test(codec.mimeType); + } + function matchCodecs(aCodec, bCodec, { strict = false, modify = false } = {}) { + const aMimeType = aCodec.mimeType.toLowerCase(); + const bMimeType = bCodec.mimeType.toLowerCase(); + if (aMimeType !== bMimeType) return false; + if (aCodec.clockRate !== bCodec.clockRate) return false; + if (aCodec.channels !== bCodec.channels) return false; + // Per codec special checks. + switch (aMimeType) { + case 'video/h264': { + const aPacketizationMode = aCodec.parameters['packetization-mode'] || 0; + const bPacketizationMode = bCodec.parameters['packetization-mode'] || 0; + if (aPacketizationMode !== bPacketizationMode) return false; + // If strict matching check profile-level-id. + if (strict) { + if (!h264.isSameProfile(aCodec.parameters, bCodec.parameters)) return false; + let selectedProfileLevelId; + try { + selectedProfileLevelId = h264.generateProfileLevelIdForAnswer( + aCodec.parameters, + bCodec.parameters, + ); + } catch (error) { + return false; + } + if (modify) { + if (selectedProfileLevelId) { + aCodec.parameters['profile-level-id'] = selectedProfileLevelId; + bCodec.parameters['profile-level-id'] = selectedProfileLevelId; + } else { + delete aCodec.parameters['profile-level-id']; + delete bCodec.parameters['profile-level-id']; + } + } + } + break; + } + case 'video/vp9': { + // If strict matching check profile-id. + if (strict) { + const aProfileId = aCodec.parameters['profile-id'] || 0; + const bProfileId = bCodec.parameters['profile-id'] || 0; + if (aProfileId !== bProfileId) return false; + } + break; + } + } + return true; + } + function matchHeaderExtensions(aExt, bExt) { + if (aExt.kind && bExt.kind && aExt.kind !== bExt.kind) return false; + if (aExt.uri !== bExt.uri) return false; + return true; + } + function reduceRtcpFeedback(codecA, codecB) { + const reducedRtcpFeedback = []; + for (const aFb of codecA.rtcpFeedback || []) { + const matchingBFb = (codecB.rtcpFeedback || []).find( + (bFb) => + bFb.type === aFb.type && + (bFb.parameter === aFb.parameter || (!bFb.parameter && !aFb.parameter)), + ); + if (matchingBFb) reducedRtcpFeedback.push(matchingBFb); + } + return reducedRtcpFeedback; + } + }, + { './utils': 39, 'h264-profile-level-id': 4 }, + ], + 37: [ + function (require, module, exports) { + 'use strict'; + Object.defineProperty(exports, '__esModule', { value: true }); + exports.parse = void 0; + const ScalabilityModeRegex = new RegExp('^[LS]([1-9]\\d{0,1})T([1-9]\\d{0,1})'); + function parse(scalabilityMode) { + const match = ScalabilityModeRegex.exec(scalabilityMode || ''); + if (match) { + return { + spatialLayers: Number(match[1]), + temporalLayers: Number(match[2]), + }; + } else { + return { + spatialLayers: 1, + temporalLayers: 1, + }; + } + } + exports.parse = parse; + }, + {}, + ], + 38: [ + function (require, module, exports) { + 'use strict'; + var __createBinding = + (this && this.__createBinding) || + (Object.create + ? function (o, m, k, k2) { + if (k2 === undefined) k2 = k; + Object.defineProperty(o, k2, { + enumerable: true, + get: function () { + return m[k]; + }, + }); + } + : function (o, m, k, k2) { + if (k2 === undefined) k2 = k; + o[k2] = m[k]; + }); + var __exportStar = + (this && this.__exportStar) || + function (m, exports) { + for (var p in m) + if (p !== 'default' && !exports.hasOwnProperty(p)) __createBinding(exports, m, p); + }; + Object.defineProperty(exports, '__esModule', { value: true }); + __exportStar(require('./Device'), exports); + __exportStar(require('./Transport'), exports); + __exportStar(require('./Producer'), exports); + __exportStar(require('./Consumer'), exports); + __exportStar(require('./DataProducer'), exports); + __exportStar(require('./DataConsumer'), exports); + __exportStar(require('./RtpParameters'), exports); + __exportStar(require('./SctpParameters'), exports); + __exportStar(require('./handlers/HandlerInterface'), exports); + __exportStar(require('./errors'), exports); + }, + { + './Consumer': 8, + './DataConsumer': 9, + './DataProducer': 10, + './Device': 11, + './Producer': 14, + './RtpParameters': 15, + './SctpParameters': 16, + './Transport': 17, + './errors': 18, + './handlers/HandlerInterface': 25, + }, + ], + 39: [ + function (require, module, exports) { + 'use strict'; + Object.defineProperty(exports, '__esModule', { value: true }); + exports.generateRandomNumber = exports.clone = void 0; + /** + * Clones the given data. + */ + function clone(data, defaultValue) { + if (typeof data === 'undefined') return defaultValue; + return JSON.parse(JSON.stringify(data)); + } + exports.clone = clone; + /** + * Generates a random positive integer. + */ + function generateRandomNumber() { + return Math.round(Math.random() * 10000000); + } + exports.generateRandomNumber = generateRandomNumber; + }, + {}, + ], + 40: [ + function (require, module, exports) { + arguments[4][5][0].apply(exports, arguments); + }, + { './common': 41, _process: 48, dup: 5 }, + ], + 41: [ + function (require, module, exports) { + arguments[4][6][0].apply(exports, arguments); + }, + { dup: 6, ms: 42 }, + ], + 42: [ + function (require, module, exports) { + arguments[4][7][0].apply(exports, arguments); + }, + { dup: 7 }, + ], + 43: [ + function (require, module, exports) { + var grammar = (module.exports = { + v: [ + { + name: 'version', + reg: /^(\d*)$/, + }, + ], + o: [ + { + // o=- 20518 0 IN IP4 203.0.113.1 + // NB: sessionId will be a String in most cases because it is huge + name: 'origin', + reg: /^(\S*) (\d*) (\d*) (\S*) IP(\d) (\S*)/, + names: ['username', 'sessionId', 'sessionVersion', 'netType', 'ipVer', 'address'], + format: '%s %s %d %s IP%d %s', + }, + ], + // default parsing of these only (though some of these feel outdated) + s: [{ name: 'name' }], + i: [{ name: 'description' }], + u: [{ name: 'uri' }], + e: [{ name: 'email' }], + p: [{ name: 'phone' }], + z: [{ name: 'timezones' }], // TODO: this one can actually be parsed properly... + r: [{ name: 'repeats' }], // TODO: this one can also be parsed properly + // k: [{}], // outdated thing ignored + t: [ + { + // t=0 0 + name: 'timing', + reg: /^(\d*) (\d*)/, + names: ['start', 'stop'], + format: '%d %d', + }, + ], + c: [ + { + // c=IN IP4 10.47.197.26 + name: 'connection', + reg: /^IN IP(\d) (\S*)/, + names: ['version', 'ip'], + format: 'IN IP%d %s', + }, + ], + b: [ + { + // b=AS:4000 + push: 'bandwidth', + reg: /^(TIAS|AS|CT|RR|RS):(\d*)/, + names: ['type', 'limit'], + format: '%s:%s', + }, + ], + m: [ + { + // m=video 51744 RTP/AVP 126 97 98 34 31 + // NB: special - pushes to session + // TODO: rtp/fmtp should be filtered by the payloads found here? + reg: /^(\w*) (\d*) ([\w/]*)(?: (.*))?/, + names: ['type', 'port', 'protocol', 'payloads'], + format: '%s %d %s %s', + }, + ], + a: [ + { + // a=rtpmap:110 opus/48000/2 + push: 'rtp', + reg: /^rtpmap:(\d*) ([\w\-.]*)(?:\s*\/(\d*)(?:\s*\/(\S*))?)?/, + names: ['payload', 'codec', 'rate', 'encoding'], + format: function (o) { + return o.encoding ? 'rtpmap:%d %s/%s/%s' : o.rate ? 'rtpmap:%d %s/%s' : 'rtpmap:%d %s'; + }, + }, + { + // a=fmtp:108 profile-level-id=24;object=23;bitrate=64000 + // a=fmtp:111 minptime=10; useinbandfec=1 + push: 'fmtp', + reg: /^fmtp:(\d*) ([\S| ]*)/, + names: ['payload', 'config'], + format: 'fmtp:%d %s', + }, + { + // a=control:streamid=0 + name: 'control', + reg: /^control:(.*)/, + format: 'control:%s', + }, + { + // a=rtcp:65179 IN IP4 193.84.77.194 + name: 'rtcp', + reg: /^rtcp:(\d*)(?: (\S*) IP(\d) (\S*))?/, + names: ['port', 'netType', 'ipVer', 'address'], + format: function (o) { + return o.address != null ? 'rtcp:%d %s IP%d %s' : 'rtcp:%d'; + }, + }, + { + // a=rtcp-fb:98 trr-int 100 + push: 'rtcpFbTrrInt', + reg: /^rtcp-fb:(\*|\d*) trr-int (\d*)/, + names: ['payload', 'value'], + format: 'rtcp-fb:%s trr-int %d', + }, + { + // a=rtcp-fb:98 nack rpsi + push: 'rtcpFb', + reg: /^rtcp-fb:(\*|\d*) ([\w-_]*)(?: ([\w-_]*))?/, + names: ['payload', 'type', 'subtype'], + format: function (o) { + return o.subtype != null ? 'rtcp-fb:%s %s %s' : 'rtcp-fb:%s %s'; + }, + }, + { + // a=extmap:2 urn:ietf:params:rtp-hdrext:toffset + // a=extmap:1/recvonly URI-gps-string + // a=extmap:3 urn:ietf:params:rtp-hdrext:encrypt urn:ietf:params:rtp-hdrext:smpte-tc 25@600/24 + push: 'ext', + reg: /^extmap:(\d+)(?:\/(\w+))?(?: (urn:ietf:params:rtp-hdrext:encrypt))? (\S*)(?: (\S*))?/, + names: ['value', 'direction', 'encrypt-uri', 'uri', 'config'], + format: function (o) { + return ( + 'extmap:%d' + + (o.direction ? '/%s' : '%v') + + (o['encrypt-uri'] ? ' %s' : '%v') + + ' %s' + + (o.config ? ' %s' : '') + ); + }, + }, + { + // a=extmap-allow-mixed + name: 'extmapAllowMixed', + reg: /^(extmap-allow-mixed)/, + }, + { + // a=crypto:1 AES_CM_128_HMAC_SHA1_80 inline:PS1uQCVeeCFCanVmcjkpPywjNWhcYD0mXXtxaVBR|2^20|1:32 + push: 'crypto', + reg: /^crypto:(\d*) ([\w_]*) (\S*)(?: (\S*))?/, + names: ['id', 'suite', 'config', 'sessionConfig'], + format: function (o) { + return o.sessionConfig != null ? 'crypto:%d %s %s %s' : 'crypto:%d %s %s'; + }, + }, + { + // a=setup:actpass + name: 'setup', + reg: /^setup:(\w*)/, + format: 'setup:%s', + }, + { + // a=connection:new + name: 'connectionType', + reg: /^connection:(new|existing)/, + format: 'connection:%s', + }, + { + // a=mid:1 + name: 'mid', + reg: /^mid:([^\s]*)/, + format: 'mid:%s', + }, + { + // a=msid:0c8b064d-d807-43b4-b434-f92a889d8587 98178685-d409-46e0-8e16-7ef0db0db64a + name: 'msid', + reg: /^msid:(.*)/, + format: 'msid:%s', + }, + { + // a=ptime:20 + name: 'ptime', + reg: /^ptime:(\d*(?:\.\d*)*)/, + format: 'ptime:%d', + }, + { + // a=maxptime:60 + name: 'maxptime', + reg: /^maxptime:(\d*(?:\.\d*)*)/, + format: 'maxptime:%d', + }, + { + // a=sendrecv + name: 'direction', + reg: /^(sendrecv|recvonly|sendonly|inactive)/, + }, + { + // a=ice-lite + name: 'icelite', + reg: /^(ice-lite)/, + }, + { + // a=ice-ufrag:F7gI + name: 'iceUfrag', + reg: /^ice-ufrag:(\S*)/, + format: 'ice-ufrag:%s', + }, + { + // a=ice-pwd:x9cml/YzichV2+XlhiMu8g + name: 'icePwd', + reg: /^ice-pwd:(\S*)/, + format: 'ice-pwd:%s', + }, + { + // a=fingerprint:SHA-1 00:11:22:33:44:55:66:77:88:99:AA:BB:CC:DD:EE:FF:00:11:22:33 + name: 'fingerprint', + reg: /^fingerprint:(\S*) (\S*)/, + names: ['type', 'hash'], + format: 'fingerprint:%s %s', + }, + { + // a=candidate:0 1 UDP 2113667327 203.0.113.1 54400 typ host + // a=candidate:1162875081 1 udp 2113937151 192.168.34.75 60017 typ host generation 0 network-id 3 network-cost 10 + // a=candidate:3289912957 2 udp 1845501695 193.84.77.194 60017 typ srflx raddr 192.168.34.75 rport 60017 generation 0 network-id 3 network-cost 10 + // a=candidate:229815620 1 tcp 1518280447 192.168.150.19 60017 typ host tcptype active generation 0 network-id 3 network-cost 10 + // a=candidate:3289912957 2 tcp 1845501695 193.84.77.194 60017 typ srflx raddr 192.168.34.75 rport 60017 tcptype passive generation 0 network-id 3 network-cost 10 + push: 'candidates', + reg: /^candidate:(\S*) (\d*) (\S*) (\d*) (\S*) (\d*) typ (\S*)(?: raddr (\S*) rport (\d*))?(?: tcptype (\S*))?(?: generation (\d*))?(?: network-id (\d*))?(?: network-cost (\d*))?/, + names: [ + 'foundation', + 'component', + 'transport', + 'priority', + 'ip', + 'port', + 'type', + 'raddr', + 'rport', + 'tcptype', + 'generation', + 'network-id', + 'network-cost', + ], + format: function (o) { + var str = 'candidate:%s %d %s %d %s %d typ %s'; + + str += o.raddr != null ? ' raddr %s rport %d' : '%v%v'; + + // NB: candidate has three optional chunks, so %void middles one if it's missing + str += o.tcptype != null ? ' tcptype %s' : '%v'; + + if (o.generation != null) { + str += ' generation %d'; + } + + str += o['network-id'] != null ? ' network-id %d' : '%v'; + str += o['network-cost'] != null ? ' network-cost %d' : '%v'; + return str; + }, + }, + { + // a=end-of-candidates (keep after the candidates line for readability) + name: 'endOfCandidates', + reg: /^(end-of-candidates)/, + }, + { + // a=remote-candidates:1 203.0.113.1 54400 2 203.0.113.1 54401 ... + name: 'remoteCandidates', + reg: /^remote-candidates:(.*)/, + format: 'remote-candidates:%s', + }, + { + // a=ice-options:google-ice + name: 'iceOptions', + reg: /^ice-options:(\S*)/, + format: 'ice-options:%s', + }, + { + // a=ssrc:2566107569 cname:t9YU8M1UxTF8Y1A1 + push: 'ssrcs', + reg: /^ssrc:(\d*) ([\w_-]*)(?::(.*))?/, + names: ['id', 'attribute', 'value'], + format: function (o) { + var str = 'ssrc:%d'; + if (o.attribute != null) { + str += ' %s'; + if (o.value != null) { + str += ':%s'; + } + } + return str; + }, + }, + { + // a=ssrc-group:FEC 1 2 + // a=ssrc-group:FEC-FR 3004364195 1080772241 + push: 'ssrcGroups', + // token-char = %x21 / %x23-27 / %x2A-2B / %x2D-2E / %x30-39 / %x41-5A / %x5E-7E + reg: /^ssrc-group:([\x21\x23\x24\x25\x26\x27\x2A\x2B\x2D\x2E\w]*) (.*)/, + names: ['semantics', 'ssrcs'], + format: 'ssrc-group:%s %s', + }, + { + // a=msid-semantic: WMS Jvlam5X3SX1OP6pn20zWogvaKJz5Hjf9OnlV + name: 'msidSemantic', + reg: /^msid-semantic:\s?(\w*) (\S*)/, + names: ['semantic', 'token'], + format: 'msid-semantic: %s %s', // space after ':' is not accidental + }, + { + // a=group:BUNDLE audio video + push: 'groups', + reg: /^group:(\w*) (.*)/, + names: ['type', 'mids'], + format: 'group:%s %s', + }, + { + // a=rtcp-mux + name: 'rtcpMux', + reg: /^(rtcp-mux)/, + }, + { + // a=rtcp-rsize + name: 'rtcpRsize', + reg: /^(rtcp-rsize)/, + }, + { + // a=sctpmap:5000 webrtc-datachannel 1024 + name: 'sctpmap', + reg: /^sctpmap:([\w_/]*) (\S*)(?: (\S*))?/, + names: ['sctpmapNumber', 'app', 'maxMessageSize'], + format: function (o) { + return o.maxMessageSize != null ? 'sctpmap:%s %s %s' : 'sctpmap:%s %s'; + }, + }, + { + // a=x-google-flag:conference + name: 'xGoogleFlag', + reg: /^x-google-flag:([^\s]*)/, + format: 'x-google-flag:%s', + }, + { + // a=rid:1 send max-width=1280;max-height=720;max-fps=30;depend=0 + push: 'rids', + reg: /^rid:([\d\w]+) (\w+)(?: ([\S| ]*))?/, + names: ['id', 'direction', 'params'], + format: function (o) { + return o.params ? 'rid:%s %s %s' : 'rid:%s %s'; + }, + }, + { + // a=imageattr:97 send [x=800,y=640,sar=1.1,q=0.6] [x=480,y=320] recv [x=330,y=250] + // a=imageattr:* send [x=800,y=640] recv * + // a=imageattr:100 recv [x=320,y=240] + push: 'imageattrs', + reg: new RegExp( + // a=imageattr:97 + '^imageattr:(\\d+|\\*)' + + // send [x=800,y=640,sar=1.1,q=0.6] [x=480,y=320] + '[\\s\\t]+(send|recv)[\\s\\t]+(\\*|\\[\\S+\\](?:[\\s\\t]+\\[\\S+\\])*)' + + // recv [x=330,y=250] + '(?:[\\s\\t]+(recv|send)[\\s\\t]+(\\*|\\[\\S+\\](?:[\\s\\t]+\\[\\S+\\])*))?', + ), + names: ['pt', 'dir1', 'attrs1', 'dir2', 'attrs2'], + format: function (o) { + return 'imageattr:%s %s %s' + (o.dir2 ? ' %s %s' : ''); + }, + }, + { + // a=simulcast:send 1,2,3;~4,~5 recv 6;~7,~8 + // a=simulcast:recv 1;4,5 send 6;7 + name: 'simulcast', + reg: new RegExp( + // a=simulcast: + '^simulcast:' + + // send 1,2,3;~4,~5 + '(send|recv) ([a-zA-Z0-9\\-_~;,]+)' + + // space + recv 6;~7,~8 + '(?:\\s?(send|recv) ([a-zA-Z0-9\\-_~;,]+))?' + + // end + '$', + ), + names: ['dir1', 'list1', 'dir2', 'list2'], + format: function (o) { + return 'simulcast:%s %s' + (o.dir2 ? ' %s %s' : ''); + }, + }, + { + // old simulcast draft 03 (implemented by Firefox) + // https://tools.ietf.org/html/draft-ietf-mmusic-sdp-simulcast-03 + // a=simulcast: recv pt=97;98 send pt=97 + // a=simulcast: send rid=5;6;7 paused=6,7 + name: 'simulcast_03', + reg: /^simulcast:[\s\t]+([\S+\s\t]+)$/, + names: ['value'], + format: 'simulcast: %s', + }, + { + // a=framerate:25 + // a=framerate:29.97 + name: 'framerate', + reg: /^framerate:(\d+(?:$|\.\d+))/, + format: 'framerate:%s', + }, + { + // RFC4570 + // a=source-filter: incl IN IP4 239.5.2.31 10.1.15.5 + name: 'sourceFilter', + reg: /^source-filter: *(excl|incl) (\S*) (IP4|IP6|\*) (\S*) (.*)/, + names: ['filterMode', 'netType', 'addressTypes', 'destAddress', 'srcList'], + format: 'source-filter: %s %s %s %s %s', + }, + { + // a=bundle-only + name: 'bundleOnly', + reg: /^(bundle-only)/, + }, + { + // a=label:1 + name: 'label', + reg: /^label:(.+)/, + format: 'label:%s', + }, + { + // RFC version 26 for SCTP over DTLS + // https://tools.ietf.org/html/draft-ietf-mmusic-sctp-sdp-26#section-5 + name: 'sctpPort', + reg: /^sctp-port:(\d+)$/, + format: 'sctp-port:%s', + }, + { + // RFC version 26 for SCTP over DTLS + // https://tools.ietf.org/html/draft-ietf-mmusic-sctp-sdp-26#section-6 + name: 'maxMessageSize', + reg: /^max-message-size:(\d+)$/, + format: 'max-message-size:%s', + }, + { + // RFC7273 + // a=ts-refclk:ptp=IEEE1588-2008:39-A7-94-FF-FE-07-CB-D0:37 + push: 'tsRefClocks', + reg: /^ts-refclk:([^\s=]*)(?:=(\S*))?/, + names: ['clksrc', 'clksrcExt'], + format: function (o) { + return 'ts-refclk:%s' + (o.clksrcExt != null ? '=%s' : ''); + }, + }, + { + // RFC7273 + // a=mediaclk:direct=963214424 + name: 'mediaClk', + reg: /^mediaclk:(?:id=(\S*))? *([^\s=]*)(?:=(\S*))?(?: *rate=(\d+)\/(\d+))?/, + names: ['id', 'mediaClockName', 'mediaClockValue', 'rateNumerator', 'rateDenominator'], + format: function (o) { + var str = 'mediaclk:'; + str += o.id != null ? 'id=%s %s' : '%v%s'; + str += o.mediaClockValue != null ? '=%s' : ''; + str += o.rateNumerator != null ? ' rate=%s' : ''; + str += o.rateDenominator != null ? '/%s' : ''; + return str; + }, + }, + { + // a=keywds:keywords + name: 'keywords', + reg: /^keywds:(.+)$/, + format: 'keywds:%s', + }, + { + // a=content:main + name: 'content', + reg: /^content:(.+)/, + format: 'content:%s', + }, + // BFCP https://tools.ietf.org/html/rfc4583 + { + // a=floorctrl:c-s + name: 'bfcpFloorCtrl', + reg: /^floorctrl:(c-only|s-only|c-s)/, + format: 'floorctrl:%s', + }, + { + // a=confid:1 + name: 'bfcpConfId', + reg: /^confid:(\d+)/, + format: 'confid:%s', + }, + { + // a=userid:1 + name: 'bfcpUserId', + reg: /^userid:(\d+)/, + format: 'userid:%s', + }, + { + // a=floorid:1 + name: 'bfcpFloorId', + reg: /^floorid:(.+) (?:m-stream|mstrm):(.+)/, + names: ['id', 'mStream'], + format: 'floorid:%s mstrm:%s', + }, + { + // any a= that we don't understand is kept verbatim on media.invalid + push: 'invalid', + names: ['value'], + }, + ], + }); + + // set sensible defaults to avoid polluting the grammar with boring details + Object.keys(grammar).forEach(function (key) { + var objs = grammar[key]; + objs.forEach(function (obj) { + if (!obj.reg) { + obj.reg = /(.*)/; + } + if (!obj.format) { + obj.format = '%s'; + } + }); + }); + }, + {}, + ], + 44: [ + function (require, module, exports) { + var parser = require('./parser'); + var writer = require('./writer'); + + exports.write = writer; + exports.parse = parser.parse; + exports.parseParams = parser.parseParams; + exports.parseFmtpConfig = parser.parseFmtpConfig; // Alias of parseParams(). + exports.parsePayloads = parser.parsePayloads; + exports.parseRemoteCandidates = parser.parseRemoteCandidates; + exports.parseImageAttributes = parser.parseImageAttributes; + exports.parseSimulcastStreamList = parser.parseSimulcastStreamList; + }, + { './parser': 45, './writer': 46 }, + ], + 45: [ + function (require, module, exports) { + var toIntIfInt = function (v) { + return String(Number(v)) === v ? Number(v) : v; + }; + + var attachProperties = function (match, location, names, rawName) { + if (rawName && !names) { + location[rawName] = toIntIfInt(match[1]); + } else { + for (var i = 0; i < names.length; i += 1) { + if (match[i + 1] != null) { + location[names[i]] = toIntIfInt(match[i + 1]); + } + } + } + }; + + var parseReg = function (obj, location, content) { + var needsBlank = obj.name && obj.names; + if (obj.push && !location[obj.push]) { + location[obj.push] = []; + } else if (needsBlank && !location[obj.name]) { + location[obj.name] = {}; + } + var keyLocation = obj.push + ? {} // blank object that will be pushed + : needsBlank + ? location[obj.name] + : location; // otherwise, named location or root + + attachProperties(content.match(obj.reg), keyLocation, obj.names, obj.name); + + if (obj.push) { + location[obj.push].push(keyLocation); + } + }; + + var grammar = require('./grammar'); + var validLine = RegExp.prototype.test.bind(/^([a-z])=(.*)/); + + exports.parse = function (sdp) { + var session = {}, + media = [], + location = session; // points at where properties go under (one of the above) + + // parse lines we understand + sdp.split(/(\r\n|\r|\n)/) + .filter(validLine) + .forEach(function (l) { + var type = l[0]; + var content = l.slice(2); + if (type === 'm') { + media.push({ rtp: [], fmtp: [] }); + location = media[media.length - 1]; // point at latest media line + } + + for (var j = 0; j < (grammar[type] || []).length; j += 1) { + var obj = grammar[type][j]; + if (obj.reg.test(content)) { + return parseReg(obj, location, content); + } + } + }); + + session.media = media; // link it up + return session; + }; + + var paramReducer = function (acc, expr) { + var s = expr.split(/=(.+)/, 2); + if (s.length === 2) { + acc[s[0]] = toIntIfInt(s[1]); + } else if (s.length === 1 && expr.length > 1) { + acc[s[0]] = undefined; + } + return acc; + }; + + exports.parseParams = function (str) { + return str.split(/;\s?/).reduce(paramReducer, {}); + }; + + // For backward compatibility - alias will be removed in 3.0.0 + exports.parseFmtpConfig = exports.parseParams; + + exports.parsePayloads = function (str) { + return str.toString().split(' ').map(Number); + }; + + exports.parseRemoteCandidates = function (str) { + var candidates = []; + var parts = str.split(' ').map(toIntIfInt); + for (var i = 0; i < parts.length; i += 3) { + candidates.push({ + component: parts[i], + ip: parts[i + 1], + port: parts[i + 2], + }); + } + return candidates; + }; + + exports.parseImageAttributes = function (str) { + return str.split(' ').map(function (item) { + return item + .substring(1, item.length - 1) + .split(',') + .reduce(paramReducer, {}); + }); + }; + + exports.parseSimulcastStreamList = function (str) { + return str.split(';').map(function (stream) { + return stream.split(',').map(function (format) { + var scid, + paused = false; + + if (format[0] !== '~') { + scid = toIntIfInt(format); + } else { + scid = toIntIfInt(format.substring(1, format.length)); + paused = true; + } + + return { + scid: scid, + paused: paused, + }; + }); + }); + }; + }, + { './grammar': 43 }, + ], + 46: [ + function (require, module, exports) { + var grammar = require('./grammar'); + + // customized util.format - discards excess arguments and can void middle ones + var formatRegExp = /%[sdv%]/g; + var format = function (formatStr) { + var i = 1; + var args = arguments; + var len = args.length; + return formatStr.replace(formatRegExp, function (x) { + if (i >= len) { + return x; // missing argument + } + var arg = args[i]; + i += 1; + switch (x) { + case '%%': + return '%'; + case '%s': + return String(arg); + case '%d': + return Number(arg); + case '%v': + return ''; + } + }); + // NB: we discard excess arguments - they are typically undefined from makeLine + }; + + var makeLine = function (type, obj, location) { + var str = + obj.format instanceof Function + ? obj.format(obj.push ? location : location[obj.name]) + : obj.format; + + var args = [type + '=' + str]; + if (obj.names) { + for (var i = 0; i < obj.names.length; i += 1) { + var n = obj.names[i]; + if (obj.name) { + args.push(location[obj.name][n]); + } else { + // for mLine and push attributes + args.push(location[obj.names[i]]); + } + } + } else { + args.push(location[obj.name]); + } + return format.apply(null, args); + }; + + // RFC specified order + // TODO: extend this with all the rest + var defaultOuterOrder = ['v', 'o', 's', 'i', 'u', 'e', 'p', 'c', 'b', 't', 'r', 'z', 'a']; + var defaultInnerOrder = ['i', 'c', 'b', 'a']; + + module.exports = function (session, opts) { + opts = opts || {}; + // ensure certain properties exist + if (session.version == null) { + session.version = 0; // 'v=0' must be there (only defined version atm) + } + if (session.name == null) { + session.name = ' '; // 's= ' must be there if no meaningful name set + } + session.media.forEach(function (mLine) { + if (mLine.payloads == null) { + mLine.payloads = ''; + } + }); + + var outerOrder = opts.outerOrder || defaultOuterOrder; + var innerOrder = opts.innerOrder || defaultInnerOrder; + var sdp = []; + + // loop through outerOrder for matching properties on session + outerOrder.forEach(function (type) { + grammar[type].forEach(function (obj) { + if (obj.name in session && session[obj.name] != null) { + sdp.push(makeLine(type, obj, session)); + } else if (obj.push in session && session[obj.push] != null) { + session[obj.push].forEach(function (el) { + sdp.push(makeLine(type, obj, el)); + }); + } + }); + }); + + // then for each media line, follow the innerOrder + session.media.forEach(function (mLine) { + sdp.push(makeLine('m', grammar.m[0], mLine)); + + innerOrder.forEach(function (type) { + grammar[type].forEach(function (obj) { + if (obj.name in mLine && mLine[obj.name] != null) { + sdp.push(makeLine(type, obj, mLine)); + } else if (obj.push in mLine && mLine[obj.push] != null) { + mLine[obj.push].forEach(function (el) { + sdp.push(makeLine(type, obj, el)); + }); + } + }); + }); + }); + + return sdp.join('\r\n') + '\r\n'; + }; + }, + { './grammar': 43 }, + ], + 47: [ + function (require, module, exports) { + // Copyright Joyent, Inc. and other Node contributors. + // + // Permission is hereby granted, free of charge, to any person obtaining a + // copy of this software and associated documentation files (the + // "Software"), to deal in the Software without restriction, including + // without limitation the rights to use, copy, modify, merge, publish, + // distribute, sublicense, and/or sell copies of the Software, and to permit + // persons to whom the Software is furnished to do so, subject to the + // following conditions: + // + // The above copyright notice and this permission notice shall be included + // in all copies or substantial portions of the Software. + // + // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS + // OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + // MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN + // NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, + // DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR + // OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE + // USE OR OTHER DEALINGS IN THE SOFTWARE. + + 'use strict'; + + var R = typeof Reflect === 'object' ? Reflect : null; + var ReflectApply = + R && typeof R.apply === 'function' + ? R.apply + : function ReflectApply(target, receiver, args) { + return Function.prototype.apply.call(target, receiver, args); + }; + + var ReflectOwnKeys; + if (R && typeof R.ownKeys === 'function') { + ReflectOwnKeys = R.ownKeys; + } else if (Object.getOwnPropertySymbols) { + ReflectOwnKeys = function ReflectOwnKeys(target) { + return Object.getOwnPropertyNames(target).concat(Object.getOwnPropertySymbols(target)); + }; + } else { + ReflectOwnKeys = function ReflectOwnKeys(target) { + return Object.getOwnPropertyNames(target); + }; + } + + function ProcessEmitWarning(warning) { + if (console && console.warn) console.warn(warning); + } + + var NumberIsNaN = + Number.isNaN || + function NumberIsNaN(value) { + return value !== value; + }; + + function EventEmitter() { + EventEmitter.init.call(this); + } + module.exports = EventEmitter; + module.exports.once = once; + + // Backwards-compat with node 0.10.x + EventEmitter.EventEmitter = EventEmitter; + + EventEmitter.prototype._events = undefined; + EventEmitter.prototype._eventsCount = 0; + EventEmitter.prototype._maxListeners = undefined; + + // By default EventEmitters will print a warning if more than 10 listeners are + // added to it. This is a useful default which helps finding memory leaks. + var defaultMaxListeners = 10; + + function checkListener(listener) { + if (typeof listener !== 'function') { + throw new TypeError( + 'The "listener" argument must be of type Function. Received type ' + typeof listener, + ); + } + } + + Object.defineProperty(EventEmitter, 'defaultMaxListeners', { + enumerable: true, + get: function () { + return defaultMaxListeners; + }, + set: function (arg) { + if (typeof arg !== 'number' || arg < 0 || NumberIsNaN(arg)) { + throw new RangeError( + 'The value of "defaultMaxListeners" is out of range. It must be a non-negative number. Received ' + + arg + + '.', + ); + } + defaultMaxListeners = arg; + }, + }); + + EventEmitter.init = function () { + if (this._events === undefined || this._events === Object.getPrototypeOf(this)._events) { + this._events = Object.create(null); + this._eventsCount = 0; + } + + this._maxListeners = this._maxListeners || undefined; + }; + + // Obviously not all Emitters should be limited to 10. This function allows + // that to be increased. Set to zero for unlimited. + EventEmitter.prototype.setMaxListeners = function setMaxListeners(n) { + if (typeof n !== 'number' || n < 0 || NumberIsNaN(n)) { + throw new RangeError( + 'The value of "n" is out of range. It must be a non-negative number. Received ' + n + '.', + ); + } + this._maxListeners = n; + return this; + }; + + function _getMaxListeners(that) { + if (that._maxListeners === undefined) return EventEmitter.defaultMaxListeners; + return that._maxListeners; + } + + EventEmitter.prototype.getMaxListeners = function getMaxListeners() { + return _getMaxListeners(this); + }; + + EventEmitter.prototype.emit = function emit(type) { + var args = []; + for (var i = 1; i < arguments.length; i++) args.push(arguments[i]); + var doError = type === 'error'; + + var events = this._events; + if (events !== undefined) doError = doError && events.error === undefined; + else if (!doError) return false; + + // If there is no 'error' event listener then throw. + if (doError) { + var er; + if (args.length > 0) er = args[0]; + if (er instanceof Error) { + // Note: The comments on the `throw` lines are intentional, they show + // up in Node's output if this results in an unhandled exception. + throw er; // Unhandled 'error' event + } + // At least give some kind of context to the user + var err = new Error('Unhandled error.' + (er ? ' (' + er.message + ')' : '')); + err.context = er; + throw err; // Unhandled 'error' event + } + + var handler = events[type]; + + if (handler === undefined) return false; + + if (typeof handler === 'function') { + ReflectApply(handler, this, args); + } else { + var len = handler.length; + var listeners = arrayClone(handler, len); + for (var i = 0; i < len; ++i) ReflectApply(listeners[i], this, args); + } + + return true; + }; + + function _addListener(target, type, listener, prepend) { + var m; + var events; + var existing; + + checkListener(listener); + + events = target._events; + if (events === undefined) { + events = target._events = Object.create(null); + target._eventsCount = 0; + } else { + // To avoid recursion in the case that type === "newListener"! Before + // adding it to the listeners, first emit "newListener". + if (events.newListener !== undefined) { + target.emit('newListener', type, listener.listener ? listener.listener : listener); + + // Re-assign `events` because a newListener handler could have caused the + // this._events to be assigned to a new object + events = target._events; + } + existing = events[type]; + } + + if (existing === undefined) { + // Optimize the case of one listener. Don't need the extra array object. + existing = events[type] = listener; + ++target._eventsCount; + } else { + if (typeof existing === 'function') { + // Adding the second element, need to change to array. + existing = events[type] = prepend ? [listener, existing] : [existing, listener]; + // If we've already got an array, just append. + } else if (prepend) { + existing.unshift(listener); + } else { + existing.push(listener); + } + + // Check for listener leak + m = _getMaxListeners(target); + if (m > 0 && existing.length > m && !existing.warned) { + existing.warned = true; + // No error code for this since it is a Warning + // eslint-disable-next-line no-restricted-syntax + var w = new Error( + 'Possible EventEmitter memory leak detected. ' + + existing.length + + ' ' + + String(type) + + ' listeners ' + + 'added. Use emitter.setMaxListeners() to ' + + 'increase limit', + ); + w.name = 'MaxListenersExceededWarning'; + w.emitter = target; + w.type = type; + w.count = existing.length; + ProcessEmitWarning(w); + } + } + + return target; + } + + EventEmitter.prototype.addListener = function addListener(type, listener) { + return _addListener(this, type, listener, false); + }; + + EventEmitter.prototype.on = EventEmitter.prototype.addListener; + + EventEmitter.prototype.prependListener = function prependListener(type, listener) { + return _addListener(this, type, listener, true); + }; + + function onceWrapper() { + if (!this.fired) { + this.target.removeListener(this.type, this.wrapFn); + this.fired = true; + if (arguments.length === 0) return this.listener.call(this.target); + return this.listener.apply(this.target, arguments); + } + } + + function _onceWrap(target, type, listener) { + var state = { fired: false, wrapFn: undefined, target: target, type: type, listener: listener }; + var wrapped = onceWrapper.bind(state); + wrapped.listener = listener; + state.wrapFn = wrapped; + return wrapped; + } + + EventEmitter.prototype.once = function once(type, listener) { + checkListener(listener); + this.on(type, _onceWrap(this, type, listener)); + return this; + }; + + EventEmitter.prototype.prependOnceListener = function prependOnceListener(type, listener) { + checkListener(listener); + this.prependListener(type, _onceWrap(this, type, listener)); + return this; + }; + + // Emits a 'removeListener' event if and only if the listener was removed. + EventEmitter.prototype.removeListener = function removeListener(type, listener) { + var list, events, position, i, originalListener; + + checkListener(listener); + + events = this._events; + if (events === undefined) return this; + + list = events[type]; + if (list === undefined) return this; + + if (list === listener || list.listener === listener) { + if (--this._eventsCount === 0) this._events = Object.create(null); + else { + delete events[type]; + if (events.removeListener) this.emit('removeListener', type, list.listener || listener); + } + } else if (typeof list !== 'function') { + position = -1; + + for (i = list.length - 1; i >= 0; i--) { + if (list[i] === listener || list[i].listener === listener) { + originalListener = list[i].listener; + position = i; + break; + } + } + + if (position < 0) return this; + + if (position === 0) list.shift(); + else { + spliceOne(list, position); + } + + if (list.length === 1) events[type] = list[0]; + + if (events.removeListener !== undefined) + this.emit('removeListener', type, originalListener || listener); + } + + return this; + }; + + EventEmitter.prototype.off = EventEmitter.prototype.removeListener; + + EventEmitter.prototype.removeAllListeners = function removeAllListeners(type) { + var listeners, events, i; + + events = this._events; + if (events === undefined) return this; + + // not listening for removeListener, no need to emit + if (events.removeListener === undefined) { + if (arguments.length === 0) { + this._events = Object.create(null); + this._eventsCount = 0; + } else if (events[type] !== undefined) { + if (--this._eventsCount === 0) this._events = Object.create(null); + else delete events[type]; + } + return this; + } + + // emit removeListener for all listeners on all events + if (arguments.length === 0) { + var keys = Object.keys(events); + var key; + for (i = 0; i < keys.length; ++i) { + key = keys[i]; + if (key === 'removeListener') continue; + this.removeAllListeners(key); + } + this.removeAllListeners('removeListener'); + this._events = Object.create(null); + this._eventsCount = 0; + return this; + } + + listeners = events[type]; + + if (typeof listeners === 'function') { + this.removeListener(type, listeners); + } else if (listeners !== undefined) { + // LIFO order + for (i = listeners.length - 1; i >= 0; i--) { + this.removeListener(type, listeners[i]); + } + } + + return this; + }; + + function _listeners(target, type, unwrap) { + var events = target._events; + + if (events === undefined) return []; + + var evlistener = events[type]; + if (evlistener === undefined) return []; + + if (typeof evlistener === 'function') + return unwrap ? [evlistener.listener || evlistener] : [evlistener]; + + return unwrap ? unwrapListeners(evlistener) : arrayClone(evlistener, evlistener.length); + } + + EventEmitter.prototype.listeners = function listeners(type) { + return _listeners(this, type, true); + }; + + EventEmitter.prototype.rawListeners = function rawListeners(type) { + return _listeners(this, type, false); + }; + + EventEmitter.listenerCount = function (emitter, type) { + if (typeof emitter.listenerCount === 'function') { + return emitter.listenerCount(type); + } else { + return listenerCount.call(emitter, type); + } + }; + + EventEmitter.prototype.listenerCount = listenerCount; + function listenerCount(type) { + var events = this._events; + + if (events !== undefined) { + var evlistener = events[type]; + + if (typeof evlistener === 'function') { + return 1; + } else if (evlistener !== undefined) { + return evlistener.length; + } + } + + return 0; + } + + EventEmitter.prototype.eventNames = function eventNames() { + return this._eventsCount > 0 ? ReflectOwnKeys(this._events) : []; + }; + + function arrayClone(arr, n) { + var copy = new Array(n); + for (var i = 0; i < n; ++i) copy[i] = arr[i]; + return copy; + } + + function spliceOne(list, index) { + for (; index + 1 < list.length; index++) list[index] = list[index + 1]; + list.pop(); + } + + function unwrapListeners(arr) { + var ret = new Array(arr.length); + for (var i = 0; i < ret.length; ++i) { + ret[i] = arr[i].listener || arr[i]; + } + return ret; + } + + function once(emitter, name) { + return new Promise(function (resolve, reject) { + function errorListener(err) { + emitter.removeListener(name, resolver); + reject(err); + } + + function resolver() { + if (typeof emitter.removeListener === 'function') { + emitter.removeListener('error', errorListener); + } + resolve([].slice.call(arguments)); + } + + eventTargetAgnosticAddListener(emitter, name, resolver, { once: true }); + if (name !== 'error') { + addErrorHandlerIfEventEmitter(emitter, errorListener, { once: true }); + } + }); + } + + function addErrorHandlerIfEventEmitter(emitter, handler, flags) { + if (typeof emitter.on === 'function') { + eventTargetAgnosticAddListener(emitter, 'error', handler, flags); + } + } + + function eventTargetAgnosticAddListener(emitter, name, listener, flags) { + if (typeof emitter.on === 'function') { + if (flags.once) { + emitter.once(name, listener); + } else { + emitter.on(name, listener); + } + } else if (typeof emitter.addEventListener === 'function') { + // EventTarget does not have `error` event semantics like Node + // EventEmitters, we do not listen for `error` events here. + emitter.addEventListener(name, function wrapListener(arg) { + // IE does not have builtin `{ once: true }` support so we + // have to do it manually. + if (flags.once) { + emitter.removeEventListener(name, wrapListener); + } + listener(arg); + }); + } else { + throw new TypeError( + 'The "emitter" argument must be of type EventEmitter. Received type ' + typeof emitter, + ); + } + } + }, + {}, + ], + 48: [ + function (require, module, exports) { + // shim for using process in browser + var process = (module.exports = {}); + + // cached from whatever global is present so that test runners that stub it + // don't break things. But we need to wrap it in a try catch in case it is + // wrapped in strict mode code which doesn't define any globals. It's inside a + // function because try/catches deoptimize in certain engines. + + var cachedSetTimeout; + var cachedClearTimeout; + + function defaultSetTimout() { + throw new Error('setTimeout has not been defined'); + } + function defaultClearTimeout() { + throw new Error('clearTimeout has not been defined'); + } + (function () { + try { + if (typeof setTimeout === 'function') { + cachedSetTimeout = setTimeout; + } else { + cachedSetTimeout = defaultSetTimout; + } + } catch (e) { + cachedSetTimeout = defaultSetTimout; + } + try { + if (typeof clearTimeout === 'function') { + cachedClearTimeout = clearTimeout; + } else { + cachedClearTimeout = defaultClearTimeout; + } + } catch (e) { + cachedClearTimeout = defaultClearTimeout; + } + })(); + function runTimeout(fun) { + if (cachedSetTimeout === setTimeout) { + //normal enviroments in sane situations + return setTimeout(fun, 0); + } + // if setTimeout wasn't available but was latter defined + if ((cachedSetTimeout === defaultSetTimout || !cachedSetTimeout) && setTimeout) { + cachedSetTimeout = setTimeout; + return setTimeout(fun, 0); + } + try { + // when when somebody has screwed with setTimeout but no I.E. maddness + return cachedSetTimeout(fun, 0); + } catch (e) { + try { + // When we are in I.E. but the script has been evaled so I.E. doesn't trust the global object when called normally + return cachedSetTimeout.call(null, fun, 0); + } catch (e) { + // same as above but when it's a version of I.E. that must have the global object for 'this', hopfully our context correct otherwise it will throw a global error + return cachedSetTimeout.call(this, fun, 0); + } + } + } + function runClearTimeout(marker) { + if (cachedClearTimeout === clearTimeout) { + //normal enviroments in sane situations + return clearTimeout(marker); + } + // if clearTimeout wasn't available but was latter defined + if ((cachedClearTimeout === defaultClearTimeout || !cachedClearTimeout) && clearTimeout) { + cachedClearTimeout = clearTimeout; + return clearTimeout(marker); + } + try { + // when when somebody has screwed with setTimeout but no I.E. maddness + return cachedClearTimeout(marker); + } catch (e) { + try { + // When we are in I.E. but the script has been evaled so I.E. doesn't trust the global object when called normally + return cachedClearTimeout.call(null, marker); + } catch (e) { + // same as above but when it's a version of I.E. that must have the global object for 'this', hopfully our context correct otherwise it will throw a global error. + // Some versions of I.E. have different rules for clearTimeout vs setTimeout + return cachedClearTimeout.call(this, marker); + } + } + } + var queue = []; + var draining = false; + var currentQueue; + var queueIndex = -1; + + function cleanUpNextTick() { + if (!draining || !currentQueue) { + return; + } + draining = false; + if (currentQueue.length) { + queue = currentQueue.concat(queue); + } else { + queueIndex = -1; + } + if (queue.length) { + drainQueue(); + } + } + + function drainQueue() { + if (draining) { + return; + } + var timeout = runTimeout(cleanUpNextTick); + draining = true; + + var len = queue.length; + while (len) { + currentQueue = queue; + queue = []; + while (++queueIndex < len) { + if (currentQueue) { + currentQueue[queueIndex].run(); + } + } + queueIndex = -1; + len = queue.length; + } + currentQueue = null; + draining = false; + runClearTimeout(timeout); + } + + process.nextTick = function (fun) { + var args = new Array(arguments.length - 1); + if (arguments.length > 1) { + for (var i = 1; i < arguments.length; i++) { + args[i - 1] = arguments[i]; + } + } + queue.push(new Item(fun, args)); + if (queue.length === 1 && !draining) { + runTimeout(drainQueue); + } + }; + + // v8 likes predictible objects + function Item(fun, array) { + this.fun = fun; + this.array = array; + } + Item.prototype.run = function () { + this.fun.apply(null, this.array); + }; + process.title = 'browser'; + process.browser = true; + process.env = {}; + process.argv = []; + process.version = ''; // empty string to avoid regexp issues + process.versions = {}; + + function noop() {} + + process.on = noop; + process.addListener = noop; + process.once = noop; + process.off = noop; + process.removeListener = noop; + process.removeAllListeners = noop; + process.emit = noop; + process.prependListener = noop; + process.prependOnceListener = noop; + + process.listeners = function (name) { + return []; + }; + + process.binding = function (name) { + throw new Error('process.binding is not supported'); + }; + + process.cwd = function () { + return '/'; + }; + process.chdir = function (dir) { + throw new Error('process.chdir is not supported'); + }; + process.umask = function () { + return 0; + }; + }, + {}, + ], + }, + {}, + [1], +); diff --git a/public/modules/MediasoupClientCompile.js b/public/modules/MediasoupClientCompile.js new file mode 100644 index 00000000..d55a4a0b --- /dev/null +++ b/public/modules/MediasoupClientCompile.js @@ -0,0 +1,2 @@ +const client = require('mediasoup-client'); +window.mediasoupClient = client; diff --git a/public/newroom.html b/public/newroom.html new file mode 100755 index 00000000..53ca0ac8 --- /dev/null +++ b/public/newroom.html @@ -0,0 +1,159 @@ + + + + + + MiroTalk SFU - Create your Room name and start the new call. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+
+
+
+
+

+ Pick name.
+ Share URL.
+ Start conference. +

+

+ Each room has its own disposable URL. Just pick a room name and share your custom + URL. It's really that easy. +

+
+
+ + + +
+
+
+
+
+
+
+
+
+
+
+
+
+
+ +
+ +
+
+
+

+ Pick a room name.
+ How about this one? +

+
+
+ + + +
+
+
+
+
+
+ + +
+ + + + + diff --git a/public/permission.html b/public/permission.html new file mode 100755 index 00000000..8e225e6c --- /dev/null +++ b/public/permission.html @@ -0,0 +1,139 @@ + + + + + + MiroTalk SFU - Allow Video or Audio access to join in the Room. + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+
+
+
+
+

Access denied.

+

+

+ This app will not work without camera or microphone access. Please Try again and + allow it. +

+ +
+ TRY AGAIN +
+
+
+ + + +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ + +
+ + + + diff --git a/public/robots.txt b/public/robots.txt new file mode 100644 index 00000000..21fed79c --- /dev/null +++ b/public/robots.txt @@ -0,0 +1,3 @@ +# Allow crawling of all content +User-agent: * +Disallow: \ No newline at end of file diff --git a/public/sounds/joined.wav b/public/sounds/joined.wav new file mode 100644 index 00000000..16ceb80a Binary files /dev/null and b/public/sounds/joined.wav differ diff --git a/public/sounds/left.wav b/public/sounds/left.wav new file mode 100644 index 00000000..0c9cee0a Binary files /dev/null and b/public/sounds/left.wav differ diff --git a/public/sounds/message.wav b/public/sounds/message.wav new file mode 100644 index 00000000..445f9270 Binary files /dev/null and b/public/sounds/message.wav differ diff --git a/public/sounds/open.wav b/public/sounds/open.wav new file mode 100644 index 00000000..e20340e0 Binary files /dev/null and b/public/sounds/open.wav differ diff --git a/public/sounds/recStart.wav b/public/sounds/recStart.wav new file mode 100644 index 00000000..166d3b8f Binary files /dev/null and b/public/sounds/recStart.wav differ diff --git a/public/sounds/recStop.wav b/public/sounds/recStop.wav new file mode 100644 index 00000000..3d97f5d9 Binary files /dev/null and b/public/sounds/recStop.wav differ diff --git a/src/Logger.js b/src/Logger.js new file mode 100644 index 00000000..0f5cf8e9 --- /dev/null +++ b/src/Logger.js @@ -0,0 +1,25 @@ +'use strict'; + +module.exports = class Logger { + constructor(appName, debugOn = true) { + if (appName) this.appName = appName; + else this.appName = 'mirotalk'; + this.debugOn = debugOn; + } + + debug(msg, op = '') { + if (this.debugOn === false) return; + let dataTime = new Date().toISOString().replace(/T/, ' ').replace(/Z/, ''); + console.log('[' + dataTime + '] [' + this.appName + '] ' + msg, op); + } + + warn(msg, op = '') { + let dataTime = new Date().toISOString().replace(/T/, ' ').replace(/Z/, ''); + console.warn('[' + dataTime + '] [' + this.appName + '] ' + msg, op); + } + + error(msg, op = '') { + let dataTime = new Date().toISOString().replace(/T/, ' ').replace(/Z/, ''); + console.error('[' + dataTime + '] [' + this.appName + '] ' + msg, op); + } +}; diff --git a/src/Peer.js b/src/Peer.js new file mode 100644 index 00000000..40cdf1e3 --- /dev/null +++ b/src/Peer.js @@ -0,0 +1,133 @@ +'use strict'; + +const Logger = require('./Logger'); +const log = new Logger('Peer'); + +module.exports = class Peer { + constructor(socket_id, data) { + this.id = socket_id; + this.peer_name = data.peer_name; + this.peer_audio = data.peer_audio; + this.peer_video = data.peer_video; + this.peer_info = data.peer_info; + this.transports = new Map(); + this.consumers = new Map(); + this.producers = new Map(); + } + + // #################################################### + // TRANSPORT + // #################################################### + + addTransport(transport) { + this.transports.set(transport.id, transport); + } + + async connectTransport(transport_id, dtlsParameters) { + if (!this.transports.has(transport_id)) return; + + await this.transports.get(transport_id).connect({ + dtlsParameters: dtlsParameters, + }); + } + + close() { + this.transports.forEach((transport) => transport.close()); + } + + // #################################################### + // PRODUCER + // #################################################### + + getProducer(producer_id) { + return this.producers.get(producer_id); + } + + async createProducer(producerTransportId, rtpParameters, kind) { + let producer = await this.transports.get(producerTransportId).produce({ + kind, + rtpParameters, + }); + + this.producers.set(producer.id, producer); + + producer.on( + 'transportclose', + function () { + log.debug('Producer transport close', { + peer_name: this.peer_name, + consumer_id: producer.id, + }); + producer.close(); + this.producers.delete(producer.id); + }.bind(this), + ); + + return producer; + } + + closeProducer(producer_id) { + try { + this.producers.get(producer_id).close(); + } catch (ex) { + log.warn('Close Producer', ex); + } + this.producers.delete(producer_id); + } + + // #################################################### + // CONSUMER + // #################################################### + + async createConsumer(consumer_transport_id, producer_id, rtpCapabilities) { + let consumerTransport = this.transports.get(consumer_transport_id); + let consumer = null; + + try { + consumer = await consumerTransport.consume({ + producerId: producer_id, + rtpCapabilities, + paused: false, + }); + } catch (error) { + console.error('Consume failed', error); + return; + } + + if (consumer.type === 'simulcast') { + await consumer.setPreferredLayers({ + spatialLayer: 2, + temporalLayer: 2, + }); + } + + this.consumers.set(consumer.id, consumer); + + consumer.on( + 'transportclose', + function () { + log.debug('Consumer transport close', { + peer_name: this.peer_name, + consumer_id: consumer.id, + }); + this.consumers.delete(consumer.id); + }.bind(this), + ); + + return { + consumer, + params: { + producerId: producer_id, + id: consumer.id, + kind: consumer.kind, + rtpParameters: consumer.rtpParameters, + type: consumer.type, + producerPaused: consumer.producerPaused, + }, + }; + } + + removeConsumer(consumer_id) { + this.consumers.delete(consumer_id); + } +}; diff --git a/src/Room.js b/src/Room.js new file mode 100644 index 00000000..52fd3f07 --- /dev/null +++ b/src/Room.js @@ -0,0 +1,203 @@ +'use strict'; + +const config = require('./config'); +const Logger = require('./Logger'); +const log = new Logger('Room'); + +module.exports = class Room { + constructor(room_id, worker, io) { + this.id = room_id; + this.worker = worker; + this.router = null; + this.io = io; + this.peers = new Map(); + this.createTheRouter(); + } + + // #################################################### + // ROUTER + // #################################################### + + createTheRouter() { + const mediaCodecs = config.mediasoup.router.mediaCodecs; + this.worker + .createRouter({ + mediaCodecs, + }) + .then( + function (router) { + this.router = router; + }.bind(this), + ); + } + + getRtpCapabilities() { + return this.router.rtpCapabilities; + } + + // #################################################### + // PEERS + // #################################################### + + addPeer(peer) { + this.peers.set(peer.id, peer); + } + + getPeers() { + return this.peers; + } + + toJson() { + return { + id: this.id, + peers: JSON.stringify([...this.peers]), + }; + } + + getProducerListForPeer() { + let producerList = []; + this.peers.forEach((peer) => { + peer.producers.forEach((producer) => { + producerList.push({ + producer_id: producer.id, + peer_name: peer.peer_name, + peer_info: peer.peer_info, + }); + }); + }); + return producerList; + } + + async connectPeerTransport(socket_id, transport_id, dtlsParameters) { + if (!this.peers.has(socket_id)) return; + await this.peers.get(socket_id).connectTransport(transport_id, dtlsParameters); + } + + async removePeer(socket_id) { + this.peers.get(socket_id).close(); + this.peers.delete(socket_id); + } + + // #################################################### + // WEBRTC TRANSPORT + // #################################################### + + async createWebRtcTransport(socket_id) { + const { maxIncomingBitrate, initialAvailableOutgoingBitrate } = config.mediasoup.webRtcTransport; + + const transport = await this.router.createWebRtcTransport({ + listenIps: config.mediasoup.webRtcTransport.listenIps, + enableUdp: true, + enableTcp: true, + preferUdp: true, + initialAvailableOutgoingBitrate, + }); + + if (maxIncomingBitrate) { + try { + await transport.setMaxIncomingBitrate(maxIncomingBitrate); + } catch (error) {} + } + + transport.on( + 'dtlsstatechange', + function (dtlsState) { + if (dtlsState === 'closed') { + log.debug('Transport close', { peer_name: this.peers.get(socket_id).peer_name }); + transport.close(); + } + }.bind(this), + ); + + transport.on('close', () => { + log.debug('Transport close', { peer_name: this.peers.get(socket_id).peer_name }); + }); + + log.debug('Adding transport', { transportId: transport.id }); + this.peers.get(socket_id).addTransport(transport); + return { + params: { + id: transport.id, + iceParameters: transport.iceParameters, + iceCandidates: transport.iceCandidates, + dtlsParameters: transport.dtlsParameters, + }, + }; + } + + // #################################################### + // PRODUCE + // #################################################### + + async produce(socket_id, producerTransportId, rtpParameters, kind) { + return new Promise( + async function (resolve, reject) { + let producer = await this.peers.get(socket_id).createProducer(producerTransportId, rtpParameters, kind); + resolve(producer.id); + this.broadCast(socket_id, 'newProducers', [ + { + producer_id: producer.id, + producer_socket_id: socket_id, + peer_name: this.peers.get(socket_id).peer_name, + }, + ]); + }.bind(this), + ); + } + + // #################################################### + // CONSUME + // #################################################### + + async consume(socket_id, consumer_transport_id, producer_id, rtpCapabilities) { + if ( + !this.router.canConsume({ + producerId: producer_id, + rtpCapabilities, + }) + ) { + log.error('can not consume'); + return; + } + + let { consumer, params } = await this.peers + .get(socket_id) + .createConsumer(consumer_transport_id, producer_id, rtpCapabilities); + + consumer.on( + 'producerclose', + function () { + log.debug('Consumer closed due to producerclose event', { + peer_name: this.peers.get(socket_id).peer_name, + consumer_id: consumer.id, + }); + this.peers.get(socket_id).removeConsumer(consumer.id); + + // tell client consumer is dead + this.io.to(socket_id).emit('consumerClosed', { + consumer_id: consumer.id, + }); + }.bind(this), + ); + + return params; + } + + closeProducer(socket_id, producer_id) { + this.peers.get(socket_id).closeProducer(producer_id); + } + + // #################################################### + // SENDER + // #################################################### + + broadCast(socket_id, peer_name, data) { + for (let otherID of Array.from(this.peers.keys()).filter((id) => id !== socket_id)) { + this.send(otherID, peer_name, data); + } + } + + send(socket_id, peer_name, data) { + this.io.to(socket_id).emit(peer_name, data); + } +}; diff --git a/src/Server.js b/src/Server.js new file mode 100644 index 00000000..27a21be5 --- /dev/null +++ b/src/Server.js @@ -0,0 +1,362 @@ +'use strict'; + +const express = require('express'); +const cors = require('cors'); +const app = express(); +const compression = require('compression'); +const https = require('httpolyglot'); +const fs = require('fs'); +const mediasoup = require('mediasoup'); +const config = require('./config'); +const path = require('path'); +const ngrok = require('ngrok'); +const Room = require('./Room'); +const Peer = require('./Peer'); +const ServerApi = require('./ServerApi'); +const Logger = require('./Logger'); +const log = new Logger('Server'); + +const options = { + key: fs.readFileSync(path.join(__dirname, config.sslKey), 'utf-8'), + cert: fs.readFileSync(path.join(__dirname, config.sslCrt), 'utf-8'), +}; + +const httpsServer = https.createServer(options, app); +const io = require('socket.io')(httpsServer); +const localHost = 'https://' + 'localhost' + ':' + config.listenPort; // config.listenIp + +// all mediasoup workers +let workers = []; +let nextMediasoupWorkerIdx = 0; + +// all Room lists +let roomList = new Map(); + +app.use(cors()); +app.use(compression()); +app.use(express.static(path.join(__dirname, '..', 'public'))); + +// Remove trailing slashes in url handle bad requests +app.use((err, req, res, next) => { + if (err instanceof SyntaxError && err.status === 400 && 'body' in err) { + log.debug('Request Error', { + header: req.headers, + body: req.body, + error: err.message, + }); + return res.status(400).send({ status: 404, message: err.message }); // Bad request + } + if (req.path.substr(-1) === '/' && req.path.length > 1) { + let query = req.url.slice(req.path.length); + res.redirect(301, req.path.slice(0, -1) + query); + } else { + next(); + } +}); + +// all start from here +app.get(['/'], (req, res) => { + res.sendFile(path.join(__dirname, '..', 'public/landing.html')); +}); + +// set new room name and join +app.get(['/newroom'], (req, res) => { + res.sendFile(path.join(__dirname, '..', 'public/newroom.html')); +}); + +// if not allow video/audio +app.get(['/permission'], (req, res) => { + res.sendFile(path.join(__dirname, '..', 'public/permission.html')); +}); + +// no room name specified to join +app.get('/join/', (req, res) => { + res.redirect('/'); +}); + +// join to room +app.get('/join/*', (req, res) => { + if (Object.keys(req.query).length > 0) { + log.debug('redirect:' + req.url + ' to ' + url.parse(req.url).pathname); + res.redirect(url.parse(req.url).pathname); + } else { + res.sendFile(path.join(__dirname, '..', 'public/Room.html')); + } +}); + +// #################################################### +// API +// #################################################### + +// Api parse body data as json +app.use(express.json()); + +// request meeting room endpoint +app.post(['/api/v1/meeting'], (req, res) => { + // check if user was authorized for the api call + let host = req.headers.host; + let authorization = req.headers.authorization; + let api = new ServerApi(host, authorization); + if (!api.isAuthorized()) { + log.debug('MiroTalk get meeting - Unauthorized', { + header: req.headers, + body: req.body, + }); + return res.status(403).json({ error: 'Unauthorized!' }); + } + // setup meeting URL + let meetingURL = api.getMeetingURL(); + res.setHeader('Content-Type', 'application/json'); + res.end(JSON.stringify({ meeting: meetingURL })); + // log.debug the output if all done + log.debug('MiroTalk get meeting - Authorized', { + header: req.headers, + body: req.body, + meeting: meetingURL, + }); +}); + +// #################################################### +// NGROK +// #################################################### + +async function ngrokStart() { + try { + await ngrok.authtoken(config.ngrokAuthToken); + await ngrok.connect(config.listenPort); + let api = ngrok.getApi(); + let data = await api.listTunnels(); + let pu0 = data.tunnels[0].public_url; + let pu1 = data.tunnels[1].public_url; + let tunnel = pu0.startsWith('https') ? pu0 : pu1; + log.debug('Listening on', { + https: localHost, + ngrok: tunnel, + }); + } catch (err) { + console.error('Ngrok Start error: ', err); + process.exit(1); + } +} + +// #################################################### +// START SERVER +// #################################################### + +httpsServer.listen(config.listenPort, () => { + if (config.ngrokAuthToken !== '') { + ngrokStart(); + return; + } + log.debug('Listening on', { + https: localHost, + }); +}); + +// #################################################### +// WORKERS +// #################################################### + +(async () => { + await createWorkers(); +})(); + +async function createWorkers() { + let { numWorkers } = config.mediasoup; + + for (let i = 0; i < numWorkers; i++) { + let worker = await mediasoup.createWorker({ + logLevel: config.mediasoup.worker.logLevel, + logTags: config.mediasoup.worker.logTags, + rtcMinPort: config.mediasoup.worker.rtcMinPort, + rtcMaxPort: config.mediasoup.worker.rtcMaxPort, + }); + worker.on('died', () => { + console.error('Mediasoup worker died, exiting in 2 seconds... [pid:%d]', worker.pid); + setTimeout(() => process.exit(1), 2000); + }); + workers.push(worker); + } +} + +async function getMediasoupWorker() { + const worker = workers[nextMediasoupWorkerIdx]; + if (++nextMediasoupWorkerIdx === workers.length) nextMediasoupWorkerIdx = 0; + return worker; +} + +// #################################################### +// SOCKET IO +// #################################################### + +io.on('connection', (socket) => { + socket.on('createRoom', async ({ room_id }, callback) => { + socket.room_id = room_id; + + if (roomList.has(socket.room_id)) { + callback('already exists'); + } else { + log.debug('Created room', { room_id: socket.room_id }); + let worker = await getMediasoupWorker(); + roomList.set(socket.room_id, new Room(socket.room_id, worker, io)); + callback(socket.room_id); + } + }); + + socket.on('join', (data, cb) => { + log.debug('User joined', data); + + if (!roomList.has(socket.room_id)) { + return cb({ + error: 'Room does not exist', + }); + } + + roomList.get(socket.room_id).addPeer(new Peer(socket.id, data)); + cb(roomList.get(socket.room_id).toJson()); + }); + + socket.on('getRouterRtpCapabilities', (_, callback) => { + log.debug('Get RouterRtpCapabilities', getPeerName()); + try { + callback(roomList.get(socket.room_id).getRtpCapabilities()); + } catch (err) { + callback({ + error: err.message, + }); + } + }); + + socket.on('getProducers', () => { + if (!roomList.has(socket.room_id)) return; + + log.debug('Get producers', getPeerName()); + + // send all the current producer to newly joined member + let producerList = roomList.get(socket.room_id).getProducerListForPeer(); + + socket.emit('newProducers', producerList); + }); + + socket.on('createWebRtcTransport', async (_, callback) => { + log.debug('Create webrtc transport', getPeerName()); + try { + const { params } = await roomList.get(socket.room_id).createWebRtcTransport(socket.id); + callback(params); + } catch (err) { + console.error('Create WebRtc Transport error: ', err); + callback({ + error: err.message, + }); + } + }); + + socket.on('connectTransport', async ({ transport_id, dtlsParameters }, callback) => { + log.debug('Connect transport', getPeerName()); + + if (!roomList.has(socket.room_id)) return; + await roomList.get(socket.room_id).connectPeerTransport(socket.id, transport_id, dtlsParameters); + + callback('success'); + }); + + socket.on('produce', async ({ kind, rtpParameters, producerTransportId }, callback) => { + if (!roomList.has(socket.room_id)) { + return callback({ error: 'Room not found' }); + } + + let producer_id = await roomList + .get(socket.room_id) + .produce(socket.id, producerTransportId, rtpParameters, kind); + + log.debug('Produce', { + kind: kind, + peer_name: getPeerName(false), + producer_id: producer_id, + }); + + callback({ + producer_id, + }); + }); + + socket.on('consume', async ({ consumerTransportId, producerId, rtpCapabilities }, callback) => { + if (!roomList.has(socket.room_id)) { + return callback({ error: 'Room not found' }); + } + + let params = await roomList + .get(socket.room_id) + .consume(socket.id, consumerTransportId, producerId, rtpCapabilities); + + log.debug('Consuming', { + peer_name: getPeerName(false), + producer_id: producerId, + consumer_id: params.id, + }); + + callback(params); + }); + + socket.on('producerClosed', ({ producer_id }) => { + log.debug('Producer close', getPeerName()); + + roomList.get(socket.room_id).closeProducer(socket.id, producer_id); + }); + + socket.on('resume', async (_, callback) => { + await consumer.resume(); + callback(); + }); + + socket.on('getMyRoomInfo', (_, cb) => { + cb(roomList.get(socket.room_id).toJson()); + }); + + socket.on('message', (data) => { + log.debug('message', data); + roomList.get(socket.room_id).broadCast(socket.id, 'message', { + peer_name: data.peer_name, + peer_msg: data.peer_msg, + }); + }); + + socket.on('disconnect', () => { + if (!socket.room_id) return; + + log.debug('Disconnect', getPeerName()); + + roomList.get(socket.room_id).removePeer(socket.id); + }); + + socket.on('exitRoom', async (_, callback) => { + if (!roomList.has(socket.room_id)) { + callback({ + error: 'Not currently in a room', + }); + return; + } + log.debug('Exit room', getPeerName()); + + // close transports + await roomList.get(socket.room_id).removePeer(socket.id); + if (roomList.get(socket.room_id).getPeers().size === 0) { + roomList.delete(socket.room_id); + } + + socket.room_id = null; + + callback('Successfully exited room'); + }); + + // common + function getPeerName(json = true) { + if (json) { + return { + peer_name: + roomList.get(socket.room_id) && roomList.get(socket.room_id).getPeers().get(socket.id).peer_name, + }; + } + return roomList.get(socket.room_id) && roomList.get(socket.room_id).getPeers().get(socket.id).peer_name; + } +}); diff --git a/src/ServerApi.js b/src/ServerApi.js new file mode 100644 index 00000000..ac424e37 --- /dev/null +++ b/src/ServerApi.js @@ -0,0 +1,21 @@ +'use strict'; + +const config = require('./config'); +const { v4: uuidV4 } = require('uuid'); + +module.exports = class ServerApi { + constructor(host, authorization) { + this._host = host; + this._authorization = authorization; + this._api_key_secret = config.apiKeySecret; + } + + isAuthorized() { + if (this._authorization != this._api_key_secret) return false; + return true; + } + + getMeetingURL() { + return 'https://' + this._host + '/join/' + uuidV4(); + } +}; diff --git a/src/config.template.js b/src/config.template.js new file mode 100644 index 00000000..05cfa795 --- /dev/null +++ b/src/config.template.js @@ -0,0 +1,78 @@ +'use strict'; + +const os = require('os'); +const ifaces = os.networkInterfaces(); + +const getLocalIp = () => { + let localIp = '127.0.0.1'; + Object.keys(ifaces).forEach((ifname) => { + for (const iface of ifaces[ifname]) { + // Ignore IPv6 and 127.0.0.1 + if (iface.family !== 'IPv4' || iface.internal !== false) { + continue; + } + // Set the local ip to the first IPv4 address found and exit the loop + localIp = iface.address; + return; + } + }); + return localIp; +}; + +// https://api.ipify.org + +module.exports = { + listenIp: '0.0.0.0', + listenPort: 3010, + // ssl/README.md + sslCrt: '../ssl/cert.pem', + sslKey: '../ssl/key.pem', + /* + Ngrok + 1. Goto https://ngrok.com + 2. Get started for free + 3. Copy YourNgrokAuthToken: https://dashboard.ngrok.com/get-started/your-authtoken + */ + ngrokAuthToken: '', + apiKeySecret: 'mirotalksfu_default_secret', + mediasoup: { + // Worker settings + numWorkers: Object.keys(os.cpus()).length, + worker: { + rtcMinPort: 40000, + rtcMaxPort: 41000, + logLevel: 'warn', + logTags: ['info', 'ice', 'dtls', 'rtp', 'srtp', 'rtcp'], + }, + // Router settings + router: { + mediaCodecs: [ + { + kind: 'audio', + mimeType: 'audio/opus', + clockRate: 48000, + channels: 2, + }, + { + kind: 'video', + mimeType: 'video/VP8', + clockRate: 90000, + parameters: { + 'x-google-start-bitrate': 1000, + }, + }, + ], + }, + // WebRtcTransport settings + webRtcTransport: { + listenIps: [ + { + ip: '0.0.0.0', + announcedIp: getLocalIp(), // replace by public static IP address https://api.ipify.org + }, + ], + maxIncomingBitrate: 1500000, + initialAvailableOutgoingBitrate: 1000000, + }, + }, +}; diff --git a/ssl/README.md b/ssl/README.md new file mode 100644 index 00000000..16af4376 --- /dev/null +++ b/ssl/README.md @@ -0,0 +1,18 @@ +## Self-signed certificate + +![mirotalksfu-https](https.png) + +```bash +# install openssl 4 ubuntu +apt install openssl +# install openssl 4 mac +brew install openssl + +# self-signed certificate +openssl genrsa -out key.pem +openssl req -new -key key.pem -out csr.pem +openssl x509 -req -days 9999 -in csr.pem -signkey key.pem -out cert.pem +rm csr.pem + +# https://www.sslchecker.com/certdecoder +``` diff --git a/ssl/cert.pem b/ssl/cert.pem new file mode 100644 index 00000000..893c641f --- /dev/null +++ b/ssl/cert.pem @@ -0,0 +1,22 @@ +-----BEGIN CERTIFICATE----- +MIIDlTCCAn0CFBM91H+g2aRKsaRrCmo3NdYjwfWUMA0GCSqGSIb3DQEBCwUAMIGG +MQswCQYDVQQGEwJJVDEOMAwGA1UECAwFSXRhbHkxETAPBgNVBAoMCE1pcm9UYWxr +MQ8wDQYDVQQLDAZXZWJSVEMxFzAVBgNVBAMMDk1pcm9zbGF2IFBlamljMSowKAYJ +KoZIhvcNAQkBFhttaXJvc2xhdi5wZWppYy44NUBnbWFpbC5jb20wHhcNMjEwODEx +MTUwOTUzWhcNNDgxMjI2MTUwOTUzWjCBhjELMAkGA1UEBhMCSVQxDjAMBgNVBAgM +BUl0YWx5MREwDwYDVQQKDAhNaXJvVGFsazEPMA0GA1UECwwGV2ViUlRDMRcwFQYD +VQQDDA5NaXJvc2xhdiBQZWppYzEqMCgGCSqGSIb3DQEJARYbbWlyb3NsYXYucGVq +aWMuODVAZ21haWwuY29tMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA +rn2tC9W6wqjDI7B/4LfEnE4ILBOdwa9889QjmWUToKua7iXTpaSNP3SefKY50Q8T +BFfkZXEGyqAESBUn2rYeHtebgLQTKHsixhSCdHqpBDOyYFeTywGiRP4gQHFKbExd +X2AAD1ptTjHVuSlg/ojWstESBh/4TktifKzy3PKVKX6p889eDfyJtlv0PADAkon/ +rZp3hHq0FORhvQEER1sm6g58WyIfqGWjW7bb7/bkyS1baQI16fPeexfu1Rs7y4kx +gX7+9/oA40D3rz0Wf378PTwVzbYCF+hZo/H9yJGTUAZSz84zNbSLvKBZFPfabA2A +l92uPgNWoct06uf7ubEJhwIDAQABMA0GCSqGSIb3DQEBCwUAA4IBAQAyH68yi+mf +2HNwEscSLN1oU9uXNC4hwHi2xoPrpxoLsbhoc/aVhgd5peOEmIlOQPQofCO/JSKt +HqidURSt8U+WrcsXz3HTdq+m/Qw3E/BA5CDXL/LUmIh43cZzkWSawx2EocJr7g1W +JeAtUt8xpDtuLlTMjoGGmTsKQG7ub8NcYN7EEqWa2v+2qSTqvhASMs+cf7DT2HZI +I3q2v6l1N+DcpO8ey8dbLhbhd4d+bGjyjEcT+clDHFrKsqiYDCS99sOmedmHoyZk ++h+CzXICtqFSrAAGE/PoetQtJlojwnu9mJN/xj3i/zJTZTRh3jOGF8Hfg2bvwgdg +vMYRLwtknqya +-----END CERTIFICATE----- diff --git a/ssl/https.png b/ssl/https.png new file mode 100644 index 00000000..467dbdee Binary files /dev/null and b/ssl/https.png differ diff --git a/ssl/key.pem b/ssl/key.pem new file mode 100644 index 00000000..54fb0c20 --- /dev/null +++ b/ssl/key.pem @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEpAIBAAKCAQEArn2tC9W6wqjDI7B/4LfEnE4ILBOdwa9889QjmWUToKua7iXT +paSNP3SefKY50Q8TBFfkZXEGyqAESBUn2rYeHtebgLQTKHsixhSCdHqpBDOyYFeT +ywGiRP4gQHFKbExdX2AAD1ptTjHVuSlg/ojWstESBh/4TktifKzy3PKVKX6p889e +DfyJtlv0PADAkon/rZp3hHq0FORhvQEER1sm6g58WyIfqGWjW7bb7/bkyS1baQI1 +6fPeexfu1Rs7y4kxgX7+9/oA40D3rz0Wf378PTwVzbYCF+hZo/H9yJGTUAZSz84z +NbSLvKBZFPfabA2Al92uPgNWoct06uf7ubEJhwIDAQABAoIBAAhskNota2LSevlS +IBpdROS277YRDGC5dDLhXwac1qG/Jy+wK9OnahpSKwShkdECBU0EYUZ0ent11j8U +pmPsvu+GQT+pcfNWXotpmhK9iUNmq4nzMHNwlMD3896omYs49JkSLW6QUw6fYU4b +LU+ck6D2bwRUrsw433xdbSw1mfXyzyCIWfPNzRmkEcUkCe1RHkGqFv8FrePhezOB +tByAwQQLtBt7FMioTSCedOe7B+tuxwqj5Px7Vr8K3x/PKccSId6hS2ihnptYB+n7 +3eAxoa049wazsW3SXJQrTxUB1z2bh2p2lA0AedgVNme1FCq4zzA7qa2WppcV2ava +Gu+dCgECgYEA1xjLFQmiUqCJVqA99DZNvgiPecuHSe2+2QRIk10uUQcxQAF8K9XT +ORpO05lc0ccPZt7tIbQytsnt/NxL2mnvXQ+dzzrQCsPu/HH8fLEaJ4xBdxuIFeDU +qCBKYlckCQkDwnNYUqCZCNfxb0Csx98RBDYlwDwZa0hFsLPbGen538cCgYEAz6wh +iRXZVTqfhy3meWYFmHqeMb0agFugl4d0Lwl/kI/0M62Jc1u1OU+uI6u5fuevEeBB +xYjnaDpBuCWccLw6R5luc98QIbxldbx2A07rUGo2JlQafmDX8wI1GVBXgzOw7HAK +jHhCw+ZgtvF2c1XjaTPaunKDomPX6Pjt6R23CEECgYEArRRvPbNt4Wz6djElCSC1 +N+ftg4TJjSx4eGog+CtvvJW8BJPtVdyORZGswknSzZ6O/yj8yTUV5c3g6apegxbh +HBIX2wupIjB9WrdiAvgDYrVSbEREIc6zb8Hj+PPDtF2Dn/FurbY6zkntJad2ILKX +H7tubxwtHA2gvkpLULPcdDsCgYAxm3WbUHvM7ycCXIWMhEFb7hZx3TFCbiDLcZDg +V42AU9LKsW5+/u4oVY9MeA3kcaWRSJeNfyl/7UKboWhgSaZGSjFnPmaVGHLIEA/E +tIpjeCudNkPp4mpTYziZ5mYxMhzWLeFnMqcIMrTxnnZkEKU1ESzzkr09AkqmHSh/ +ohiBwQKBgQCvaDQS7aBS/1+iBrOyMwxARwFirReJHdgEtkBVrB+sasbj1B4Jd3zO +gL6FzxKCKBMIF+sSAjKs0XoRG1K6OPGFg1b5naEq023gObO6aBZqSXhHCajGK790 +Xhrvj6BojWkJUnc8T0cocrwhMJrTFC0u00KgAnRNyNYw1vccd5q2uQ== +-----END RSA PRIVATE KEY-----