├── .DS_Store ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── NOTICE.txt ├── README.md ├── Videochat-using-KVS-for-WebRTC.png ├── eslint ├── index.js ├── package.json └── sorted-imports.js ├── examples ├── .DS_Store ├── amazon-cognito-identity.min.js ├── app.css ├── app.js ├── createSignalingChannel.js ├── favicon.ico ├── index.html ├── loader.css ├── master.js └── viewer.js ├── jest.config.js ├── package-lock.json ├── package.json ├── src ├── .DS_Store ├── Role.ts ├── SigV4RequestSigner.spec.ts ├── SigV4RequestSigner.ts ├── SignalingClient.spec.ts ├── SignalingClient.ts ├── index.spec.ts ├── index.ts └── internal │ ├── utils.spec.ts │ └── utils.ts ├── tsconfig.json ├── webpack.config.js ├── webpack.debug.config.js ├── webpack.dev.config.js └── webpack.dist.config.js /.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/amazon-kinesis-video-streams-webrtc-sdk-js-with-amazon-cognito/ff11b8c20e1bc6e32136e519c6c43bf817b8b270/.DS_Store -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | ## Code of Conduct 2 | This project has adopted the [Amazon Open Source Code of Conduct](https://aws.github.io/code-of-conduct). 3 | For more information see the [Code of Conduct FAQ](https://aws.github.io/code-of-conduct-faq) or contact 4 | opensource-codeofconduct@amazon.com with any additional questions or comments. 5 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing Guidelines 2 | 3 | Thank you for your interest in contributing to our project. Whether it's a bug report, new feature, correction, or additional 4 | documentation, we greatly value feedback and contributions from our community. 5 | 6 | Please read through this document before submitting any issues or pull requests to ensure we have all the necessary 7 | information to effectively respond to your bug report or contribution. 8 | 9 | 10 | ## Reporting Bugs/Feature Requests 11 | 12 | We welcome you to use the GitHub issue tracker to report bugs or suggest features. 13 | 14 | When filing an issue, please check existing open, or recently closed, issues to make sure somebody else hasn't already 15 | reported the issue. Please try to include as much information as you can. Details like these are incredibly useful: 16 | 17 | * A reproducible test case or series of steps 18 | * The version of our code being used 19 | * Any modifications you've made relevant to the bug 20 | * Anything unusual about your environment or deployment 21 | 22 | 23 | ## Contributing via Pull Requests 24 | Contributions via pull requests are much appreciated. Before sending us a pull request, please ensure that: 25 | 26 | 1. You are working against the latest source on the *master* branch. 27 | 2. You check existing open, and recently merged, pull requests to make sure someone else hasn't addressed the problem already. 28 | 3. You open an issue to discuss any significant work - we would hate for your time to be wasted. 29 | 30 | To send us a pull request, please: 31 | 32 | 1. Fork the repository. 33 | 2. Modify the source; please focus on the specific change you are contributing. If you also reformat all the code, it will be hard for us to focus on your change. 34 | 3. Ensure local tests pass. 35 | 4. Commit to your fork using clear commit messages. 36 | 5. Send us a pull request, answering any default questions in the pull request interface. 37 | 6. Pay attention to any automated CI failures reported in the pull request, and stay involved in the conversation. 38 | 39 | GitHub provides additional document on [forking a repository](https://help.github.com/articles/fork-a-repo/) and 40 | [creating a pull request](https://help.github.com/articles/creating-a-pull-request/). 41 | 42 | 43 | ## Finding contributions to work on 44 | Looking at the existing issues is a great way to find something to contribute on. As our projects, by default, use the default GitHub issue labels (enhancement/bug/duplicate/help wanted/invalid/question/wontfix), looking at any 'help wanted' issues is a great place to start. 45 | 46 | 47 | ## Code of Conduct 48 | This project has adopted the [Amazon Open Source Code of Conduct](https://aws.github.io/code-of-conduct). 49 | For more information see the [Code of Conduct FAQ](https://aws.github.io/code-of-conduct-faq) or contact 50 | opensource-codeofconduct@amazon.com with any additional questions or comments. 51 | 52 | 53 | ## Security issue notifications 54 | If you discover a potential security issue in this project we ask that you notify AWS/Amazon Security via our [vulnerability reporting page](http://aws.amazon.com/security/vulnerability-reporting/). Please do **not** create a public github issue. 55 | 56 | 57 | ## Licensing 58 | 59 | See the [LICENSE](LICENSE) file for our project's licensing. We will ask you to confirm the licensing of your contribution. 60 | 61 | We may ask you to sign a [Contributor License Agreement (CLA)](http://en.wikipedia.org/wiki/Contributor_License_Agreement) for larger changes. 62 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of 4 | this software and associated documentation files (the "Software"), to deal in 5 | the Software without restriction, including without limitation the rights to 6 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 7 | the Software, and to permit persons to whom the Software is furnished to do so. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 10 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 11 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 12 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 13 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 14 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 15 | 16 | -------------------------------------------------------------------------------- /NOTICE.txt: -------------------------------------------------------------------------------- 1 | Amazon Kinesis Video Streams WebRTC SDK for JavaScript 2 | Copyright 2019-2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | 4 | This product includes software developed at 5 | Amazon Web Services, Inc. (http://aws.amazon.com/). -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Amazon Kinesis Video Streams WebRTC SDK for JavaScript with Amazon Cognito 2 | 3 | This sample code is derived from [Amazon Kinesis Video Streams WebRTC SDK for JavaScript](https://github.com/awslabs/amazon-kinesis-video-streams-webrtc-sdk-js). 4 | 5 | ## Summary 6 | [Amazon Kinesis Video Streams for WebRTC](https://docs.aws.amazon.com/kinesisvideostreams-webrtc-dg/latest/devguide/kvswebrtc-how-it-works.html) is a fully managed AWS service that supports thousands of simultaneous video chats, and frees developers from having to procure, set up and maintain their own media servers. This solution allows you to build a basic browser-based video chat application. 7 | 8 | ## How It Works 9 | ![](Videochat-using-KVS-for-WebRTC.png) 10 | 11 | ## References 12 | [Enabling Video Chats Using Amazon Kinesis Video Streams for WebRTC](https://aws.amazon.com/blogs/media/enabling-video-chats-using-amazon-kinesis-video-streams-for-webrtc/) 13 | 14 | ## License 15 | 16 | This library is licensed under the MIT-0 License. See the LICENSE file. 17 | -------------------------------------------------------------------------------- /Videochat-using-KVS-for-WebRTC.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/amazon-kinesis-video-streams-webrtc-sdk-js-with-amazon-cognito/ff11b8c20e1bc6e32136e519c6c43bf817b8b270/Videochat-using-KVS-for-WebRTC.png -------------------------------------------------------------------------------- /eslint/index.js: -------------------------------------------------------------------------------- 1 | const sortedImports = require('./sorted-imports'); 2 | 3 | module.exports = { 4 | rules: { 5 | 'sorted-imports': { 6 | create: sortedImports, 7 | }, 8 | }, 9 | }; 10 | -------------------------------------------------------------------------------- /eslint/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "eslint-plugin-kvs-webrtc", 3 | "version": "1.0.0", 4 | "main": "index.js" 5 | } 6 | -------------------------------------------------------------------------------- /eslint/sorted-imports.js: -------------------------------------------------------------------------------- 1 | function isBlankLineBefore(context, node) { 2 | return node.range[0] > 0 && !context.getSourceCode().getLocFromIndex(node.range[0] - 1).column; 3 | } 4 | 5 | function getText(context, node) { 6 | return context.getSourceCode().getText(node); 7 | } 8 | 9 | module.exports = context => { 10 | let previousImport = null; 11 | let previousLocalImport = null; 12 | 13 | return { 14 | meta: { 15 | type: 'layout', 16 | fixable: 'whitespace', 17 | }, 18 | ImportDeclaration(node) { 19 | const sourceValue = node.source.value; 20 | 21 | if (sourceValue.startsWith('.') || sourceValue.startsWith('/')) { 22 | // Check if there is not a blank line before the first local import 23 | if (!isBlankLineBefore(context, node) && node.range[0] > 0 && previousLocalImport === null) { 24 | context.report({ 25 | node, 26 | message: 'The local imports should be separated by an empty line.', 27 | fix(fixer) { 28 | return fixer.insertTextBefore(node, '\n'); 29 | }, 30 | }); 31 | } 32 | 33 | // Check if there is a blank line between local imports 34 | if (isBlankLineBefore(context, node) && previousLocalImport !== null) { 35 | context.report({ 36 | node, 37 | message: 'The local imports should not be separated by any empty lines.', 38 | fix(fixer) { 39 | return fixer.removeRange([node.range[0] - 1, node.range[0]]); 40 | }, 41 | }); 42 | } 43 | 44 | // Check if a local import is not sorted 45 | if (previousLocalImport !== null && sourceValue.toString() < previousLocalImport.source.value.toString()) { 46 | context.report({ 47 | node: node.source, 48 | message: `Imports should be sorted. '${previousLocalImport.source.value}' should come after '${sourceValue}'`, 49 | fix(fixer) { 50 | return [fixer.replaceText(node, getText(context, previousLocalImport)), fixer.replaceText(previousLocalImport, getText(context, node))]; 51 | }, 52 | }); 53 | } 54 | previousLocalImport = node; 55 | } else { 56 | // Check if a non-local import came after a local import 57 | if (previousLocalImport !== null) { 58 | context.report({ 59 | node: node.source, 60 | message: `Non-local imports should come before KVS imports. '${sourceValue}' should come before '${previousLocalImport.source.value}'`, 61 | fix(fixer) { 62 | return [fixer.replaceText(node, getText(context, previousLocalImport)), fixer.replaceText(previousLocalImport, getText(context, node))]; 63 | }, 64 | }); 65 | } 66 | 67 | // Check if there is a blank line between non-KVS imports 68 | if (isBlankLineBefore(context, node) && previousImport !== null) { 69 | context.report({ 70 | node, 71 | message: 'The non-local imports should not be separated by any empty lines.', 72 | fix(fixer) { 73 | return fixer.removeRange([node.range[0] - 1, node.range[0]]); 74 | }, 75 | }); 76 | } 77 | 78 | // Check if a non-kvs-webrtc import is not sorted 79 | if (previousImport !== null && sourceValue < previousImport.source.value) { 80 | context.report({ 81 | node: node.source, 82 | message: `Imports should be sorted. '${previousImport.source.value}' should come after '${sourceValue}'`, 83 | fix(fixer) { 84 | return [fixer.replaceText(node, getText(context, previousImport)), fixer.replaceText(previousImport, getText(context, node))]; 85 | }, 86 | }); 87 | } 88 | previousImport = node; 89 | } 90 | 91 | // Check if import specifiers are. not sorted 92 | const specifiers = node.specifiers.filter(s => s.type === 'ImportSpecifier'); 93 | const sortedSpecifiers = specifiers.concat().sort((a, b) => { 94 | if (a.local.name > b.local.name) { 95 | return 1; 96 | } 97 | return -1; 98 | }); 99 | for (const i in specifiers) { 100 | const specifier = specifiers[i]; 101 | const specifierName = specifier.local.name; 102 | const sortedSpecifier = sortedSpecifiers[i]; 103 | const sortedSpecifierName = sortedSpecifier.local.name; 104 | if (specifierName !== sortedSpecifierName) { 105 | context.report({ 106 | node: specifier, 107 | message: `Import specifiers should be sorted. '${specifierName}' should come after '${sortedSpecifierName}'`, 108 | fix(fixer) { 109 | const fixes = []; 110 | for (const j in specifiers) { 111 | const s = specifiers[j]; 112 | const ss = sortedSpecifiers[j]; 113 | fixes.push(fixer.replaceText(s, getText(context, ss))); 114 | } 115 | return fixes; 116 | }, 117 | }); 118 | break; 119 | } 120 | } 121 | }, 122 | }; 123 | }; 124 | -------------------------------------------------------------------------------- /examples/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/amazon-kinesis-video-streams-webrtc-sdk-js-with-amazon-cognito/ff11b8c20e1bc6e32136e519c6c43bf817b8b270/examples/.DS_Store -------------------------------------------------------------------------------- /examples/amazon-cognito-identity.min.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * Copyright 2016 Amazon.com, 3 | * Inc. or its affiliates. All Rights Reserved. 4 | * 5 | * Licensed under the Amazon Software License (the "License"). 6 | * You may not use this file except in compliance with the 7 | * License. A copy of the License is located at 8 | * 9 | * http://aws.amazon.com/asl/ 10 | * 11 | * or in the "license" file accompanying this file. This file is 12 | * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR 13 | * CONDITIONS OF ANY KIND, express or implied. See the License 14 | * for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | !function(e,t){"object"==typeof exports&&"object"==typeof module?module.exports=t():"function"==typeof define&&define.amd?define([],t):"object"==typeof exports?exports.AmazonCognitoIdentity=t():e.AmazonCognitoIdentity=t()}(this,function(){return function(e){function t(r){if(n[r])return n[r].exports;var i=n[r]={exports:{},id:r,loaded:!1};return e[r].call(i.exports,i,i.exports,t),i.loaded=!0,i.exports}var n={};return t.m=e,t.c=n,t.p="",t(0)}([function(e,t,n){"use strict";function r(e){return e&&e.__esModule?e:{default:e}}t.__esModule=!0;var i=n(19);Object.defineProperty(t,"AuthenticationDetails",{enumerable:!0,get:function(){return r(i).default}});var o=n(3);Object.defineProperty(t,"AuthenticationHelper",{enumerable:!0,get:function(){return r(o).default}});var s=n(5);Object.defineProperty(t,"CognitoAccessToken",{enumerable:!0,get:function(){return r(s).default}});var a=n(6);Object.defineProperty(t,"CognitoIdToken",{enumerable:!0,get:function(){return r(a).default}});var u=n(8);Object.defineProperty(t,"CognitoRefreshToken",{enumerable:!0,get:function(){return r(u).default}});var c=n(9);Object.defineProperty(t,"CognitoUser",{enumerable:!0,get:function(){return r(c).default}});var h=n(10);Object.defineProperty(t,"CognitoUserAttribute",{enumerable:!0,get:function(){return r(h).default}});var f=n(21);Object.defineProperty(t,"CognitoUserPool",{enumerable:!0,get:function(){return r(f).default}});var l=n(11);Object.defineProperty(t,"CognitoUserSession",{enumerable:!0,get:function(){return r(l).default}});var p=n(22);Object.defineProperty(t,"CookieStorage",{enumerable:!0,get:function(){return r(p).default}});var d=n(12);Object.defineProperty(t,"DateHelper",{enumerable:!0,get:function(){return r(d).default}})},function(e,t,n){(function(e){/*! 18 | * The buffer module from node.js, for the browser. 19 | * 20 | * @author Feross Aboukhadijeh 21 | * @license MIT 22 | */ 23 | "use strict";function r(){try{var e=new Uint8Array(1);return e.__proto__={__proto__:Uint8Array.prototype,foo:function(){return 42}},42===e.foo()&&"function"==typeof e.subarray&&0===e.subarray(1,1).byteLength}catch(e){return!1}}function i(){return s.TYPED_ARRAY_SUPPORT?2147483647:1073741823}function o(e,t){if(i()=i())throw new RangeError("Attempt to allocate Buffer larger than maximum size: 0x"+i().toString(16)+" bytes");return 0|e}function v(e){return+e!=e&&(e=0),s.alloc(+e)}function y(e,t){if(s.isBuffer(e))return e.length;if("undefined"!=typeof ArrayBuffer&&"function"==typeof ArrayBuffer.isView&&(ArrayBuffer.isView(e)||e instanceof ArrayBuffer))return e.byteLength;"string"!=typeof e&&(e=""+e);var n=e.length;if(0===n)return 0;for(var r=!1;;)switch(t){case"ascii":case"latin1":case"binary":return n;case"utf8":case"utf-8":case void 0:return H(e).length;case"ucs2":case"ucs-2":case"utf16le":case"utf-16le":return 2*n;case"hex":return n>>>1;case"base64":return G(e).length;default:if(r)return H(e).length;t=(""+t).toLowerCase(),r=!0}}function m(e,t,n){var r=!1;if((void 0===t||t<0)&&(t=0),t>this.length)return"";if((void 0===n||n>this.length)&&(n=this.length),n<=0)return"";if(n>>>=0,t>>>=0,n<=t)return"";for(e||(e="utf8");;)switch(e){case"hex":return F(this,t,n);case"utf8":case"utf-8":return P(this,t,n);case"ascii":return b(this,t,n);case"latin1":case"binary":return k(this,t,n);case"base64":return R(this,t,n);case"ucs2":case"ucs-2":case"utf16le":case"utf-16le":return B(this,t,n);default:if(r)throw new TypeError("Unknown encoding: "+e);e=(e+"").toLowerCase(),r=!0}}function S(e,t,n){var r=e[t];e[t]=e[n],e[n]=r}function w(e,t,n,r,i){if(0===e.length)return-1;if("string"==typeof n?(r=n,n=0):n>2147483647?n=2147483647:n<-2147483648&&(n=-2147483648),n=+n,isNaN(n)&&(n=i?0:e.length-1),n<0&&(n=e.length+n),n>=e.length){if(i)return-1;n=e.length-1}else if(n<0){if(!i)return-1;n=0}if("string"==typeof t&&(t=s.from(t,r)),s.isBuffer(t))return 0===t.length?-1:A(e,t,n,r,i);if("number"==typeof t)return t&=255,s.TYPED_ARRAY_SUPPORT&&"function"==typeof Uint8Array.prototype.indexOf?i?Uint8Array.prototype.indexOf.call(e,t,n):Uint8Array.prototype.lastIndexOf.call(e,t,n):A(e,[t],n,r,i);throw new TypeError("val must be string, number or Buffer")}function A(e,t,n,r,i){function o(e,t){return 1===s?e[t]:e.readUInt16BE(t*s)}var s=1,a=e.length,u=t.length;if(void 0!==r&&(r=String(r).toLowerCase(),"ucs2"===r||"ucs-2"===r||"utf16le"===r||"utf-16le"===r)){if(e.length<2||t.length<2)return-1;s=2,a/=2,u/=2,n/=2}var c;if(i){var h=-1;for(c=n;ca&&(n=a-u),c=n;c>=0;c--){for(var f=!0,l=0;li&&(r=i)):r=i;var o=t.length;if(o%2!==0)throw new TypeError("Invalid hex string");r>o/2&&(r=o/2);for(var s=0;s239?4:o>223?3:o>191?2:1;if(i+a<=n){var u,c,h,f;switch(a){case 1:o<128&&(s=o);break;case 2:u=e[i+1],128===(192&u)&&(f=(31&o)<<6|63&u,f>127&&(s=f));break;case 3:u=e[i+1],c=e[i+2],128===(192&u)&&128===(192&c)&&(f=(15&o)<<12|(63&u)<<6|63&c,f>2047&&(f<55296||f>57343)&&(s=f));break;case 4:u=e[i+1],c=e[i+2],h=e[i+3],128===(192&u)&&128===(192&c)&&128===(192&h)&&(f=(15&o)<<18|(63&u)<<12|(63&c)<<6|63&h,f>65535&&f<1114112&&(s=f))}}null===s?(s=65533,a=1):s>65535&&(s-=65536,r.push(s>>>10&1023|55296),s=56320|1023&s),r.push(s),i+=a}return _(r)}function _(e){var t=e.length;if(t<=ee)return String.fromCharCode.apply(String,e);for(var n="",r=0;rr)&&(n=r);for(var i="",o=t;on)throw new RangeError("Trying to access beyond buffer length")}function x(e,t,n,r,i,o){if(!s.isBuffer(e))throw new TypeError('"buffer" argument must be a Buffer instance');if(t>i||te.length)throw new RangeError("Index out of range")}function O(e,t,n,r){t<0&&(t=65535+t+1);for(var i=0,o=Math.min(e.length-n,2);i>>8*(r?i:1-i)}function N(e,t,n,r){t<0&&(t=4294967295+t+1);for(var i=0,o=Math.min(e.length-n,4);i>>8*(r?i:3-i)&255}function V(e,t,n,r,i,o){if(n+r>e.length)throw new RangeError("Index out of range");if(n<0)throw new RangeError("Index out of range")}function K(e,t,n,r,i){return i||V(e,t,n,4,3.4028234663852886e38,-3.4028234663852886e38),Q.write(e,t,n,r,23,4),n+4}function q(e,t,n,r,i){return i||V(e,t,n,8,1.7976931348623157e308,-1.7976931348623157e308),Q.write(e,t,n,r,52,8),n+8}function L(e){if(e=Y(e).replace(te,""),e.length<2)return"";for(;e.length%4!==0;)e+="=";return e}function Y(e){return e.trim?e.trim():e.replace(/^\s+|\s+$/g,"")}function j(e){return e<16?"0"+e.toString(16):e.toString(16)}function H(e,t){t=t||1/0;for(var n,r=e.length,i=null,o=[],s=0;s55295&&n<57344){if(!i){if(n>56319){(t-=3)>-1&&o.push(239,191,189);continue}if(s+1===r){(t-=3)>-1&&o.push(239,191,189);continue}i=n;continue}if(n<56320){(t-=3)>-1&&o.push(239,191,189),i=n;continue}n=(i-55296<<10|n-56320)+65536}else i&&(t-=3)>-1&&o.push(239,191,189);if(i=null,n<128){if((t-=1)<0)break;o.push(n)}else if(n<2048){if((t-=2)<0)break;o.push(n>>6|192,63&n|128)}else if(n<65536){if((t-=3)<0)break;o.push(n>>12|224,n>>6&63|128,63&n|128)}else{if(!(n<1114112))throw new Error("Invalid code point");if((t-=4)<0)break;o.push(n>>18|240,n>>12&63|128,n>>6&63|128,63&n|128)}}return o}function J(e){for(var t=[],n=0;n>8,i=n%256,o.push(i),o.push(r);return o}function G(e){return X.toByteArray(L(e))}function z(e,t,n,r){for(var i=0;i=t.length||i>=e.length);++i)t[i+n]=e[i];return i}function Z(e){return e!==e}var X=n(15),Q=n(16),$=n(17);t.Buffer=s,t.SlowBuffer=v,t.INSPECT_MAX_BYTES=50,s.TYPED_ARRAY_SUPPORT=void 0!==e.TYPED_ARRAY_SUPPORT?e.TYPED_ARRAY_SUPPORT:r(),t.kMaxLength=i(),s.poolSize=8192,s._augment=function(e){return e.__proto__=s.prototype,e},s.from=function(e,t,n){return a(null,e,t,n)},s.TYPED_ARRAY_SUPPORT&&(s.prototype.__proto__=Uint8Array.prototype,s.__proto__=Uint8Array,"undefined"!=typeof Symbol&&Symbol.species&&s[Symbol.species]===s&&Object.defineProperty(s,Symbol.species,{value:null,configurable:!0})),s.alloc=function(e,t,n){return c(null,e,t,n)},s.allocUnsafe=function(e){return h(null,e)},s.allocUnsafeSlow=function(e){return h(null,e)},s.isBuffer=function(e){return!(null==e||!e._isBuffer)},s.compare=function(e,t){if(!s.isBuffer(e)||!s.isBuffer(t))throw new TypeError("Arguments must be Buffers");if(e===t)return 0;for(var n=e.length,r=t.length,i=0,o=Math.min(n,r);i0&&(e=this.toString("hex",0,n).match(/.{2}/g).join(" "),this.length>n&&(e+=" ... ")),""},s.prototype.compare=function(e,t,n,r,i){if(!s.isBuffer(e))throw new TypeError("Argument must be a Buffer");if(void 0===t&&(t=0),void 0===n&&(n=e?e.length:0),void 0===r&&(r=0),void 0===i&&(i=this.length),t<0||n>e.length||r<0||i>this.length)throw new RangeError("out of range index");if(r>=i&&t>=n)return 0;if(r>=i)return-1;if(t>=n)return 1;if(t>>>=0,n>>>=0,r>>>=0,i>>>=0,this===e)return 0;for(var o=i-r,a=n-t,u=Math.min(o,a),c=this.slice(r,i),h=e.slice(t,n),f=0;fi)&&(n=i),e.length>0&&(n<0||t<0)||t>this.length)throw new RangeError("Attempt to write outside buffer bounds");r||(r="utf8");for(var o=!1;;)switch(r){case"hex":return C(this,e,t,n);case"utf8":case"utf-8":return U(this,e,t,n);case"ascii":return E(this,e,t,n);case"latin1":case"binary":return T(this,e,t,n);case"base64":return D(this,e,t,n);case"ucs2":case"ucs-2":case"utf16le":case"utf-16le":return I(this,e,t,n);default:if(o)throw new TypeError("Unknown encoding: "+r);r=(""+r).toLowerCase(),o=!0}},s.prototype.toJSON=function(){return{type:"Buffer",data:Array.prototype.slice.call(this._arr||this,0)}};var ee=4096;s.prototype.slice=function(e,t){var n=this.length;e=~~e,t=void 0===t?n:~~t,e<0?(e+=n,e<0&&(e=0)):e>n&&(e=n),t<0?(t+=n,t<0&&(t=0)):t>n&&(t=n),t0&&(i*=256);)r+=this[e+--t]*i;return r},s.prototype.readUInt8=function(e,t){return t||M(e,1,this.length),this[e]},s.prototype.readUInt16LE=function(e,t){return t||M(e,2,this.length),this[e]|this[e+1]<<8},s.prototype.readUInt16BE=function(e,t){return t||M(e,2,this.length),this[e]<<8|this[e+1]},s.prototype.readUInt32LE=function(e,t){return t||M(e,4,this.length),(this[e]|this[e+1]<<8|this[e+2]<<16)+16777216*this[e+3]},s.prototype.readUInt32BE=function(e,t){return t||M(e,4,this.length),16777216*this[e]+(this[e+1]<<16|this[e+2]<<8|this[e+3])},s.prototype.readIntLE=function(e,t,n){e|=0,t|=0,n||M(e,t,this.length);for(var r=this[e],i=1,o=0;++o=i&&(r-=Math.pow(2,8*t)),r},s.prototype.readIntBE=function(e,t,n){e|=0,t|=0,n||M(e,t,this.length);for(var r=t,i=1,o=this[e+--r];r>0&&(i*=256);)o+=this[e+--r]*i;return i*=128,o>=i&&(o-=Math.pow(2,8*t)),o},s.prototype.readInt8=function(e,t){return t||M(e,1,this.length),128&this[e]?(255-this[e]+1)*-1:this[e]},s.prototype.readInt16LE=function(e,t){t||M(e,2,this.length);var n=this[e]|this[e+1]<<8;return 32768&n?4294901760|n:n},s.prototype.readInt16BE=function(e,t){t||M(e,2,this.length);var n=this[e+1]|this[e]<<8;return 32768&n?4294901760|n:n},s.prototype.readInt32LE=function(e,t){return t||M(e,4,this.length),this[e]|this[e+1]<<8|this[e+2]<<16|this[e+3]<<24},s.prototype.readInt32BE=function(e,t){return t||M(e,4,this.length),this[e]<<24|this[e+1]<<16|this[e+2]<<8|this[e+3]},s.prototype.readFloatLE=function(e,t){return t||M(e,4,this.length),Q.read(this,e,!0,23,4)},s.prototype.readFloatBE=function(e,t){return t||M(e,4,this.length),Q.read(this,e,!1,23,4)},s.prototype.readDoubleLE=function(e,t){return t||M(e,8,this.length),Q.read(this,e,!0,52,8)},s.prototype.readDoubleBE=function(e,t){return t||M(e,8,this.length),Q.read(this,e,!1,52,8)},s.prototype.writeUIntLE=function(e,t,n,r){if(e=+e,t|=0,n|=0,!r){var i=Math.pow(2,8*n)-1;x(this,e,t,n,i,0)}var o=1,s=0;for(this[t]=255&e;++s=0&&(s*=256);)this[t+o]=e/s&255;return t+n},s.prototype.writeUInt8=function(e,t,n){return e=+e,t|=0,n||x(this,e,t,1,255,0),s.TYPED_ARRAY_SUPPORT||(e=Math.floor(e)),this[t]=255&e,t+1},s.prototype.writeUInt16LE=function(e,t,n){return e=+e,t|=0,n||x(this,e,t,2,65535,0),s.TYPED_ARRAY_SUPPORT?(this[t]=255&e,this[t+1]=e>>>8):O(this,e,t,!0),t+2},s.prototype.writeUInt16BE=function(e,t,n){return e=+e,t|=0,n||x(this,e,t,2,65535,0),s.TYPED_ARRAY_SUPPORT?(this[t]=e>>>8,this[t+1]=255&e):O(this,e,t,!1),t+2},s.prototype.writeUInt32LE=function(e,t,n){return e=+e,t|=0,n||x(this,e,t,4,4294967295,0),s.TYPED_ARRAY_SUPPORT?(this[t+3]=e>>>24,this[t+2]=e>>>16,this[t+1]=e>>>8,this[t]=255&e):N(this,e,t,!0),t+4},s.prototype.writeUInt32BE=function(e,t,n){return e=+e,t|=0,n||x(this,e,t,4,4294967295,0),s.TYPED_ARRAY_SUPPORT?(this[t]=e>>>24,this[t+1]=e>>>16,this[t+2]=e>>>8,this[t+3]=255&e):N(this,e,t,!1),t+4},s.prototype.writeIntLE=function(e,t,n,r){if(e=+e,t|=0,!r){var i=Math.pow(2,8*n-1);x(this,e,t,n,i-1,-i)}var o=0,s=1,a=0;for(this[t]=255&e;++o>0)-a&255;return t+n},s.prototype.writeIntBE=function(e,t,n,r){if(e=+e,t|=0,!r){var i=Math.pow(2,8*n-1);x(this,e,t,n,i-1,-i)}var o=n-1,s=1,a=0;for(this[t+o]=255&e;--o>=0&&(s*=256);)e<0&&0===a&&0!==this[t+o+1]&&(a=1),this[t+o]=(e/s>>0)-a&255;return t+n},s.prototype.writeInt8=function(e,t,n){return e=+e,t|=0,n||x(this,e,t,1,127,-128),s.TYPED_ARRAY_SUPPORT||(e=Math.floor(e)),e<0&&(e=255+e+1),this[t]=255&e,t+1},s.prototype.writeInt16LE=function(e,t,n){return e=+e,t|=0,n||x(this,e,t,2,32767,-32768),s.TYPED_ARRAY_SUPPORT?(this[t]=255&e,this[t+1]=e>>>8):O(this,e,t,!0),t+2},s.prototype.writeInt16BE=function(e,t,n){return e=+e,t|=0,n||x(this,e,t,2,32767,-32768),s.TYPED_ARRAY_SUPPORT?(this[t]=e>>>8,this[t+1]=255&e):O(this,e,t,!1),t+2},s.prototype.writeInt32LE=function(e,t,n){return e=+e,t|=0,n||x(this,e,t,4,2147483647,-2147483648),s.TYPED_ARRAY_SUPPORT?(this[t]=255&e,this[t+1]=e>>>8,this[t+2]=e>>>16,this[t+3]=e>>>24):N(this,e,t,!0),t+4},s.prototype.writeInt32BE=function(e,t,n){return e=+e,t|=0,n||x(this,e,t,4,2147483647,-2147483648),e<0&&(e=4294967295+e+1),s.TYPED_ARRAY_SUPPORT?(this[t]=e>>>24,this[t+1]=e>>>16,this[t+2]=e>>>8,this[t+3]=255&e):N(this,e,t,!1),t+4},s.prototype.writeFloatLE=function(e,t,n){return K(this,e,t,!0,n)},s.prototype.writeFloatBE=function(e,t,n){return K(this,e,t,!1,n)},s.prototype.writeDoubleLE=function(e,t,n){return q(this,e,t,!0,n)},s.prototype.writeDoubleBE=function(e,t,n){return q(this,e,t,!1,n)},s.prototype.copy=function(e,t,n,r){if(n||(n=0),r||0===r||(r=this.length),t>=e.length&&(t=e.length),t||(t=0),r>0&&r=this.length)throw new RangeError("sourceStart out of bounds");if(r<0)throw new RangeError("sourceEnd out of bounds");r>this.length&&(r=this.length),e.length-t=0;--i)e[i+t]=this[i+n];else if(o<1e3||!s.TYPED_ARRAY_SUPPORT)for(i=0;i>>=0,n=void 0===n?this.length:n>>>0,e||(e=0);var o;if("number"==typeof e)for(o=t;o=0;){var s=t*this[e++]+n[r]+i;i=Math.floor(s/67108864),n[r++]=67108863&s}return i}function o(e,t,n,r,i,o){for(var s=32767&t,a=t>>15;--o>=0;){var u=32767&this[e],c=this[e++]>>15,h=a*u+c*s;u=s*u+((32767&h)<<15)+n[r]+(1073741823&i),i=(u>>>30)+(h>>>15)+a*c+(i>>>30),n[r++]=1073741823&u}return i}function s(e,t,n,r,i,o){for(var s=16383&t,a=t>>14;--o>=0;){var u=16383&this[e],c=this[e++]>>14,h=a*u+c*s;u=s*u+((16383&h)<<14)+n[r]+i,i=(u>>28)+(h>>14)+a*c,n[r++]=268435455&u}return i}function a(e){return Z.charAt(e)}function u(e,t){var n=X[e.charCodeAt(t)];return null==n?-1:n}function c(e){for(var t=this.t-1;t>=0;--t)e[t]=this[t];e.t=this.t,e.s=this.s}function h(e){this.t=1,this.s=e<0?-1:0,e>0?this[0]=e:e<-1?this[0]=e+this.DV:this.t=0}function f(e){var t=r();return t.fromInt(e),t}function l(e,t){var r;if(16==t)r=4;else if(8==t)r=3;else if(2==t)r=1;else if(32==t)r=5;else{if(4!=t)throw new Error("Only radix 2, 4, 8, 16, 32 are supported");r=2}this.t=0,this.s=0;for(var i=e.length,o=!1,s=0;--i>=0;){var a=u(e,i);a<0?"-"==e.charAt(i)&&(o=!0):(o=!1,0==s?this[this.t++]=a:s+r>this.DB?(this[this.t-1]|=(a&(1<>this.DB-s):this[this.t-1]|=a<=this.DB&&(s-=this.DB))}this.clamp(),o&&n.ZERO.subTo(this,this)}function p(){for(var e=this.s&this.DM;this.t>0&&this[this.t-1]==e;)--this.t}function d(e){if(this.s<0)return"-"+this.negate().toString();var t;if(16==e)t=4;else if(8==e)t=3;else if(2==e)t=1;else if(32==e)t=5;else{if(4!=e)throw new Error("Only radix 2, 4, 8, 16, 32 are supported");t=2}var n,r=(1<0)for(u>u)>0&&(i=!0,o=a(n));s>=0;)u>(u+=this.DB-t)):(n=this[s]>>(u-=t)&r,u<=0&&(u+=this.DB,--s)),n>0&&(i=!0),i&&(o+=a(n));return i?o:"0"}function g(){var e=r();return n.ZERO.subTo(this,e),e}function v(){return this.s<0?this.negate():this}function y(e){var t=this.s-e.s;if(0!=t)return t;var n=this.t;if(t=n-e.t,0!=t)return this.s<0?-t:t;for(;--n>=0;)if(0!=(t=this[n]-e[n]))return t;return 0}function m(e){var t,n=1;return 0!=(t=e>>>16)&&(e=t,n+=16),0!=(t=e>>8)&&(e=t,n+=8),0!=(t=e>>4)&&(e=t,n+=4),0!=(t=e>>2)&&(e=t,n+=2),0!=(t=e>>1)&&(e=t,n+=1),n}function S(){return this.t<=0?0:this.DB*(this.t-1)+m(this[this.t-1]^this.s&this.DM)}function w(e,t){var n;for(n=this.t-1;n>=0;--n)t[n+e]=this[n];for(n=e-1;n>=0;--n)t[n]=0;t.t=this.t+e,t.s=this.s}function A(e,t){for(var n=e;n=0;--n)t[n+s+1]=this[n]>>i|a,a=(this[n]&o)<=0;--n)t[n]=0;t[s]=a,t.t=this.t+s+1,t.s=this.s,t.clamp()}function U(e,t){t.s=this.s;var n=Math.floor(e/this.DB);if(n>=this.t)return void(t.t=0);var r=e%this.DB,i=this.DB-r,o=(1<>r;for(var s=n+1;s>r;r>0&&(t[this.t-n-1]|=(this.s&o)<>=this.DB;if(e.t>=this.DB;r+=this.s}else{for(r+=this.s;n>=this.DB;r-=e.s}t.s=r<0?-1:0,r<-1?t[n++]=this.DV+r:r>0&&(t[n++]=r),t.t=n,t.clamp()}function T(e,t){var r=this.abs(),i=e.abs(),o=r.t;for(t.t=o+i.t;--o>=0;)t[o]=0;for(o=0;o=0;)e[n]=0;for(n=0;n=t.DV&&(e[n+t.t]-=t.DV,e[n+t.t+1]=1)}e.t>0&&(e[e.t-1]+=t.am(n,t[n],e,2*n,0,1)),e.s=0,e.clamp()}function I(e,t,i){var o=e.abs();if(!(o.t<=0)){var s=this.abs();if(s.t0?(o.lShiftTo(h,a),s.lShiftTo(h,i)):(o.copyTo(a),s.copyTo(i));var f=a.t,l=a[f-1];if(0!=l){var p=l*(1<1?a[f-2]>>this.F2:0),d=this.FV/p,g=(1<=0&&(i[i.t++]=1,i.subTo(w,i)),n.ONE.dlShiftTo(f,w),w.subTo(a,a);a.t=0;){var A=i[--y]==l?this.DM:Math.floor(i[y]*d+(i[y-1]+v)*g);if((i[y]+=a.am(0,A,i,S,0,f))0&&i.rShiftTo(h,i),u<0&&n.ZERO.subTo(i,i)}}}function R(e){var t=r();return this.abs().divRemTo(e,null,t),this.s<0&&t.compareTo(n.ZERO)>0&&e.subTo(t,t),t}function P(){if(this.t<1)return 0;var e=this[0];if(0==(1&e))return 0;var t=3&e;return t=t*(2-(15&e)*t)&15,t=t*(2-(255&e)*t)&255,t=t*(2-((65535&e)*t&65535))&65535,t=t*(2-e*t%this.DV)%this.DV,t>0?this.DV-t:-t}function _(e){return 0==this.compareTo(e)}function b(e,t){for(var n=0,r=0,i=Math.min(e.t,this.t);n>=this.DB;if(e.t>=this.DB;r+=this.s}else{for(r+=this.s;n>=this.DB;r+=e.s}t.s=r<0?-1:0,r>0?t[n++]=r:r<-1&&(t[n++]=this.DV+r),t.t=n,t.clamp()}function k(e){var t=r();return this.addTo(e,t),t}function F(e){var t=r();return this.subTo(e,t),t}function B(e){var t=r();return this.multiplyTo(e,t),t}function M(e){var t=r();return this.divRemTo(e,t,null),t}function x(e){this.m=e,this.mp=e.invDigit(),this.mpl=32767&this.mp,this.mph=this.mp>>15,this.um=(1<0&&this.m.subTo(t,t),t}function N(e){var t=r();return e.copyTo(t),this.reduce(t),t}function V(e){for(;e.t<=this.mt2;)e[e.t++]=0;for(var t=0;t>15)*this.mpl&this.um)<<15)&e.DM;for(n=t+this.m.t,e[n]+=this.m.am(0,r,e,t,0,this.m.t);e[n]>=e.DV;)e[n]-=e.DV,e[++n]++}e.clamp(),e.drShiftTo(this.m.t,e),e.compareTo(this.m)>=0&&e.subTo(this.m,e)}function K(e,t){e.squareTo(t),this.reduce(t)}function q(e,t,n){e.multiplyTo(t,n),this.reduce(n)}function L(e,t,n){var i,o=e.bitLength(),s=f(1),a=new x(t);if(o<=0)return s;i=o<18?1:o<48?3:o<144?4:o<768?5:6;var u=new Array,c=3,h=i-1,l=(1<1){var p=r();for(a.sqrTo(u[1],p);c<=l;)u[c]=r(),a.mulTo(p,u[c-2],u[c]),c+=2}var d,g,v=e.t-1,y=!0,S=r();for(o=m(e[v])-1;v>=0;){for(o>=h?d=e[v]>>o-h&l:(d=(e[v]&(1<0&&(d|=e[v-1]>>this.DB+o-h)),c=i;0==(1&d);)d>>=1,--c;if((o-=c)<0&&(o+=this.DB,--v),y)u[d].copyTo(s),y=!1;else{for(;c>1;)a.sqrTo(s,S),a.sqrTo(S,s),c-=2;c>0?a.sqrTo(s,S):(g=s,s=S,S=g),a.mulTo(S,u[d],s)}for(;v>=0&&0==(e[v]&1<0&&void 0!==arguments[0]?arguments[0]:{},r=n.AccessToken;return i(this,t),o(this,e.call(this,r||""))}return s(t,e),t}(u.default);t.default=c},function(e,t,n){"use strict";function r(e){return e&&e.__esModule?e:{default:e}}function i(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function o(e,t){if(!e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!t||"object"!=typeof t&&"function"!=typeof t?e:t}function s(e,t){if("function"!=typeof t&&null!==t)throw new TypeError("Super expression must either be null or a function, not "+typeof t);e.prototype=Object.create(t&&t.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}}),t&&(Object.setPrototypeOf?Object.setPrototypeOf(e,t):e.__proto__=t)}t.__esModule=!0;var a=n(7),u=r(a),c=function(e){function t(){var n=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{},r=n.IdToken;return i(this,t),o(this,e.call(this,r||""))}return s(t,e),t}(u.default);t.default=c},function(e,t,n){"use strict";function r(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}t.__esModule=!0;var i=n(1),o=function(){function e(t){r(this,e),this.jwtToken=t||"",this.payload=this.decodePayload()}return e.prototype.getJwtToken=function(){return this.jwtToken},e.prototype.getExpiration=function(){return this.payload.exp},e.prototype.getIssuedAt=function(){return this.payload.iat},e.prototype.decodePayload=function(){var e=this.jwtToken.split(".")[1];try{return JSON.parse(i.Buffer.from(e,"base64").toString("utf8"))}catch(e){return{}}},e}();t.default=o},function(e,t){"use strict";function n(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}t.__esModule=!0;/*! 25 | * Copyright 2016 Amazon.com, 26 | * Inc. or its affiliates. All Rights Reserved. 27 | * 28 | * Licensed under the Amazon Software License (the "License"). 29 | * You may not use this file except in compliance with the 30 | * License. A copy of the License is located at 31 | * 32 | * http://aws.amazon.com/asl/ 33 | * 34 | * or in the "license" file accompanying this file. This file is 35 | * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR 36 | * CONDITIONS OF ANY KIND, express or implied. See the License 37 | * for the specific language governing permissions and 38 | * limitations under the License. 39 | */ 40 | var r=function(){function e(){var t=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{},r=t.RefreshToken;n(this,e),this.token=r||""}return e.prototype.getToken=function(){return this.token},e}();t.default=r},function(e,t,n){"use strict";function r(e){return e&&e.__esModule?e:{default:e}}function i(e){if(e&&e.__esModule)return e;var t={};if(null!=e)for(var n in e)Object.prototype.hasOwnProperty.call(e,n)&&(t[n]=e[n]);return t.default=e,t}function o(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}t.__esModule=!0;var s=n(1),a=n(14),u=i(a),c=n(4),h=r(c),f=n(3),l=r(f),p=n(5),d=r(p),g=n(6),v=r(g),y=n(8),m=r(y),S=n(11),w=r(S),A=n(12),C=r(A),U=n(10),E=r(U),T=n(13),D=r(T),I=u.createHmac,R=function(){function e(t){if(o(this,e),null==t||null==t.Username||null==t.Pool)throw new Error("Username and pool information are required.");this.username=t.Username||"",this.pool=t.Pool,this.Session=null,this.client=t.Pool.client,this.signInUserSession=null,this.authenticationFlowType="USER_SRP_AUTH",this.storage=t.Storage||(new D.default).getStorage()}return e.prototype.setSignInUserSession=function(e){this.clearCachedTokens(),this.signInUserSession=e,this.cacheTokens()},e.prototype.getSignInUserSession=function(){return this.signInUserSession},e.prototype.getUsername=function(){return this.username},e.prototype.getAuthenticationFlowType=function(){return this.authenticationFlowType},e.prototype.setAuthenticationFlowType=function(e){this.authenticationFlowType=e},e.prototype.initiateAuth=function(e,t){var n=this,r=e.getAuthParameters();r.USERNAME=this.username;var i={AuthFlow:"CUSTOM_AUTH",ClientId:this.pool.getClientId(),AuthParameters:r,ClientMetadata:e.getValidationData()};this.getUserContextData()&&(i.UserContextData=this.getUserContextData()),this.client.request("InitiateAuth",i,function(e,r){if(e)return t.onFailure(e);var i=r.ChallengeName,o=r.ChallengeParameters;return"CUSTOM_CHALLENGE"===i?(n.Session=r.Session,t.customChallenge(o)):(n.signInUserSession=n.getCognitoUserSession(r.AuthenticationResult),n.cacheTokens(),t.onSuccess(n.signInUserSession))})},e.prototype.authenticateUser=function(e,t){return"USER_PASSWORD_AUTH"===this.authenticationFlowType?this.authenticateUserPlainUsernamePassword(e,t):"USER_SRP_AUTH"===this.authenticationFlowType?this.authenticateUserDefaultAuth(e,t):t.onFailure(new Error("Authentication flow type is invalid."))},e.prototype.authenticateUserDefaultAuth=function(e,t){var n=this,r=new l.default(this.pool.getUserPoolId().split("_")[1]),i=new C.default,o=void 0,a=void 0,u={};null!=this.deviceKey&&(u.DEVICE_KEY=this.deviceKey),u.USERNAME=this.username,r.getLargeAValue(function(c,f){c&&t.onFailure(c),u.SRP_A=f.toString(16),"CUSTOM_AUTH"===n.authenticationFlowType&&(u.CHALLENGE_NAME="SRP_A");var l={AuthFlow:n.authenticationFlowType,ClientId:n.pool.getClientId(),AuthParameters:u,ClientMetadata:e.getValidationData()};n.getUserContextData(n.username)&&(l.UserContextData=n.getUserContextData(n.username)),n.client.request("InitiateAuth",l,function(u,c){if(u)return t.onFailure(u);var f=c.ChallengeParameters;n.username=f.USER_ID_FOR_SRP,o=new h.default(f.SRP_B,16),a=new h.default(f.SALT,16),n.getCachedDeviceKeyAndPassword(),r.getPasswordAuthenticationKey(n.username,e.getPassword(),o,a,function(e,o){e&&t.onFailure(e);var a=i.getNowString(),u=I("sha256",o).update(s.Buffer.concat([s.Buffer.from(n.pool.getUserPoolId().split("_")[1],"utf8"),s.Buffer.from(n.username,"utf8"),s.Buffer.from(f.SECRET_BLOCK,"base64"),s.Buffer.from(a,"utf8")])).digest("base64"),h={};h.USERNAME=n.username,h.PASSWORD_CLAIM_SECRET_BLOCK=f.SECRET_BLOCK,h.TIMESTAMP=a,h.PASSWORD_CLAIM_SIGNATURE=u,null!=n.deviceKey&&(h.DEVICE_KEY=n.deviceKey);var l=function e(t,r){return n.client.request("RespondToAuthChallenge",t,function(i,o){return i&&"ResourceNotFoundException"===i.code&&i.message.toLowerCase().indexOf("device")!==-1?(h.DEVICE_KEY=null,n.deviceKey=null,n.randomPassword=null,n.deviceGroupKey=null,n.clearCachedDeviceKeyAndPassword(),e(t,r)):r(i,o)})},p={ChallengeName:"PASSWORD_VERIFIER",ClientId:n.pool.getClientId(),ChallengeResponses:h,Session:c.Session};n.getUserContextData()&&(p.UserContextData=n.getUserContextData()),l(p,function(e,i){if(e)return t.onFailure(e);var o=i.ChallengeName;if("NEW_PASSWORD_REQUIRED"===o){n.Session=i.Session;var s=null,a=null,u=[],c=r.getNewPasswordRequiredChallengeUserAttributePrefix();if(i.ChallengeParameters&&(s=JSON.parse(i.ChallengeParameters.userAttributes),a=JSON.parse(i.ChallengeParameters.requiredAttributes)),a)for(var h=0;h0&&void 0!==arguments[0]?arguments[0]:{},r=t.Name,i=t.Value;n(this,e),this.Name=r||"",this.Value=i||""}return e.prototype.getValue=function(){return this.Value},e.prototype.setValue=function(e){return this.Value=e,this},e.prototype.getName=function(){return this.Name},e.prototype.setName=function(e){return this.Name=e,this},e.prototype.toString=function(){return JSON.stringify(this)},e.prototype.toJSON=function(){return{Name:this.Name,Value:this.Value}},e}();t.default=r},function(e,t){"use strict";function n(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}t.__esModule=!0;/*! 57 | * Copyright 2016 Amazon.com, 58 | * Inc. or its affiliates. All Rights Reserved. 59 | * 60 | * Licensed under the Amazon Software License (the "License"). 61 | * You may not use this file except in compliance with the 62 | * License. A copy of the License is located at 63 | * 64 | * http://aws.amazon.com/asl/ 65 | * 66 | * or in the "license" file accompanying this file. This file is 67 | * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR 68 | * CONDITIONS OF ANY KIND, express or implied. See the License 69 | * for the specific language governing permissions and 70 | * limitations under the License. 71 | */ 72 | var r=function(){function e(){var t=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{},r=t.IdToken,i=t.RefreshToken,o=t.AccessToken,s=t.ClockDrift;if(n(this,e),null==o||null==r)throw new Error("Id token and Access Token must be present.");this.idToken=r,this.refreshToken=i,this.accessToken=o,this.clockDrift=void 0===s?this.calculateClockDrift():s}return e.prototype.getIdToken=function(){return this.idToken},e.prototype.getRefreshToken=function(){return this.refreshToken},e.prototype.getAccessToken=function(){return this.accessToken},e.prototype.getClockDrift=function(){return this.clockDrift},e.prototype.calculateClockDrift=function(){var e=Math.floor(new Date/1e3),t=Math.min(this.accessToken.getIssuedAt(),this.idToken.getIssuedAt());return e-t},e.prototype.isValid=function(){var e=Math.floor(new Date/1e3),t=e-this.clockDrift;return tp?t=e(t):t.length0)throw new Error("Invalid string. Length must be a multiple of 4");var n=e.indexOf("=");n===-1&&(n=t);var r=n===t?0:4-n%4;return[n,r]}function r(e){var t=n(e),r=t[0],i=t[1];return 3*(r+i)/4-i}function i(e,t,n){return 3*(t+n)/4-n}function o(e){for(var t,r=n(e),o=r[0],s=r[1],a=new f(i(e,o,s)),u=0,c=s>0?o-4:o,l=0;l>16&255,a[u++]=t>>8&255,a[u++]=255&t;return 2===s&&(t=h[e.charCodeAt(l)]<<2|h[e.charCodeAt(l+1)]>>4,a[u++]=255&t),1===s&&(t=h[e.charCodeAt(l)]<<10|h[e.charCodeAt(l+1)]<<4|h[e.charCodeAt(l+2)]>>2,a[u++]=t>>8&255,a[u++]=255&t),a}function s(e){return c[e>>18&63]+c[e>>12&63]+c[e>>6&63]+c[63&e]}function a(e,t,n){for(var r,i=[],o=t;ou?u:s+o));return 1===r?(t=e[n-1],i.push(c[t>>2]+c[t<<4&63]+"==")):2===r&&(t=(e[n-2]<<8)+e[n-1],i.push(c[t>>10]+c[t>>4&63]+c[t<<2&63]+"=")),i.join("")}t.byteLength=r,t.toByteArray=o,t.fromByteArray=u;for(var c=[],h=[],f="undefined"!=typeof Uint8Array?Uint8Array:Array,l="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/",p=0,d=l.length;p>1,h=-7,f=n?i-1:0,l=n?-1:1,p=e[t+f];for(f+=l,o=p&(1<<-h)-1,p>>=-h,h+=a;h>0;o=256*o+e[t+f],f+=l,h-=8);for(s=o&(1<<-h)-1,o>>=-h,h+=r;h>0;s=256*s+e[t+f],f+=l,h-=8);if(0===o)o=1-c;else{if(o===u)return s?NaN:(p?-1:1)*(1/0);s+=Math.pow(2,r),o-=c}return(p?-1:1)*s*Math.pow(2,o-r)},t.write=function(e,t,n,r,i,o){var s,a,u,c=8*o-i-1,h=(1<>1,l=23===i?Math.pow(2,-24)-Math.pow(2,-77):0,p=r?0:o-1,d=r?1:-1,g=t<0||0===t&&1/t<0?1:0;for(t=Math.abs(t),isNaN(t)||t===1/0?(a=isNaN(t)?1:0,s=h):(s=Math.floor(Math.log(t)/Math.LN2),t*(u=Math.pow(2,-s))<1&&(s--,u*=2),t+=s+f>=1?l/u:l*Math.pow(2,1-f),t*u>=2&&(s++,u/=2),s+f>=h?(a=0,s=h):s+f>=1?(a=(t*u-1)*Math.pow(2,i),s+=f):(a=t*Math.pow(2,f-1)*Math.pow(2,i),s=0));i>=8;e[n+p]=255&a,p+=d,a/=256,i-=8);for(s=s<0;e[n+p]=255&s,p+=d,s/=256,c-=8);e[n+p-d]|=128*g}},function(e,t){var n={}.toString;e.exports=Array.isArray||function(e){return"[object Array]"==n.call(e)}},function(e,t,n){var r,i;!function(o){var s=!1;if(r=o,i="function"==typeof r?r.call(t,n,t,e):r,!(void 0!==i&&(e.exports=i)),s=!0,e.exports=o(),s=!0,!s){var a=window.Cookies,u=window.Cookies=o();u.noConflict=function(){return window.Cookies=a,u}}}(function(){function e(){for(var e=0,t={};e1){if(o=e({path:"/"},r.defaults,o),"number"==typeof o.expires){var a=new Date;a.setMilliseconds(a.getMilliseconds()+864e5*o.expires),o.expires=a}o.expires=o.expires?o.expires.toUTCString():"";try{s=JSON.stringify(i),/^[\{\[]/.test(s)&&(i=s)}catch(e){}i=n.write?n.write(i,t):encodeURIComponent(String(i)).replace(/%(23|24|26|2B|3A|3C|3E|3D|2F|3F|40|5B|5D|5E|60|7B|7D|7C)/g,decodeURIComponent),t=encodeURIComponent(String(t)),t=t.replace(/%(23|24|26|2B|5E|60|7C)/g,decodeURIComponent),t=t.replace(/[\(\)]/g,escape);var u="";for(var c in o)o[c]&&(u+="; "+c,o[c]!==!0&&(u+="="+o[c]));return document.cookie=t+"="+i+u}t||(s={});for(var h=document.cookie?document.cookie.split("; "):[],f=/(%[0-9A-Z]{2})+/g,l=0;l>5]|=128<>>9<<4)+14]=t;for(var n=1732584193,r=-271733879,i=-1732584194,h=271733878,f=0;f>16)+(t>>16)+(n>>16);return r<<16|65535&n}function h(e,t){return e<>>32-t}var f=n(2);e.exports=function(e){return f.hash(e,r,16)}},function(e,t){!function(){var t,n,r=this;t=function(e){for(var t,t,n=new Array(e),r=0;r>>((3&r)<<3)&255;return n},r.crypto&&crypto.getRandomValues&&(n=function(e){var t=new Uint8Array(e);return crypto.getRandomValues(t),t}),e.exports=n||t}()},function(e,t,n){function r(e,t){e[t>>5]|=128<<24-t%32,e[(t+64>>9<<4)+15]=t;for(var n=Array(80),r=1732584193,u=-271733879,c=-1732584194,h=271733878,f=-1009589776,l=0;l>16)+(t>>16)+(n>>16);return r<<16|65535&n}function a(e,t){return e<>>32-t}var u=n(2);e.exports=function(e){return u.hash(e,r,20,!0)}},function(e,t,n){var r=n(2),i=function(e,t){var n=(65535&e)+(65535&t),r=(e>>16)+(t>>16)+(n>>16);return r<<16|65535&n},o=function(e,t){return e>>>t|e<<32-t},s=function(e,t){return e>>>t},a=function(e,t,n){return e&t^~e&n},u=function(e,t,n){return e&t^e&n^t&n},c=function(e){return o(e,2)^o(e,13)^o(e,22)},h=function(e){return o(e,6)^o(e,11)^o(e,25)},f=function(e){return o(e,7)^o(e,18)^s(e,3)},l=function(e){return o(e,17)^o(e,19)^s(e,10)},p=function(e,t){var n,r,o,s,p,d,g,v,y,m,S,w,A=new Array(1116352408,1899447441,3049323471,3921009573,961987163,1508970993,2453635748,2870763221,3624381080,310598401,607225278,1426881987,1925078388,2162078206,2614888103,3248222580,3835390401,4022224774,264347078,604807628,770255983,1249150122,1555081692,1996064986,2554220882,2821834349,2952996808,3210313671,3336571891,3584528711,113926993,338241895,666307205,773529912,1294757372,1396182291,1695183700,1986661051,2177026350,2456956037,2730485921,2820302411,3259730800,3345764771,3516065817,3600352804,4094571909,275423344,430227734,506948616,659060556,883997877,958139571,1322822218,1537002063,1747873779,1955562222,2024104815,2227730452,2361852424,2428436474,2756734187,3204031479,3329325298),C=new Array(1779033703,3144134277,1013904242,2773480762,1359893119,2600822924,528734635,1541459225),U=new Array(64);e[t>>5]|=128<<24-t%32,e[(t+64>>9<<4)+15]=t;for(var y=0;y { 5 | if (typeof message === 'object') { 6 | return JSON.stringify(message, null, 2); 7 | } else { 8 | return message; 9 | } 10 | }) 11 | .join(' '); 12 | $('#logs').append($(`
`).text(`[${new Date().toISOString()}] [${level}] ${text}\n`)); 13 | } 14 | 15 | console._error = console.error; 16 | console.error = function(...rest) { 17 | log('ERROR', Array.prototype.slice.call(rest)); 18 | console._error.apply(this, rest); 19 | }; 20 | 21 | console._warn = console.warn; 22 | console.warn = function(...rest) { 23 | log('WARN', Array.prototype.slice.call(rest)); 24 | console._warn.apply(this, rest); 25 | }; 26 | 27 | console._log = console.log; 28 | console.log = function(...rest) { 29 | log('INFO', Array.prototype.slice.call(rest)); 30 | console._log.apply(this, rest); 31 | }; 32 | } 33 | 34 | function getRandomClientId() { 35 | return Math.random() 36 | .toString(36) 37 | .substring(2) 38 | .toUpperCase(); 39 | } 40 | 41 | function getFormValues() { 42 | return { 43 | region: $('#region').val(), 44 | channelName: $('#channelName').val(), 45 | clientId: $('#clientId').val() || getRandomClientId(), 46 | sendVideo: $('#sendVideo').is(':checked'), 47 | sendAudio: $('#sendAudio').is(':checked'), 48 | openDataChannel: $('#openDataChannel').is(':checked'), 49 | widescreen: $('#widescreen').is(':checked'), 50 | fullscreen: $('#fullscreen').is(':checked'), 51 | useTrickleICE: $('#useTrickleICE').is(':checked'), 52 | natTraversalDisabled: $('#natTraversalDisabled').is(':checked'), 53 | forceTURN: $('#forceTURN').is(':checked'), 54 | username: $('#username').val(), 55 | endpoint: $('#endpoint').val() || null, 56 | password: $('#password').val(), 57 | }; 58 | } 59 | 60 | function toggleDataChannelElements() { 61 | if (getFormValues().openDataChannel) { 62 | $('.datachannel').removeClass('d-none'); 63 | } else { 64 | $('.datachannel').addClass('d-none'); 65 | } 66 | } 67 | 68 | function onStatsReport(report) { 69 | // TODO: Publish stats 70 | } 71 | 72 | window.addEventListener('error', function(event) { 73 | console.error(event.message); 74 | event.preventDefault(); 75 | }); 76 | 77 | window.addEventListener('unhandledrejection', function(event) { 78 | console.error(event.reason.toString()); 79 | event.preventDefault(); 80 | }); 81 | 82 | configureLogging(); 83 | 84 | $('#master-button').click(async () => { 85 | $('#form').addClass('d-none'); 86 | $('#master').removeClass('d-none'); 87 | 88 | const localView = $('#master .local-view')[0]; 89 | const remoteView = $('#master .remote-view')[0]; 90 | const localMessage = $('#master .local-message')[0]; 91 | const remoteMessage = $('#master .remote-message')[0]; 92 | const formValues = getFormValues(); 93 | 94 | $(remoteMessage).empty(); 95 | localMessage.value = ''; 96 | toggleDataChannelElements(); 97 | 98 | startMaster(localView, remoteView, formValues, onStatsReport, event => { 99 | remoteMessage.append(`${event.data}\n`); 100 | }); 101 | }); 102 | 103 | $('#stop-master-button').click(async () => { 104 | stopMaster(); 105 | 106 | $('#form').removeClass('d-none'); 107 | $('#master').addClass('d-none'); 108 | }); 109 | 110 | $('#viewer-button').click(async () => { 111 | $('#form').addClass('d-none'); 112 | $('#viewer').removeClass('d-none'); 113 | 114 | const localView = $('#viewer .local-view')[0]; 115 | const remoteView = $('#viewer .remote-view')[0]; 116 | const localMessage = $('#viewer .local-message')[0]; 117 | const remoteMessage = $('#viewer .remote-message')[0]; 118 | const formValues = getFormValues(); 119 | 120 | $(remoteMessage).empty(); 121 | localMessage.value = ''; 122 | toggleDataChannelElements(); 123 | 124 | startViewer(localView, remoteView, formValues, onStatsReport, event => { 125 | remoteMessage.append(`${event.data}\n`); 126 | }); 127 | }); 128 | 129 | $('#stop-viewer-button').click(async () => { 130 | stopViewer(); 131 | 132 | $('#form').removeClass('d-none'); 133 | $('#viewer').addClass('d-none'); 134 | }); 135 | 136 | $('#create-channel-button').click(async () => { 137 | const formValues = getFormValues(); 138 | 139 | createSignalingChannel(formValues); 140 | }); 141 | 142 | $('#master .send-message').click(async () => { 143 | const masterLocalMessage = $('#master .local-message')[0]; 144 | sendMasterMessage(masterLocalMessage.value); 145 | }); 146 | 147 | $('#viewer .send-message').click(async () => { 148 | const viewerLocalMessage = $('#viewer .local-message')[0]; 149 | sendViewerMessage(viewerLocalMessage.value); 150 | }); 151 | 152 | // Read/Write all of the fields to/from localStorage so that fields are not lost on refresh. 153 | const urlParams = new URLSearchParams(window.location.search); 154 | const fields = [ 155 | { field: 'channelName', type: 'text' }, 156 | { field: 'clientId', type: 'text' }, 157 | { field: 'region', type: 'text' }, 158 | { field: 'username', type: 'text' }, 159 | { field: 'password', type: 'text' }, 160 | { field: 'endpoint', type: 'text' }, 161 | { field: 'sendVideo', type: 'checkbox' }, 162 | { field: 'sendAudio', type: 'checkbox' }, 163 | { field: 'widescreen', type: 'radio', name: 'resolution' }, 164 | { field: 'fullscreen', type: 'radio', name: 'resolution' }, 165 | { field: 'openDataChannel', type: 'checkbox' }, 166 | { field: 'useTrickleICE', type: 'checkbox' }, 167 | { field: 'natTraversalEnabled', type: 'radio', name: 'natTraversal' }, 168 | { field: 'forceTURN', type: 'radio', name: 'natTraversal' }, 169 | { field: 'natTraversalDisabled', type: 'radio', name: 'natTraversal' }, 170 | ]; 171 | fields.forEach(({ field, type, name }) => { 172 | const id = '#' + field; 173 | 174 | // Read field from localStorage 175 | try { 176 | const localStorageValue = localStorage.getItem(field); 177 | if (localStorageValue) { 178 | if (type === 'checkbox' || type === 'radio') { 179 | $(id).prop('checked', localStorageValue === 'true'); 180 | } else { 181 | $(id).val(localStorageValue); 182 | } 183 | $(id).trigger('change'); 184 | } 185 | } catch (e) { 186 | /* Don't use localStorage */ 187 | } 188 | 189 | // Read field from query string 190 | if (urlParams.has(field)) { 191 | paramValue = urlParams.get(field); 192 | if (type === 'checkbox' || type === 'radio') { 193 | $(id).prop('checked', paramValue === 'true'); 194 | } else { 195 | $(id).val(paramValue); 196 | } 197 | } 198 | 199 | // Write field to localstorage on change event 200 | $(id).change(function() { 201 | try { 202 | if (type === 'checkbox') { 203 | localStorage.setItem(field, $(id).is(':checked')); 204 | } else if (type === 'radio') { 205 | fields 206 | .filter(fieldItem => fieldItem.name === name) 207 | .forEach(fieldItem => { 208 | localStorage.setItem(fieldItem.field, fieldItem.field === field); 209 | }); 210 | } else { 211 | localStorage.setItem(field, $(id).val()); 212 | } 213 | } catch (e) { 214 | /* Don't use localStorage */ 215 | } 216 | }); 217 | }); 218 | 219 | // The page is all setup. Hide the loading spinner and show the page content. 220 | $('.loader').addClass('d-none'); 221 | $('#main').removeClass('d-none'); 222 | console.log('Page loaded'); 223 | -------------------------------------------------------------------------------- /examples/createSignalingChannel.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This file demonstrates the process of creating a KVS Signaling Channel. 3 | */ 4 | 5 | async function createSignalingChannel(formValues) { 6 | // Create KVS client 7 | const kinesisVideoClient = new AWS.KinesisVideo({ 8 | region: formValues.region, 9 | endpoint: formValues.endpoint, 10 | }); 11 | 12 | // Get signaling channel ARN 13 | await kinesisVideoClient 14 | .createSignalingChannel({ 15 | ChannelName: formValues.channelName, 16 | }) 17 | .promise(); 18 | 19 | // Get signaling channel ARN 20 | const describeSignalingChannelResponse = await kinesisVideoClient 21 | .describeSignalingChannel({ 22 | ChannelName: formValues.channelName, 23 | }) 24 | .promise(); 25 | const channelARN = describeSignalingChannelResponse.ChannelInfo.ChannelARN; 26 | console.log('[CREATE_SIGNALING_CHANNEL] Channel ARN: ', channelARN); 27 | } 28 | -------------------------------------------------------------------------------- /examples/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/amazon-kinesis-video-streams-webrtc-sdk-js-with-amazon-cognito/ff11b8c20e1bc6e32136e519c6c43bf817b8b270/examples/favicon.ico -------------------------------------------------------------------------------- /examples/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | KVS WebRTC Test Page 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 |
20 |

KVS WebRTC Test Page

21 |

This is the KVS Signaling Channel WebRTC test page. Use this page to connect to a signaling channel as either the MASTER or as a VIEWER.

22 | 23 |
24 |
25 |
26 |

KVS Endpoint

27 |
28 | 29 | 30 |
31 |
32 | 33 | 34 |
35 |

AWS Credentials

36 |
37 | 38 | 39 |
40 |
41 | 42 | 43 |
44 |

Signaling Channel

45 |
46 | 47 | 48 |
49 |
50 | 51 | 52 |
53 |

Tracks

54 |

Control which media types are transmitted to the remote client.

55 |
56 |
57 | 58 | 59 |
60 |
61 | 62 | 63 |
64 |
65 | 66 | 67 |
68 |
69 |

Video Resolution

70 |

Set the desired video resolution and aspect ratio.

71 |
72 |
73 | 74 | 75 |
76 |
77 | 78 | 79 |
80 |
81 |

NAT Traversal

82 |

Control settings for ICE candidate generation.

83 |
84 |
85 | 86 | 87 |
88 |
89 | 90 | 91 |
92 |
93 | 94 | 95 |
96 |
97 |
98 |
99 | 100 | 101 |
102 |
103 |
104 | 105 | 106 | 107 |
108 |
109 | 110 |
111 |

Master

112 |
113 |
114 |
Master Section
115 |
116 |
117 |
118 |
Viewer Return Channel
119 |
120 |
121 |
122 |
123 |
124 |
125 | 126 |
127 |
128 |
129 |
130 |

131 |                     
132 |
133 |
134 |
135 | 136 | 137 | 138 | 139 |
140 |
141 | 142 |
143 |

Viewer

144 |
145 |
146 |
Return Channel
147 |
148 |
149 |
150 |
From Master
151 |
152 |
153 |
154 |
155 |
156 |
157 | 158 |
159 |
160 |
161 |
162 |

163 |                     
164 |
165 |
166 |
167 | 168 | 169 | 170 | 171 |
172 |
173 | 174 |

Logs

175 |
176 |

177 |         
178 | 179 |
180 |
181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | -------------------------------------------------------------------------------- /examples/loader.css: -------------------------------------------------------------------------------- 1 | .loader, 2 | .loader:after { 3 | border-radius: 50%; 4 | width: 10em; 5 | height: 10em; 6 | } 7 | 8 | .loader { 9 | margin: 60px auto; 10 | font-size: 10px; 11 | position: relative; 12 | text-indent: -9999em; 13 | border-top: 1.1em solid rgba(0, 0, 0, 0.2); 14 | border-right: 1.1em solid rgba(0, 0, 0, 0.2); 15 | border-bottom: 1.1em solid rgba(0, 0, 0, 0.2); 16 | border-left: 1.1em solid #000000; 17 | -webkit-transform: translateZ(0); 18 | -ms-transform: translateZ(0); 19 | transform: translateZ(0); 20 | -webkit-animation: load8 1.1s infinite linear; 21 | animation: load8 1.1s infinite linear; 22 | } 23 | @-webkit-keyframes load8 { 24 | 0% { 25 | -webkit-transform: rotate(0deg); 26 | transform: rotate(0deg); 27 | } 28 | 100% { 29 | -webkit-transform: rotate(360deg); 30 | transform: rotate(360deg); 31 | } 32 | } 33 | @keyframes load8 { 34 | 0% { 35 | -webkit-transform: rotate(0deg); 36 | transform: rotate(0deg); 37 | } 38 | 100% { 39 | -webkit-transform: rotate(360deg); 40 | transform: rotate(360deg); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /examples/master.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This file demonstrates the process of starting WebRTC streaming using a KVS Signaling Channel. 3 | */ 4 | 5 | const master = { 6 | signalingClient: null, 7 | peerConnectionByClientId: {}, 8 | dataChannelByClientId: {}, 9 | localStream: null, 10 | remoteStreams: [], 11 | peerConnectionStatsInterval: null, 12 | }; 13 | 14 | function getCredential(formValues, callback, err) { 15 | if (err) 16 | console.log(err); 17 | else { 18 | var authenticationData = { 19 | Username: formValues.username, 20 | Password: formValues.password, 21 | }; 22 | 23 | var authenticationDetails = new AmazonCognitoIdentity.AuthenticationDetails( 24 | authenticationData 25 | ); 26 | 27 | var poolData = { 28 | UserPoolId: '', // Your user pool id here 29 | ClientId: '', // Your client id here 30 | }; 31 | var userPool = new AmazonCognitoIdentity.CognitoUserPool(poolData); 32 | 33 | var userData = { 34 | Username: formValues.username, 35 | Pool: userPool, 36 | }; 37 | var cognitoUser = new AmazonCognitoIdentity.CognitoUser(userData); 38 | 39 | //console.log(AWS.config.credentials.accessKeyId) 40 | 41 | //this is the call where it throws an error in the first run 42 | cognitoUser.authenticateUser(authenticationDetails, { 43 | onSuccess: function(result) { 44 | var accessToken = result.getAccessToken().getJwtToken(); 45 | 46 | //POTENTIAL: Region needs to be set if not already set previously elsewhere. 47 | AWS.config.region = formValues.region; 48 | 49 | AWS.config.credentials = new AWS.CognitoIdentityCredentials({ 50 | IdentityPoolId: '', // your identity pool id here 51 | Logins: { 52 | // Change the key below according to the specific region your user pool is in. 53 | 'cognito-idp..amazonaws.com/': result 54 | .getIdToken() 55 | .getJwtToken(), 56 | }, 57 | }); 58 | 59 | //refreshes credentials using AWS.CognitoIdentity.getCredentialsForIdentity() 60 | AWS.config.credentials.refresh(error => { 61 | if (error) { 62 | console.error(error); 63 | } else { 64 | // Instantiate aws sdk service objects now that the credentials have been updated. 65 | // example: var s3 = new AWS.S3(); 66 | console.log('Successfully logged!'); 67 | callback(); 68 | } 69 | }); 70 | }, 71 | onFailure: function(err) { 72 | alert(err.message || JSON.stringify(err)); 73 | }, 74 | }); 75 | } 76 | } 77 | 78 | async function postMasterLogin(localView, remoteView, formValues, onStatsReport, onRemoteDataMessage){ 79 | master.localView = localView; 80 | master.remoteView = remoteView; 81 | 82 | 83 | // Create KVS client 84 | const kinesisVideoClient = new AWS.KinesisVideo({ 85 | region: formValues.region, 86 | endpoint: formValues.endpoint, 87 | correctClockSkew: true, 88 | }); 89 | 90 | // Get signaling channel ARN 91 | const describeSignalingChannelResponse = await kinesisVideoClient 92 | .describeSignalingChannel({ 93 | ChannelName: formValues.channelName, 94 | }) 95 | .promise(); 96 | const channelARN = describeSignalingChannelResponse.ChannelInfo.ChannelARN; 97 | console.log('[MASTER] Channel ARN: ', channelARN); 98 | 99 | // Get signaling channel endpoints 100 | const getSignalingChannelEndpointResponse = await kinesisVideoClient 101 | .getSignalingChannelEndpoint({ 102 | ChannelARN: channelARN, 103 | SingleMasterChannelEndpointConfiguration: { 104 | Protocols: ['WSS', 'HTTPS'], 105 | Role: KVSWebRTC.Role.MASTER, 106 | }, 107 | }) 108 | .promise(); 109 | const endpointsByProtocol = getSignalingChannelEndpointResponse.ResourceEndpointList.reduce((endpoints, endpoint) => { 110 | endpoints[endpoint.Protocol] = endpoint.ResourceEndpoint; 111 | return endpoints; 112 | }, {}); 113 | console.log('[MASTER] Endpoints: ', endpointsByProtocol); 114 | 115 | // Create Signaling Client 116 | master.signalingClient = new KVSWebRTC.SignalingClient({ 117 | channelARN, 118 | channelEndpoint: endpointsByProtocol.WSS, 119 | role: KVSWebRTC.Role.MASTER, 120 | region: formValues.region, 121 | credentials: { 122 | accessKeyId: AWS.config.credentials.accessKeyId, 123 | secretAccessKey: AWS.config.credentials.secretAccessKey, 124 | sessionToken: AWS.config.credentials.sessionToken, 125 | }, 126 | }); 127 | 128 | // Get ICE server configuration 129 | const kinesisVideoSignalingChannelsClient = new AWS.KinesisVideoSignalingChannels({ 130 | region: formValues.region, 131 | endpoint: endpointsByProtocol.HTTPS, 132 | correctClockSkew: true, 133 | 134 | }); 135 | const getIceServerConfigResponse = await kinesisVideoSignalingChannelsClient 136 | .getIceServerConfig({ 137 | ChannelARN: channelARN, 138 | }) 139 | .promise(); 140 | const iceServers = []; 141 | if (!formValues.natTraversalDisabled && !formValues.forceTURN) { 142 | iceServers.push({ urls: `stun:stun.kinesisvideo.${formValues.region}.amazonaws.com:443` }); 143 | } 144 | if (!formValues.natTraversalDisabled) { 145 | getIceServerConfigResponse.IceServerList.forEach(iceServer => 146 | iceServers.push({ 147 | urls: iceServer.Uris, 148 | username: iceServer.Username, 149 | credential: iceServer.Password, 150 | }), 151 | ); 152 | } 153 | console.log('[MASTER] ICE servers: ', iceServers); 154 | 155 | const configuration = { 156 | iceServers, 157 | iceTransportPolicy: formValues.forceTURN ? 'relay' : 'all', 158 | }; 159 | 160 | const resolution = formValues.widescreen ? { width: { ideal: 1280 }, height: { ideal: 720 } } : { width: { ideal: 640 }, height: { ideal: 480 } }; 161 | const constraints = { 162 | video: formValues.sendVideo ? resolution : false, 163 | audio: formValues.sendAudio, 164 | }; 165 | 166 | // Get a stream from the webcam and display it in the local view 167 | try { 168 | master.localStream = await navigator.mediaDevices.getUserMedia(constraints); 169 | localView.srcObject = master.localStream; 170 | } catch (e) { 171 | console.error('[MASTER] Could not find webcam'); 172 | } 173 | 174 | master.signalingClient.on('open', async () => { 175 | console.log('[MASTER] Connected to signaling service'); 176 | }); 177 | 178 | master.signalingClient.on('sdpOffer', async (offer, remoteClientId) => { 179 | console.log('[MASTER] Received SDP offer from client: ' + remoteClientId); 180 | 181 | // Create a new peer connection using the offer from the given client 182 | const peerConnection = new RTCPeerConnection(configuration); 183 | master.peerConnectionByClientId[remoteClientId] = peerConnection; 184 | 185 | if (formValues.openDataChannel) { 186 | master.dataChannelByClientId[remoteClientId] = peerConnection.createDataChannel('kvsDataChannel'); 187 | peerConnection.ondatachannel = event => { 188 | event.channel.onmessage = onRemoteDataMessage; 189 | }; 190 | } 191 | 192 | // Poll for connection stats 193 | if (!master.peerConnectionStatsInterval) { 194 | master.peerConnectionStatsInterval = setInterval(() => peerConnection.getStats().then(onStatsReport), 1000); 195 | } 196 | 197 | // Send any ICE candidates to the other peer 198 | peerConnection.addEventListener('icecandidate', ({ candidate }) => { 199 | if (candidate) { 200 | console.log('[MASTER] Generated ICE candidate for client: ' + remoteClientId); 201 | 202 | // When trickle ICE is enabled, send the ICE candidates as they are generated. 203 | if (formValues.useTrickleICE) { 204 | console.log('[MASTER] Sending ICE candidate to client: ' + remoteClientId); 205 | master.signalingClient.sendIceCandidate(candidate, remoteClientId); 206 | } 207 | } else { 208 | console.log('[MASTER] All ICE candidates have been generated for client: ' + remoteClientId); 209 | 210 | // When trickle ICE is disabled, send the answer now that all the ICE candidates have ben generated. 211 | if (!formValues.useTrickleICE) { 212 | console.log('[MASTER] Sending SDP answer to client: ' + remoteClientId); 213 | master.signalingClient.sendSdpAnswer(peerConnection.localDescription, remoteClientId); 214 | } 215 | } 216 | }); 217 | 218 | // As remote tracks are received, add them to the remote view 219 | peerConnection.addEventListener('track', event => { 220 | console.log('[MASTER] Received remote track from client: ' + remoteClientId); 221 | if (remoteView.srcObject) { 222 | return; 223 | } 224 | remoteView.srcObject = event.streams[0]; 225 | }); 226 | 227 | master.localStream.getTracks().forEach(track => peerConnection.addTrack(track, master.localStream)); 228 | await peerConnection.setRemoteDescription(offer); 229 | 230 | // Create an SDP answer to send back to the client 231 | console.log('[MASTER] Creating SDP answer for client: ' + remoteClientId); 232 | await peerConnection.setLocalDescription( 233 | await peerConnection.createAnswer({ 234 | offerToReceiveAudio: true, 235 | offerToReceiveVideo: true, 236 | }), 237 | ); 238 | 239 | // When trickle ICE is enabled, send the answer now and then send ICE candidates as they are generated. Otherwise wait on the ICE candidates. 240 | if (formValues.useTrickleICE) { 241 | console.log('[MASTER] Sending SDP answer to client: ' + remoteClientId); 242 | master.signalingClient.sendSdpAnswer(peerConnection.localDescription, remoteClientId); 243 | } 244 | console.log('[MASTER] Generating ICE candidates for client: ' + remoteClientId); 245 | }); 246 | 247 | master.signalingClient.on('iceCandidate', async (candidate, remoteClientId) => { 248 | console.log('[MASTER] Received ICE candidate from client: ' + remoteClientId); 249 | 250 | // Add the ICE candidate received from the client to the peer connection 251 | const peerConnection = master.peerConnectionByClientId[remoteClientId]; 252 | peerConnection.addIceCandidate(candidate); 253 | }); 254 | 255 | master.signalingClient.on('close', () => { 256 | console.log('[MASTER] Disconnected from signaling channel'); 257 | }); 258 | 259 | master.signalingClient.on('error', () => { 260 | console.error('[MASTER] Signaling client error'); 261 | }); 262 | 263 | console.log('[MASTER] Starting master connection'); 264 | master.signalingClient.open(); 265 | } 266 | 267 | function startMaster(localView, remoteView, formValues, onStatsReport, onRemoteDataMessage) { 268 | getCredential(formValues, function(){ 269 | postMasterLogin(localView, remoteView, formValues, onStatsReport, onRemoteDataMessage) 270 | }); 271 | } 272 | 273 | function stopMaster() { 274 | console.log('[MASTER] Stopping master connection'); 275 | if (master.signalingClient) { 276 | master.signalingClient.close(); 277 | master.signalingClient = null; 278 | } 279 | 280 | Object.keys(master.peerConnectionByClientId).forEach(clientId => { 281 | master.peerConnectionByClientId[clientId].close(); 282 | }); 283 | master.peerConnectionByClientId = []; 284 | 285 | if (master.localStream) { 286 | master.localStream.getTracks().forEach(track => track.stop()); 287 | master.localStream = null; 288 | } 289 | 290 | master.remoteStreams.forEach(remoteStream => remoteStream.getTracks().forEach(track => track.stop())); 291 | master.remoteStreams = []; 292 | 293 | if (master.peerConnectionStatsInterval) { 294 | clearInterval(master.peerConnectionStatsInterval); 295 | master.peerConnectionStatsInterval = null; 296 | } 297 | 298 | if (master.localView) { 299 | master.localView.srcObject = null; 300 | } 301 | 302 | if (master.remoteView) { 303 | master.remoteView.srcObject = null; 304 | } 305 | 306 | if (master.dataChannelByClientId) { 307 | master.dataChannelByClientId = {}; 308 | } 309 | } 310 | 311 | function sendMasterMessage(message) { 312 | Object.keys(master.dataChannelByClientId).forEach(clientId => { 313 | try { 314 | master.dataChannelByClientId[clientId].send(message); 315 | } catch (e) { 316 | console.error('[MASTER] Send DataChannel: ', e.toString()); 317 | } 318 | }); 319 | } -------------------------------------------------------------------------------- /examples/viewer.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This file demonstrates the process of starting WebRTC streaming using a KVS Signaling Channel. 3 | */ 4 | const viewer = {}; 5 | 6 | function getCredential(formValues, callback, err) { 7 | if (err) 8 | console.log(err); 9 | else { 10 | var authenticationData = { 11 | Username: formValues.username, 12 | Password: formValues.password, 13 | }; 14 | 15 | var authenticationDetails = new AmazonCognitoIdentity.AuthenticationDetails( 16 | authenticationData 17 | ); 18 | 19 | var poolData = { 20 | UserPoolId: '', // Your user pool id here 21 | ClientId: '', // Your client id here 22 | }; 23 | var userPool = new AmazonCognitoIdentity.CognitoUserPool(poolData); 24 | 25 | var userData = { 26 | Username: formValues.username, 27 | Pool: userPool, 28 | }; 29 | var cognitoUser = new AmazonCognitoIdentity.CognitoUser(userData); 30 | 31 | //console.log(AWS.config.credentials.accessKeyId) 32 | 33 | //this is the call where it throws an error in the first run 34 | cognitoUser.authenticateUser(authenticationDetails, { 35 | onSuccess: function(result) { 36 | var accessToken = result.getAccessToken().getJwtToken(); 37 | 38 | //POTENTIAL: Region needs to be set if not already set previously elsewhere. 39 | AWS.config.region = formValues.region; 40 | 41 | AWS.config.credentials = new AWS.CognitoIdentityCredentials({ 42 | IdentityPoolId: '', // your identity pool id here 43 | Logins: { 44 | // Change the key below according to the specific region your user pool is in. 45 | 'cognito-idp..amazonaws.com/': result 46 | .getIdToken() 47 | .getJwtToken(), 48 | }, 49 | }); 50 | 51 | //refreshes credentials using AWS.CognitoIdentity.getCredentialsForIdentity() 52 | AWS.config.credentials.refresh(error => { 53 | if (error) { 54 | console.error(error); 55 | } else { 56 | // Instantiate aws sdk service objects now that the credentials have been updated. 57 | // example: var s3 = new AWS.S3(); 58 | console.log('Successfully logged!'); 59 | callback(); 60 | } 61 | }); 62 | }, 63 | onFailure: function(err) { 64 | alert(err.message || JSON.stringify(err)); 65 | }, 66 | }); 67 | } 68 | } 69 | 70 | async function postViewerLogin(localView, remoteView, formValues, onStatsReport, onRemoteDataMessage) { 71 | viewer.localView = localView; 72 | viewer.remoteView = remoteView; 73 | 74 | // Create KVS client 75 | const kinesisVideoClient = new AWS.KinesisVideo({ 76 | region: formValues.region, 77 | endpoint: formValues.endpoint, 78 | correctClockSkew: true, 79 | }); 80 | 81 | // Get signaling channel ARN 82 | const describeSignalingChannelResponse = await kinesisVideoClient 83 | .describeSignalingChannel({ 84 | ChannelName: formValues.channelName, 85 | }) 86 | .promise(); 87 | const channelARN = describeSignalingChannelResponse.ChannelInfo.ChannelARN; 88 | console.log('[VIEWER] Channel ARN: ', channelARN); 89 | 90 | // Get signaling channel endpoints 91 | const getSignalingChannelEndpointResponse = await kinesisVideoClient 92 | .getSignalingChannelEndpoint({ 93 | ChannelARN: channelARN, 94 | SingleMasterChannelEndpointConfiguration: { 95 | Protocols: ['WSS', 'HTTPS'], 96 | Role: KVSWebRTC.Role.VIEWER, 97 | }, 98 | }) 99 | .promise(); 100 | const endpointsByProtocol = getSignalingChannelEndpointResponse.ResourceEndpointList.reduce((endpoints, endpoint) => { 101 | endpoints[endpoint.Protocol] = endpoint.ResourceEndpoint; 102 | return endpoints; 103 | }, {}); 104 | console.log('[VIEWER] Endpoints: ', endpointsByProtocol); 105 | 106 | const kinesisVideoSignalingChannelsClient = new AWS.KinesisVideoSignalingChannels({ 107 | region: formValues.region, 108 | endpoint: endpointsByProtocol.HTTPS, 109 | correctClockSkew: true, 110 | }); 111 | 112 | // Get ICE server configuration 113 | const getIceServerConfigResponse = await kinesisVideoSignalingChannelsClient 114 | .getIceServerConfig({ 115 | ChannelARN: channelARN, 116 | }) 117 | .promise(); 118 | const iceServers = []; 119 | if (!formValues.natTraversalDisabled && !formValues.forceTURN) { 120 | iceServers.push({ urls: `stun:stun.kinesisvideo.${formValues.region}.amazonaws.com:443` }); 121 | } 122 | if (!formValues.natTraversalDisabled) { 123 | getIceServerConfigResponse.IceServerList.forEach(iceServer => 124 | iceServers.push({ 125 | urls: iceServer.Uris, 126 | username: iceServer.Username, 127 | credential: iceServer.Password, 128 | }), 129 | ); 130 | } 131 | console.log('[VIEWER] ICE servers: ', iceServers); 132 | 133 | // Create Signaling Client 134 | viewer.signalingClient = new KVSWebRTC.SignalingClient({ 135 | channelARN, 136 | channelEndpoint: endpointsByProtocol.WSS, 137 | clientId: formValues.clientId, 138 | role: KVSWebRTC.Role.VIEWER, 139 | region: formValues.region, 140 | credentials: { 141 | accessKeyId: AWS.config.credentials.accessKeyId, 142 | secretAccessKey: AWS.config.credentials.secretAccessKey, 143 | sessionToken: AWS.config.credentials.sessionToken, 144 | }, 145 | }); 146 | 147 | const resolution = formValues.widescreen ? { width: { ideal: 1280 }, height: { ideal: 720 } } : { width: { ideal: 640 }, height: { ideal: 480 } }; 148 | const constraints = { 149 | video: formValues.sendVideo ? resolution : false, 150 | audio: formValues.sendAudio, 151 | }; 152 | const configuration = { 153 | iceServers, 154 | iceTransportPolicy: formValues.forceTURN ? 'relay' : 'all', 155 | }; 156 | viewer.peerConnection = new RTCPeerConnection(configuration); 157 | if (formValues.openDataChannel) { 158 | viewer.dataChannel = viewer.peerConnection.createDataChannel('kvsDataChannel'); 159 | viewer.peerConnection.ondatachannel = event => { 160 | event.channel.onmessage = onRemoteDataMessage; 161 | }; 162 | } 163 | 164 | // Poll for connection stats 165 | viewer.peerConnectionStatsInterval = setInterval(() => viewer.peerConnection.getStats().then(onStatsReport), 1000); 166 | 167 | viewer.signalingClient.on('open', async () => { 168 | console.log('[VIEWER] Connected to signaling service'); 169 | 170 | // Get a stream from the webcam, add it to the peer connection, and display it in the local view 171 | try { 172 | viewer.localStream = await navigator.mediaDevices.getUserMedia(constraints); 173 | viewer.localStream.getTracks().forEach(track => viewer.peerConnection.addTrack(track, viewer.localStream)); 174 | localView.srcObject = viewer.localStream; 175 | } catch (e) { 176 | console.error('[VIEWER] Could not find webcam'); 177 | return; 178 | } 179 | 180 | // Create an SDP offer to send to the master 181 | console.log('[VIEWER] Creating SDP offer'); 182 | await viewer.peerConnection.setLocalDescription( 183 | await viewer.peerConnection.createOffer({ 184 | offerToReceiveAudio: true, 185 | offerToReceiveVideo: true, 186 | }), 187 | ); 188 | 189 | // When trickle ICE is enabled, send the offer now and then send ICE candidates as they are generated. Otherwise wait on the ICE candidates. 190 | if (formValues.useTrickleICE) { 191 | console.log('[VIEWER] Sending SDP offer'); 192 | viewer.signalingClient.sendSdpOffer(viewer.peerConnection.localDescription); 193 | } 194 | console.log('[VIEWER] Generating ICE candidates'); 195 | }); 196 | 197 | viewer.signalingClient.on('sdpAnswer', async answer => { 198 | // Add the SDP answer to the peer connection 199 | console.log('[VIEWER] Received SDP answer'); 200 | await viewer.peerConnection.setRemoteDescription(answer); 201 | }); 202 | 203 | viewer.signalingClient.on('iceCandidate', candidate => { 204 | // Add the ICE candidate received from the MASTER to the peer connection 205 | console.log('[VIEWER] Received ICE candidate'); 206 | viewer.peerConnection.addIceCandidate(candidate); 207 | }); 208 | 209 | viewer.signalingClient.on('close', () => { 210 | console.log('[VIEWER] Disconnected from signaling channel'); 211 | }); 212 | 213 | viewer.signalingClient.on('error', error => { 214 | console.error('[VIEWER] Signaling client error: ', error); 215 | }); 216 | 217 | // Send any ICE candidates to the other peer 218 | viewer.peerConnection.addEventListener('icecandidate', ({ candidate }) => { 219 | if (candidate) { 220 | console.log('[VIEWER] Generated ICE candidate'); 221 | 222 | // When trickle ICE is enabled, send the ICE candidates as they are generated. 223 | if (formValues.useTrickleICE) { 224 | console.log('[VIEWER] Sending ICE candidate'); 225 | viewer.signalingClient.sendIceCandidate(candidate); 226 | } 227 | } else { 228 | console.log('[VIEWER] All ICE candidates have been generated'); 229 | 230 | // When trickle ICE is disabled, send the offer now that all the ICE candidates have ben generated. 231 | if (!formValues.useTrickleICE) { 232 | console.log('[VIEWER] Sending SDP offer'); 233 | viewer.signalingClient.sendSdpOffer(viewer.peerConnection.localDescription); 234 | } 235 | } 236 | }); 237 | 238 | // As remote tracks are received, add them to the remote view 239 | viewer.peerConnection.addEventListener('track', event => { 240 | console.log('[VIEWER] Received remote track'); 241 | if (remoteView.srcObject) { 242 | return; 243 | } 244 | viewer.remoteStream = event.streams[0]; 245 | remoteView.srcObject = viewer.remoteStream; 246 | }); 247 | 248 | console.log('[VIEWER] Starting viewer connection'); 249 | viewer.signalingClient.open(); 250 | 251 | } 252 | 253 | function startViewer(localView, remoteView, formValues, onStatsReport, onRemoteDataMessage) { 254 | getCredential(formValues, function(){ 255 | postViewerLogin(localView, remoteView, formValues, onStatsReport, onRemoteDataMessage) 256 | }); 257 | } 258 | 259 | function stopViewer() { 260 | console.log('[VIEWER] Stopping viewer connection'); 261 | if (viewer.signalingClient) { 262 | viewer.signalingClient.close(); 263 | viewer.signalingClient = null; 264 | } 265 | 266 | if (viewer.peerConnection) { 267 | viewer.peerConnection.close(); 268 | viewer.peerConnection = null; 269 | } 270 | 271 | if (viewer.localStream) { 272 | viewer.localStream.getTracks().forEach(track => track.stop()); 273 | viewer.localStream = null; 274 | } 275 | 276 | if (viewer.remoteStream) { 277 | viewer.remoteStream.getTracks().forEach(track => track.stop()); 278 | viewer.remoteStream = null; 279 | } 280 | 281 | if (viewer.peerConnectionStatsInterval) { 282 | clearInterval(viewer.peerConnectionStatsInterval); 283 | viewer.peerConnectionStatsInterval = null; 284 | } 285 | 286 | if (viewer.localView) { 287 | viewer.localView.srcObject = null; 288 | } 289 | 290 | if (viewer.remoteView) { 291 | viewer.remoteView.srcObject = null; 292 | } 293 | 294 | if (viewer.dataChannel) { 295 | viewer.dataChannel = null; 296 | } 297 | } 298 | 299 | function sendViewerMessage(message) { 300 | if (viewer.dataChannel) { 301 | try { 302 | viewer.dataChannel.send(message); 303 | } catch (e) { 304 | console.error('[VIEWER] Send DataChannel: ', e.toString()); 305 | } 306 | } 307 | } 308 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | process.env.PACKAGE_VERSION = 'test.test.test'; 2 | 3 | module.exports = { 4 | collectCoverage: true, 5 | coverageThreshold: { 6 | global: { 7 | branches: 100, 8 | functions: 100, 9 | lines: 100, 10 | statements: 100, 11 | }, 12 | }, 13 | roots: ['/src'], 14 | testMatch: ['**/*.spec.ts'], 15 | transform: { 16 | '^.+\\.ts$': 'ts-jest', 17 | }, 18 | }; 19 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "amazon-kinesis-video-streams-webrtc", 3 | "version": "1.0.8", 4 | "description": "Amazon Kinesis Video Streams WebRTC SDK for JavaScript.", 5 | "repository": { 6 | "type": "git", 7 | "url": "https://github.com/awslabs/amazon-kinesis-video-streams-webrtc-sdk-js.git" 8 | }, 9 | "main": "lib/index.js", 10 | "types": "lib/index.d.ts", 11 | "files": [ 12 | "lib/**/*", 13 | "dist/**/*", 14 | "examples/**/*", 15 | "LICENSE.txt", 16 | "NOTICE.txt" 17 | ], 18 | "scripts": { 19 | "integ-server": "ws -p 3000", 20 | "test": "jest --runInBand", 21 | "test-watch": "jest --watch", 22 | "build-all": "npm run build-commonjs && npm run build-debug && npm run build-dist", 23 | "build-commonjs": "tsc -p tsconfig.json", 24 | "build-debug": "webpack --config webpack.debug.config.js", 25 | "build-dist": "webpack --config webpack.dist.config.js", 26 | "copy-examples-to-dist": "cp -r examples dist", 27 | "develop": "webpack-dev-server --config webpack.dev.config.js", 28 | "lint": "eslint 'src/**/*.{js,ts}'", 29 | "release": "npm run lint && npm run test && npm run build-all && npm run copy-examples-to-dist" 30 | }, 31 | "author": "Mitchell Loeppky ", 32 | "license": "Apache-2.0", 33 | "devDependencies": { 34 | "@trust/webcrypto": "^0.9.2", 35 | "@types/jest": "^24.0.23", 36 | "@typescript-eslint/eslint-plugin": "^2.2.0", 37 | "@typescript-eslint/parser": "^2.2.0", 38 | "codecov": "^3.7.1", 39 | "eslint": "^6.3.0", 40 | "eslint-config-prettier": "^6.3.0", 41 | "eslint-plugin-kvs-webrtc": "file:eslint", 42 | "eslint-plugin-prettier": "^3.1.0", 43 | "fork-ts-checker-webpack-plugin": "^4.1.2", 44 | "jest": "^25.5.4", 45 | "license-webpack-plugin": "^2.1.2", 46 | "prettier": "^1.18.2", 47 | "ts-jest": "^25.4.0", 48 | "ts-loader": "^6.0.4", 49 | "typescript": "^3.6.2", 50 | "webpack": "^4.39.3", 51 | "webpack-cli": "^3.3.11", 52 | "webpack-dev-server": "^5.0.4", 53 | "webpack-merge": "^4.2.2" 54 | }, 55 | "dependencies": { 56 | "isomorphic-webcrypto": "^2.3.6", 57 | "tslib": "^1.10.0" 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/amazon-kinesis-video-streams-webrtc-sdk-js-with-amazon-cognito/ff11b8c20e1bc6e32136e519c6c43bf817b8b270/src/.DS_Store -------------------------------------------------------------------------------- /src/Role.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Signaling client role. 3 | */ 4 | export enum Role { 5 | MASTER = 'MASTER', 6 | VIEWER = 'VIEWER', 7 | } 8 | -------------------------------------------------------------------------------- /src/SigV4RequestSigner.spec.ts: -------------------------------------------------------------------------------- 1 | import crypto from '@trust/webcrypto'; 2 | 3 | import { SigV4RequestSigner } from './SigV4RequestSigner'; 4 | import { Credentials, QueryParams } from './SignalingClient'; 5 | 6 | describe('SigV4RequestSigner', () => { 7 | let region: string; 8 | let credentials: Credentials; 9 | let signer: SigV4RequestSigner; 10 | let queryParams: QueryParams; 11 | let date: Date; 12 | 13 | // eslint-disable-next-line @typescript-eslint/ban-ts-ignore 14 | // @ts-ignore 15 | global.crypto = crypto; 16 | 17 | beforeEach(() => { 18 | region = 'us-west-2'; 19 | credentials = { 20 | accessKeyId: 'AKIA4F7WJQR7FMMWMNXI', 21 | secretAccessKey: 'FakeSecretKey', 22 | sessionToken: 'FakeSessionToken', 23 | }; 24 | signer = new SigV4RequestSigner(region, credentials); 25 | queryParams = { 26 | 'X-Amz-TestParam': 'test-param-value', 27 | }; 28 | date = new Date('2019-12-01T00:00:00.000Z'); 29 | }); 30 | 31 | describe('getSignedURL', () => { 32 | it('should fail when the endpoint is not a WSS endpoint', async () => { 33 | await expect(signer.getSignedURL('https://kvs.awsamazon.com', queryParams, date)).rejects.toBeTruthy(); 34 | }); 35 | 36 | it('should fail when the endpoint contains query params', async () => { 37 | await expect(signer.getSignedURL('wss://kvs.awsamazon.com?a=b', queryParams, date)).rejects.toBeTruthy(); 38 | }); 39 | 40 | const expectedSignedURL = 41 | 'wss://kvs.awsamazon.com/?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=AKIA4F7WJQR7FMMWMNXI%2F20191201%2Fus-west-2%2Fkinesisvideo%2Faws4_request&X-Amz-Date=20191201T000000Z&X-Amz-Expires=299&X-Amz-Security-Token=FakeSessionToken&X-Amz-Signature=fc268038be276315822b4f73eafd28ee3a5632a2a35fdb0a88db9a42b13d6c92&X-Amz-SignedHeaders=host&X-Amz-TestParam=test-param-value'; 42 | it('should generate a valid signed URL with static credentials', async () => { 43 | await expect(signer.getSignedURL('wss://kvs.awsamazon.com', queryParams, date)).resolves.toBe(expectedSignedURL); 44 | }); 45 | 46 | it('should generate a valid signed URL with dynamic credentials', async () => { 47 | credentials = { 48 | accessKeyId: null, 49 | secretAccessKey: null, 50 | getPromise(): Promise { 51 | return new Promise(resolve => { 52 | credentials.accessKeyId = 'AKIA4F7WJQR7FMMWMNXI'; 53 | credentials.secretAccessKey = 'FakeSecretKey'; 54 | credentials.sessionToken = 'FakeSessionToken'; 55 | resolve(); 56 | }); 57 | }, 58 | }; 59 | signer = new SigV4RequestSigner(region, credentials); 60 | await expect(signer.getSignedURL('wss://kvs.awsamazon.com', queryParams, date)).resolves.toBe(expectedSignedURL); 61 | }); 62 | 63 | it('should generate a valid signed URL without a session token', async () => { 64 | delete credentials.sessionToken; 65 | signer = new SigV4RequestSigner(region, credentials); 66 | await expect(signer.getSignedURL('wss://kvs.awsamazon.com', queryParams, date)).resolves.toBe( 67 | 'wss://kvs.awsamazon.com/?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=AKIA4F7WJQR7FMMWMNXI%2F20191201%2Fus-west-2%2Fkinesisvideo%2Faws4_request&X-Amz-Date=20191201T000000Z&X-Amz-Expires=299&X-Amz-Signature=be1e78950d956a8a9a1997417099ddbd7455619f3d08c4ad20e1e272179ca695&X-Amz-SignedHeaders=host&X-Amz-TestParam=test-param-value', 68 | ); 69 | }); 70 | 71 | it('should generate a valid signed URL with a service override', async () => { 72 | signer = new SigV4RequestSigner(region, credentials, 'firehose'); 73 | await expect(signer.getSignedURL('wss://kvs.awsamazon.com', queryParams, date)).resolves.toBe( 74 | 'wss://kvs.awsamazon.com/?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=AKIA4F7WJQR7FMMWMNXI%2F20191201%2Fus-west-2%2Ffirehose%2Faws4_request&X-Amz-Date=20191201T000000Z&X-Amz-Expires=299&X-Amz-Security-Token=FakeSessionToken&X-Amz-Signature=f15308513d21a381d38b7607a0439f25fc2e6c9f5ff56a48c1664b486e6234d5&X-Amz-SignedHeaders=host&X-Amz-TestParam=test-param-value', 75 | ); 76 | }); 77 | 78 | it('should generate a valid signed URL with a path', async () => { 79 | await expect(signer.getSignedURL('wss://kvs.awsamazon.com/path/path/path', queryParams, date)).resolves.toBe( 80 | 'wss://kvs.awsamazon.com/path/path/path?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=AKIA4F7WJQR7FMMWMNXI%2F20191201%2Fus-west-2%2Fkinesisvideo%2Faws4_request&X-Amz-Date=20191201T000000Z&X-Amz-Expires=299&X-Amz-Security-Token=FakeSessionToken&X-Amz-Signature=0bf3df6ca23d8d82f688e8dbfb90d69e74843d40038541b1721c545eef7612a4&X-Amz-SignedHeaders=host&X-Amz-TestParam=test-param-value', 81 | ); 82 | }); 83 | 84 | it('should generate a valid signed URL without a mocked date', async () => { 85 | await expect(signer.getSignedURL('wss://kvs.awsamazon.com', queryParams)).resolves.toBeTruthy(); 86 | }); 87 | 88 | it ('should generate a valid signed URL with query parameter override', async () => { 89 | signer = new SigV4RequestSigner(region, credentials); 90 | queryParams = { 91 | 'X-Amz-TestParam': 'test-param-value', 92 | 'X-Amz-Expires': '86400', // should override the default of 299 seconds 93 | }; 94 | await expect(signer.getSignedURL('wss://kvs.awsamazon.com', queryParams, date)).resolves.toBe( 95 | 'wss://kvs.awsamazon.com/?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=AKIA4F7WJQR7FMMWMNXI%2F20191201%2Fus-west-2%2Fkinesisvideo%2Faws4_request&X-Amz-Date=20191201T000000Z&X-Amz-Expires=86400&X-Amz-Security-Token=FakeSessionToken&X-Amz-Signature=b62a078631a8f1e31ad09bce25d251611a22b65eac8836f6d700cee50a04e9e1&X-Amz-SignedHeaders=host&X-Amz-TestParam=test-param-value' 96 | ); 97 | }) 98 | }); 99 | }); 100 | -------------------------------------------------------------------------------- /src/SigV4RequestSigner.ts: -------------------------------------------------------------------------------- 1 | import { Credentials, QueryParams, RequestSigner } from './SignalingClient'; 2 | import { validateValueNonNil } from './internal/utils'; 3 | type Headers = { [header: string]: string }; 4 | 5 | /** 6 | * Utility class for SigV4 signing requests. The AWS SDK cannot be used for this purpose because it does not have support for WebSocket endpoints. 7 | */ 8 | export class SigV4RequestSigner implements RequestSigner { 9 | private static readonly DEFAULT_ALGORITHM = 'AWS4-HMAC-SHA256'; 10 | private static readonly DEFAULT_SERVICE = 'kinesisvideo'; 11 | 12 | private readonly region: string; 13 | private readonly credentials: Credentials; 14 | private readonly service: string; 15 | 16 | public constructor(region: string, credentials: Credentials, service: string = SigV4RequestSigner.DEFAULT_SERVICE) { 17 | this.region = region; 18 | this.credentials = credentials; 19 | this.service = service; 20 | } 21 | 22 | /** 23 | * Creates a SigV4 signed WebSocket URL for the given host/endpoint with the given query params. 24 | * 25 | * @param endpoint The WebSocket service endpoint including protocol, hostname, and path (if applicable). 26 | * @param queryParams Query parameters to include in the URL. 27 | * @param date Date to use for request signing. Defaults to NOW. 28 | * 29 | * Implementation note: Query parameters should be in alphabetical order. 30 | * 31 | * Note from AWS docs: "When you add the X-Amz-Security-Token parameter to the query string, some services require that you include this parameter in the 32 | * canonical (signed) request. For other services, you add this parameter at the end, after you calculate the signature. For details, see the API reference 33 | * documentation for that service." KVS Signaling Service requires that the session token is added to the canonical request. 34 | * 35 | * @see https://docs.aws.amazon.com/AmazonS3/latest/API/sigv4-query-string-auth.html 36 | * @see https://gist.github.com/prestomation/24b959e51250a8723b9a5a4f70dcae08 37 | */ 38 | public async getSignedURL(endpoint: string, queryParams: QueryParams, date: Date = new Date()): Promise { 39 | // Refresh credentials 40 | if (typeof this.credentials.getPromise === 'function') { 41 | await this.credentials.getPromise(); 42 | } 43 | validateValueNonNil(this.credentials.accessKeyId, 'credentials.accessKeyId'); 44 | validateValueNonNil(this.credentials.secretAccessKey, 'credentials.secretAccessKey'); 45 | 46 | // Prepare date strings 47 | const datetimeString = SigV4RequestSigner.getDateTimeString(date); 48 | const dateString = SigV4RequestSigner.getDateString(date); 49 | 50 | // Validate and parse endpoint 51 | const protocol = 'wss'; 52 | const urlProtocol = `${protocol}://`; 53 | if (!endpoint.startsWith(urlProtocol)) { 54 | throw new Error(`Endpoint '${endpoint}' is not a secure WebSocket endpoint. It should start with '${urlProtocol}'.`); 55 | } 56 | if (endpoint.includes('?')) { 57 | throw new Error(`Endpoint '${endpoint}' should not contain any query parameters.`); 58 | } 59 | const pathStartIndex = endpoint.indexOf('/', urlProtocol.length); 60 | let host; 61 | let path; 62 | if (pathStartIndex < 0) { 63 | host = endpoint.substring(urlProtocol.length); 64 | path = '/'; 65 | } else { 66 | host = endpoint.substring(urlProtocol.length, pathStartIndex); 67 | path = endpoint.substring(pathStartIndex); 68 | } 69 | 70 | const signedHeaders = ['host'].join(';'); 71 | 72 | // Prepare method 73 | const method = 'GET'; // Method is always GET for signed URLs 74 | 75 | // Prepare canonical query string 76 | const credentialScope = dateString + '/' + this.region + '/' + this.service + '/' + 'aws4_request'; 77 | const canonicalQueryParams = Object.assign({}, { 78 | 'X-Amz-Algorithm': SigV4RequestSigner.DEFAULT_ALGORITHM, 79 | 'X-Amz-Credential': this.credentials.accessKeyId + '/' + credentialScope, 80 | 'X-Amz-Date': datetimeString, 81 | 'X-Amz-Expires': '299', 82 | 'X-Amz-SignedHeaders': signedHeaders, 83 | }, queryParams); 84 | if (this.credentials.sessionToken) { 85 | Object.assign(canonicalQueryParams, { 86 | 'X-Amz-Security-Token': this.credentials.sessionToken, 87 | }); 88 | } 89 | const canonicalQueryString = SigV4RequestSigner.createQueryString(canonicalQueryParams); 90 | 91 | // Prepare canonical headers 92 | const canonicalHeaders = { 93 | host, 94 | }; 95 | const canonicalHeadersString = SigV4RequestSigner.createHeadersString(canonicalHeaders); 96 | 97 | // Prepare payload hash 98 | const payloadHash = await SigV4RequestSigner.sha256(''); 99 | 100 | // Combine canonical request parts into a canonical request string and hash 101 | const canonicalRequest = [method, path, canonicalQueryString, canonicalHeadersString, signedHeaders, payloadHash].join('\n'); 102 | const canonicalRequestHash = await SigV4RequestSigner.sha256(canonicalRequest); 103 | 104 | // Create signature 105 | const stringToSign = [SigV4RequestSigner.DEFAULT_ALGORITHM, datetimeString, credentialScope, canonicalRequestHash].join('\n'); 106 | const signingKey = await this.getSignatureKey(dateString); 107 | const signature = await SigV4RequestSigner.toHex(await SigV4RequestSigner.hmac(signingKey, stringToSign)); 108 | 109 | // Add signature to query params 110 | const signedQueryParams = Object.assign({}, canonicalQueryParams, { 111 | 'X-Amz-Signature': signature, 112 | }); 113 | 114 | // Create signed URL 115 | return protocol + '://' + host + path + '?' + SigV4RequestSigner.createQueryString(signedQueryParams); 116 | } 117 | 118 | /** 119 | * Utility method for generating the key to use for calculating the signature. This combines together the date string, region, service name, and secret 120 | * access key. 121 | * 122 | * @see https://docs.aws.amazon.com/general/latest/gr/sigv4-calculate-signature.html 123 | */ 124 | private async getSignatureKey(dateString: string): Promise { 125 | const kDate = await SigV4RequestSigner.hmac('AWS4' + this.credentials.secretAccessKey, dateString); 126 | const kRegion = await SigV4RequestSigner.hmac(kDate, this.region); 127 | const kService = await SigV4RequestSigner.hmac(kRegion, this.service); 128 | return await SigV4RequestSigner.hmac(kService, 'aws4_request'); 129 | } 130 | 131 | /** 132 | * Utility method for converting a map of headers to a string for signing. 133 | */ 134 | private static createHeadersString(headers: Headers): string { 135 | return Object.keys(headers) 136 | .map(header => `${header}:${headers[header]}\n`) 137 | .join(); 138 | } 139 | 140 | /** 141 | * Utility method for converting a map of query parameters to a string with the parameter names sorted. 142 | */ 143 | private static createQueryString(queryParams: QueryParams): string { 144 | return Object.keys(queryParams) 145 | .sort() 146 | .map(key => `${key}=${encodeURIComponent(queryParams[key])}`) 147 | .join('&'); 148 | } 149 | 150 | /** 151 | * Gets a datetime string for the given date to use for signing. For example: "20190927T165210Z" 152 | * @param date 153 | */ 154 | private static getDateTimeString(date: Date): string { 155 | return date 156 | .toISOString() 157 | .replace(/\.\d{3}Z$/, 'Z') 158 | .replace(/[:\-]/g, ''); 159 | } 160 | 161 | /** 162 | * Gets a date string for the given date to use for signing. For example: "20190927" 163 | * @param date 164 | */ 165 | private static getDateString(date: Date): string { 166 | return this.getDateTimeString(date).substring(0, 8); 167 | } 168 | 169 | private static async sha256(message: string): Promise { 170 | const hashBuffer = await crypto.subtle.digest({ name: 'SHA-256' }, this.toUint8Array(message)); 171 | return this.toHex(hashBuffer); 172 | } 173 | 174 | private static async hmac(key: string | ArrayBuffer, message: string): Promise { 175 | const keyBuffer = typeof key === 'string' ? this.toUint8Array(key).buffer : key; 176 | const messageBuffer = this.toUint8Array(message).buffer; 177 | const cryptoKey = await crypto.subtle.importKey( 178 | 'raw', 179 | keyBuffer, 180 | { 181 | name: 'HMAC', 182 | hash: { 183 | name: 'SHA-256', 184 | }, 185 | }, 186 | false, 187 | ['sign'], 188 | ); 189 | return await crypto.subtle.sign('HMAC', cryptoKey, messageBuffer); 190 | } 191 | 192 | /** 193 | * Note that this implementation does not work with two-byte characters. 194 | * However, no inputs into a signed signaling service request should have two-byte characters. 195 | */ 196 | private static toUint8Array(input: string): Uint8Array { 197 | const buf = new ArrayBuffer(input.length); 198 | const bufView = new Uint8Array(buf); 199 | for (let i = 0, strLen = input.length; i < strLen; i++) { 200 | bufView[i] = input.charCodeAt(i); 201 | } 202 | return bufView; 203 | } 204 | 205 | private static toHex(buffer: ArrayBuffer): string { 206 | return Array.from(new Uint8Array(buffer)) 207 | .map(b => b.toString(16).padStart(2, '0')) 208 | .join(''); 209 | } 210 | } 211 | -------------------------------------------------------------------------------- /src/SignalingClient.spec.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-explicit-any */ 2 | import crypto from '@trust/webcrypto'; 3 | import { EventEmitter } from 'events'; 4 | import * as util from 'util'; 5 | 6 | import { Role } from './Role'; 7 | import { SignalingClient, SignalingClientConfig } from './SignalingClient'; 8 | 9 | const RealWebSocket = window.WebSocket; 10 | 11 | const ENDPOINT = 'wss://endpoint.kinesisvideo.amazonaws.com'; 12 | const CHANNEL_ARN = 'arn:aws:kinesisvideo:us-west-2:123456789012:channel/testChannel/1234567890'; 13 | const CLIENT_ID = 'TestClientId'; 14 | const SDP_OFFER_OBJECT = { 15 | sdp: 'offer= true\nvideo= true', 16 | type: 'offer', 17 | }; 18 | const SDP_OFFER: RTCSessionDescription = { 19 | ...SDP_OFFER_OBJECT, 20 | toJSON: () => SDP_OFFER_OBJECT, 21 | } as any; 22 | const SDP_OFFER_VIEWER_STRING = '{"action":"SDP_OFFER","messagePayload":"eyJzZHAiOiJvZmZlcj0gdHJ1ZVxudmlkZW89IHRydWUiLCJ0eXBlIjoib2ZmZXIifQ=="}'; 23 | const SDP_OFFER_MASTER_STRING = 24 | '{"action":"SDP_OFFER","messagePayload":"eyJzZHAiOiJvZmZlcj0gdHJ1ZVxudmlkZW89IHRydWUiLCJ0eXBlIjoib2ZmZXIifQ==","recipientClientId":"TestClientId"}'; 25 | const SDP_OFFER_VIEWER_MESSAGE = 26 | '{"messageType":"SDP_OFFER","messagePayload":"eyJzZHAiOiJvZmZlcj0gdHJ1ZVxudmlkZW89IHRydWUiLCJ0eXBlIjoib2ZmZXIifQ==","senderClientId":"TestClientId"}'; 27 | const SDP_OFFER_MASTER_MESSAGE = '{"messageType":"SDP_OFFER","messagePayload":"eyJzZHAiOiJvZmZlcj0gdHJ1ZVxudmlkZW89IHRydWUiLCJ0eXBlIjoib2ZmZXIifQ=="}'; 28 | 29 | const SDP_ANSWER_OBJECT = { 30 | sdp: 'offer= true\nvideo= true', 31 | type: 'answer', 32 | }; 33 | const SDP_ANSWER: RTCSessionDescription = { 34 | ...SDP_ANSWER_OBJECT, 35 | toJSON: () => SDP_ANSWER_OBJECT, 36 | } as any; 37 | const SDP_ANSWER_VIEWER_STRING = '{"action":"SDP_ANSWER","messagePayload":"eyJzZHAiOiJvZmZlcj0gdHJ1ZVxudmlkZW89IHRydWUiLCJ0eXBlIjoiYW5zd2VyIn0="}'; 38 | const SDP_ANSWER_MASTER_STRING = 39 | '{"action":"SDP_ANSWER","messagePayload":"eyJzZHAiOiJvZmZlcj0gdHJ1ZVxudmlkZW89IHRydWUiLCJ0eXBlIjoiYW5zd2VyIn0=","recipientClientId":"TestClientId"}'; 40 | const SDP_ANSWER_VIEWER_MESSAGE = 41 | '{"messageType":"SDP_ANSWER","messagePayload":"eyJzZHAiOiJvZmZlcj0gdHJ1ZVxudmlkZW89IHRydWUiLCJ0eXBlIjoiYW5zd2VyIn0=","senderClientId":"TestClientId"}'; 42 | const SDP_ANSWER_MASTER_MESSAGE = '{"messageType":"SDP_ANSWER","messagePayload":"eyJzZHAiOiJvZmZlcj0gdHJ1ZVxudmlkZW89IHRydWUiLCJ0eXBlIjoiYW5zd2VyIn0="}'; 43 | 44 | const ICE_CANDIDATE_OBJECT = { 45 | candidate: 'upd 10.111.34.88', 46 | sdpMid: '1', 47 | sdpMLineIndex: 1, 48 | }; 49 | const ICE_CANDIDATE: RTCIceCandidate = { 50 | ...ICE_CANDIDATE_OBJECT, 51 | toJSON: () => ICE_CANDIDATE_OBJECT, 52 | } as any; 53 | const ICE_CANDIDATE_VIEWER_STRING = 54 | '{"action":"ICE_CANDIDATE","messagePayload":"eyJjYW5kaWRhdGUiOiJ1cGQgMTAuMTExLjM0Ljg4Iiwic2RwTWlkIjoiMSIsInNkcE1MaW5lSW5kZXgiOjF9"}'; 55 | const ICE_CANDIDATE_MASTER_STRING = 56 | '{"action":"ICE_CANDIDATE","messagePayload":"eyJjYW5kaWRhdGUiOiJ1cGQgMTAuMTExLjM0Ljg4Iiwic2RwTWlkIjoiMSIsInNkcE1MaW5lSW5kZXgiOjF9","recipientClientId":"TestClientId"}'; 57 | const ICE_CANDIDATE_VIEWER_MESSAGE = 58 | '{"messageType":"ICE_CANDIDATE","messagePayload":"eyJjYW5kaWRhdGUiOiJ1cGQgMTAuMTExLjM0Ljg4Iiwic2RwTWlkIjoiMSIsInNkcE1MaW5lSW5kZXgiOjF9","senderClientId":"TestClientId"}'; 59 | const ICE_CANDIDATE_MASTER_MESSAGE = 60 | '{"messageType":"ICE_CANDIDATE","messagePayload":"eyJjYW5kaWRhdGUiOiJ1cGQgMTAuMTExLjM0Ljg4Iiwic2RwTWlkIjoiMSIsInNkcE1MaW5lSW5kZXgiOjF9"}'; 61 | 62 | class MockWebSocket extends EventEmitter { 63 | static instance: MockWebSocket; 64 | 65 | public readyState: number; 66 | public send = jest.fn(); 67 | public close = jest.fn().mockImplementation(() => { 68 | if (this.readyState === RealWebSocket.CONNECTING || this.readyState === RealWebSocket.OPEN) { 69 | this.readyState = RealWebSocket.CLOSING; 70 | setTimeout(() => { 71 | if (this.readyState === RealWebSocket.CLOSING) { 72 | this.readyState = RealWebSocket.CLOSED; 73 | this.emit('close'); 74 | } 75 | }, 5); 76 | } 77 | }); 78 | 79 | public constructor() { 80 | super(); 81 | this.readyState = RealWebSocket.CONNECTING; 82 | setTimeout(() => { 83 | if (this.readyState === RealWebSocket.CONNECTING) { 84 | this.readyState = RealWebSocket.OPEN; 85 | this.emit('open'); 86 | } 87 | }, 10); 88 | MockWebSocket.instance = this; 89 | } 90 | 91 | public addEventListener(...args: any[]): void { 92 | super.addListener.apply(this, args); 93 | } 94 | 95 | public removeEventListener(...args: any[]): void { 96 | super.removeListener.apply(this, args); 97 | } 98 | } 99 | window.WebSocket = MockWebSocket as any; 100 | 101 | describe('SignalingClient', () => { 102 | let config: Partial; 103 | let signer: jest.Mock; 104 | 105 | // eslint-disable-next-line @typescript-eslint/ban-ts-ignore 106 | // @ts-ignore 107 | global.crypto = crypto; 108 | // eslint-disable-next-line @typescript-eslint/ban-ts-ignore 109 | // @ts-ignore 110 | global.TextEncoder = util.TextEncoder; 111 | 112 | beforeEach(() => { 113 | signer = jest.fn().mockImplementation(endpoint => new Promise(resolve => resolve(endpoint))); 114 | config = { 115 | role: Role.VIEWER, 116 | clientId: CLIENT_ID, 117 | channelARN: CHANNEL_ARN, 118 | region: 'us-west-2', 119 | channelEndpoint: ENDPOINT, 120 | requestSigner: { 121 | getSignedURL: signer, 122 | }, 123 | }; 124 | }); 125 | 126 | describe('constructor', () => { 127 | beforeEach(() => { 128 | delete config.requestSigner; 129 | config.credentials = { 130 | accessKeyId: 'ACCESS_KEY_ID', 131 | secretAccessKey: 'SECRET_ACCESS_KEY', 132 | sessionToken: 'SESSION_TOKEN', 133 | }; 134 | }); 135 | 136 | it('should not throw if valid viewer config provided', () => { 137 | new SignalingClient(config as SignalingClientConfig); 138 | }); 139 | 140 | it('should not throw if valid master config provided', () => { 141 | config.role = Role.MASTER; 142 | delete config.clientId; 143 | new SignalingClient(config as SignalingClientConfig); 144 | }); 145 | 146 | it('should throw if no config provided', () => { 147 | expect(() => new SignalingClient(null)).toThrow('SignalingClientConfig cannot be null'); 148 | }); 149 | 150 | it('should throw if viewer and no client id is provided', () => { 151 | config.clientId = null; 152 | expect(() => new SignalingClient(config as SignalingClientConfig)).toThrow('clientId cannot be null'); 153 | }); 154 | 155 | it('should throw if master and a client id is provided', () => { 156 | config.role = Role.MASTER; 157 | expect(() => new SignalingClient(config as SignalingClientConfig)).toThrow('clientId should be null'); 158 | }); 159 | 160 | it('should throw if ARN is not provided', () => { 161 | config.channelARN = null; 162 | expect(() => new SignalingClient(config as SignalingClientConfig)).toThrow('channelARN cannot be null'); 163 | }); 164 | 165 | it('should throw if region is not provided', () => { 166 | config.region = null; 167 | expect(() => new SignalingClient(config as SignalingClientConfig)).toThrow('region cannot be null'); 168 | }); 169 | 170 | it('should throw if channelEndpoint is not provided', () => { 171 | config.channelEndpoint = null; 172 | expect(() => new SignalingClient(config as SignalingClientConfig)).toThrow('channelEndpoint cannot be null'); 173 | }); 174 | }); 175 | 176 | describe('open', () => { 177 | it('should open a connection to the signaling server as the viewer', done => { 178 | const client = new SignalingClient(config as SignalingClientConfig); 179 | client.on('open', () => { 180 | expect(signer).toBeCalledWith(ENDPOINT, { 181 | 'X-Amz-ChannelARN': CHANNEL_ARN, 182 | 'X-Amz-ClientId': CLIENT_ID, 183 | }); 184 | done(); 185 | }); 186 | client.open(); 187 | }); 188 | 189 | it('should open a connection to the signaling server as the master', done => { 190 | config.role = Role.MASTER; 191 | delete config.clientId; 192 | const client = new SignalingClient(config as SignalingClientConfig); 193 | client.on('open', () => { 194 | expect(signer).toBeCalledWith(ENDPOINT, { 195 | 'X-Amz-ChannelARN': CHANNEL_ARN, 196 | }); 197 | done(); 198 | }); 199 | client.open(); 200 | }); 201 | 202 | it('should not open a connection to the signaling server if it is closed while opening', async () => { 203 | config.requestSigner.getSignedURL = jest.fn().mockImplementation(endpoint => new Promise(resolve => setTimeout(() => resolve(endpoint), 5))); 204 | const client = new SignalingClient(config as SignalingClientConfig); 205 | client.on('open', () => { 206 | expect('Should not have fired an event').toBeFalsy(); 207 | }); 208 | client.open(); 209 | client.close(); 210 | return new Promise(resolve => setTimeout(resolve, 100)); 211 | }); 212 | 213 | it('should throw an error when making multiple open requests', () => { 214 | const client = new SignalingClient(config as SignalingClientConfig); 215 | expect(() => { 216 | client.open(); 217 | client.open(); 218 | }).toThrow('Client is already open, opening, or closing'); 219 | }); 220 | 221 | it('should emit an error event if the connection cannot be started', done => { 222 | signer.mockImplementation(endpoint => new Promise((_, reject) => reject(new Error(endpoint)))); 223 | const client = new SignalingClient(config as SignalingClientConfig); 224 | client.on('error', () => { 225 | done(); 226 | }); 227 | client.open(); 228 | }); 229 | }); 230 | 231 | describe('close', () => { 232 | it('should close an open connection', done => { 233 | const client = new SignalingClient(config as SignalingClientConfig); 234 | 235 | // Open a channel, close it, then wait for the close event. 236 | client.on('open', () => { 237 | client.close(); 238 | }); 239 | client.on('close', () => { 240 | expect(MockWebSocket.instance.close).toHaveBeenCalled(); 241 | done(); 242 | }); 243 | client.open(); 244 | }); 245 | 246 | it('should do nothing if the connection is closing', done => { 247 | const client = new SignalingClient(config as SignalingClientConfig); 248 | 249 | // Open a channel, close it, try to close it again, then wait for the close event. 250 | client.on('open', () => { 251 | client.close(); 252 | expect(() => client.close()).not.toThrow(); 253 | }); 254 | client.on('close', () => { 255 | done(); 256 | }); 257 | client.open(); 258 | }); 259 | 260 | it('should do nothing if the connection is not open', async () => { 261 | const client = new SignalingClient(config as SignalingClientConfig); 262 | 263 | // Close the client and then wait 100ms. If the close event fires, fail. 264 | client.on('close', () => { 265 | expect('Should not have fired an event').toBeFalsy(); 266 | }); 267 | client.close(); 268 | return new Promise(resolve => setTimeout(resolve, 100)); 269 | }); 270 | }); 271 | 272 | describe('sendSdpOffer', () => { 273 | it('should send the message as the viewer', done => { 274 | const client = new SignalingClient(config as SignalingClientConfig); 275 | client.open(); 276 | client.on('open', () => { 277 | client.sendSdpOffer(SDP_OFFER); 278 | expect(MockWebSocket.instance.send).toHaveBeenCalledWith(SDP_OFFER_VIEWER_STRING); 279 | done(); 280 | }); 281 | }); 282 | 283 | it('should send the message as the master', done => { 284 | config.role = Role.MASTER; 285 | delete config.clientId; 286 | const client = new SignalingClient(config as SignalingClientConfig); 287 | client.open(); 288 | client.on('open', () => { 289 | client.sendSdpOffer(SDP_OFFER, CLIENT_ID); 290 | expect(MockWebSocket.instance.send).toHaveBeenCalledWith(SDP_OFFER_MASTER_STRING); 291 | done(); 292 | }); 293 | }); 294 | 295 | it('should throw an error if the connection is not open', () => { 296 | const client = new SignalingClient(config as SignalingClientConfig); 297 | expect(() => client.sendSdpOffer(SDP_OFFER)).toThrow('Could not send message because the connection to the signaling service is not open.'); 298 | }); 299 | 300 | it('should throw an error if there is a recipient id as viewer', done => { 301 | const client = new SignalingClient(config as SignalingClientConfig); 302 | client.open(); 303 | client.on('open', () => { 304 | expect(() => client.sendSdpOffer(SDP_OFFER, CLIENT_ID)).toThrow( 305 | 'Unexpected recipient client id. As the VIEWER, messages must not be sent with a recipient client id.', 306 | ); 307 | done(); 308 | }); 309 | }); 310 | 311 | it('should throw an error if there is no recipient id as master', done => { 312 | config.role = Role.MASTER; 313 | delete config.clientId; 314 | const client = new SignalingClient(config as SignalingClientConfig); 315 | client.open(); 316 | client.on('open', () => { 317 | expect(() => client.sendSdpOffer(SDP_OFFER)).toThrow( 318 | 'Missing recipient client id. As the MASTER, all messages must be sent with a recipient client id.', 319 | ); 320 | done(); 321 | }); 322 | }); 323 | }); 324 | 325 | describe('sendSdpAnswer', () => { 326 | it('should send the message as the viewer', done => { 327 | const client = new SignalingClient(config as SignalingClientConfig); 328 | client.open(); 329 | client.on('open', () => { 330 | client.sendSdpAnswer(SDP_ANSWER); 331 | expect(MockWebSocket.instance.send).toHaveBeenCalledWith(SDP_ANSWER_VIEWER_STRING); 332 | done(); 333 | }); 334 | }); 335 | 336 | it('should send the message as the master', done => { 337 | config.role = Role.MASTER; 338 | delete config.clientId; 339 | const client = new SignalingClient(config as SignalingClientConfig); 340 | client.open(); 341 | client.on('open', () => { 342 | client.sendSdpAnswer(SDP_ANSWER, CLIENT_ID); 343 | expect(MockWebSocket.instance.send).toHaveBeenCalledWith(SDP_ANSWER_MASTER_STRING); 344 | done(); 345 | }); 346 | }); 347 | 348 | it('should throw an error if the connection is not open', () => { 349 | const client = new SignalingClient(config as SignalingClientConfig); 350 | expect(() => client.sendSdpAnswer(SDP_ANSWER)).toThrow('Could not send message because the connection to the signaling service is not open.'); 351 | }); 352 | 353 | it('should throw an error if there is a recipient id as viewer', done => { 354 | const client = new SignalingClient(config as SignalingClientConfig); 355 | client.open(); 356 | client.on('open', () => { 357 | expect(() => client.sendSdpAnswer(SDP_ANSWER, CLIENT_ID)).toThrow( 358 | 'Unexpected recipient client id. As the VIEWER, messages must not be sent with a recipient client id.', 359 | ); 360 | done(); 361 | }); 362 | }); 363 | 364 | it('should throw an error if there is no recipient id as master', done => { 365 | config.role = Role.MASTER; 366 | delete config.clientId; 367 | const client = new SignalingClient(config as SignalingClientConfig); 368 | client.open(); 369 | client.on('open', () => { 370 | expect(() => client.sendSdpAnswer(SDP_ANSWER)).toThrow( 371 | 'Missing recipient client id. As the MASTER, all messages must be sent with a recipient client id.', 372 | ); 373 | done(); 374 | }); 375 | }); 376 | }); 377 | 378 | describe('sendIceCandidate', () => { 379 | it('should send the message as the viewer', done => { 380 | const client = new SignalingClient(config as SignalingClientConfig); 381 | client.open(); 382 | client.on('open', () => { 383 | client.sendIceCandidate(ICE_CANDIDATE); 384 | expect(MockWebSocket.instance.send).toHaveBeenCalledWith(ICE_CANDIDATE_VIEWER_STRING); 385 | done(); 386 | }); 387 | }); 388 | 389 | it('should send the message as the master', done => { 390 | config.role = Role.MASTER; 391 | delete config.clientId; 392 | const client = new SignalingClient(config as SignalingClientConfig); 393 | client.open(); 394 | client.on('open', () => { 395 | client.sendIceCandidate(ICE_CANDIDATE, CLIENT_ID); 396 | expect(MockWebSocket.instance.send).toHaveBeenCalledWith(ICE_CANDIDATE_MASTER_STRING); 397 | done(); 398 | }); 399 | }); 400 | 401 | it('should throw an error if the connection is not open', () => { 402 | const client = new SignalingClient(config as SignalingClientConfig); 403 | expect(() => client.sendIceCandidate(ICE_CANDIDATE)).toThrow('Could not send message because the connection to the signaling service is not open.'); 404 | }); 405 | 406 | it('should throw an error if there is a recipient id as viewer', done => { 407 | const client = new SignalingClient(config as SignalingClientConfig); 408 | client.open(); 409 | client.on('open', () => { 410 | expect(() => client.sendIceCandidate(ICE_CANDIDATE, CLIENT_ID)).toThrow( 411 | 'Unexpected recipient client id. As the VIEWER, messages must not be sent with a recipient client id.', 412 | ); 413 | done(); 414 | }); 415 | }); 416 | 417 | it('should throw an error if there is no recipient id as master', done => { 418 | config.role = Role.MASTER; 419 | delete config.clientId; 420 | const client = new SignalingClient(config as SignalingClientConfig); 421 | client.open(); 422 | client.on('open', () => { 423 | expect(() => client.sendIceCandidate(ICE_CANDIDATE)).toThrow( 424 | 'Missing recipient client id. As the MASTER, all messages must be sent with a recipient client id.', 425 | ); 426 | done(); 427 | }); 428 | }); 429 | }); 430 | 431 | describe('events', () => { 432 | it('should ignore non-parsable messages from the signaling service', done => { 433 | const client = new SignalingClient(config as SignalingClientConfig); 434 | 435 | // Open a connection, receive a faulty message, and then continue to receive and process a non-faulty message. 436 | client.on('sdpOffer', () => { 437 | done(); 438 | }); 439 | client.on('open', () => { 440 | MockWebSocket.instance.emit('message', { data: 'not valid JSON' }); 441 | MockWebSocket.instance.emit('message', { data: SDP_OFFER_MASTER_MESSAGE }); 442 | }); 443 | client.open(); 444 | }); 445 | 446 | describe('sdpOffer', () => { 447 | it('should parse sdpOffer messages from the master', done => { 448 | const client = new SignalingClient(config as SignalingClientConfig); 449 | client.on('sdpOffer', (sdpOffer, senderClientId) => { 450 | expect(sdpOffer).toEqual(SDP_OFFER_OBJECT); 451 | expect(senderClientId).toBeFalsy(); 452 | done(); 453 | }); 454 | client.on('open', () => { 455 | MockWebSocket.instance.emit('message', { data: SDP_OFFER_MASTER_MESSAGE }); 456 | }); 457 | client.open(); 458 | }); 459 | 460 | it('should parse sdpOffer messages from the viewer', done => { 461 | config.role = Role.MASTER; 462 | delete config.clientId; 463 | const client = new SignalingClient(config as SignalingClientConfig); 464 | client.on('sdpOffer', (sdpOffer, senderClientId) => { 465 | expect(sdpOffer).toEqual(SDP_OFFER_OBJECT); 466 | expect(senderClientId).toEqual(CLIENT_ID); 467 | done(); 468 | }); 469 | client.on('open', () => { 470 | MockWebSocket.instance.emit('message', { data: SDP_OFFER_VIEWER_MESSAGE }); 471 | }); 472 | client.open(); 473 | }); 474 | 475 | it('should parse sdpOffer messages from the master and release pending ICE candidates', done => { 476 | const client = new SignalingClient(config as SignalingClientConfig); 477 | client.on('sdpOffer', (sdpOffer, senderClientId) => { 478 | expect(sdpOffer).toEqual(SDP_OFFER_OBJECT); 479 | expect(senderClientId).toBeFalsy(); 480 | client.on('iceCandidate', (iceCandidate, senderClientId) => { 481 | expect(iceCandidate).toEqual(ICE_CANDIDATE_OBJECT); 482 | expect(senderClientId).toBeFalsy(); 483 | done(); 484 | }); 485 | }); 486 | client.on('open', () => { 487 | MockWebSocket.instance.emit('message', { data: ICE_CANDIDATE_MASTER_MESSAGE }); 488 | MockWebSocket.instance.emit('message', { data: ICE_CANDIDATE_MASTER_MESSAGE }); 489 | MockWebSocket.instance.emit('message', { data: SDP_OFFER_MASTER_MESSAGE }); 490 | }); 491 | client.open(); 492 | }); 493 | }); 494 | 495 | describe('sdpAnswer', () => { 496 | it('should parse sdpAnswer messages from the master', done => { 497 | const client = new SignalingClient(config as SignalingClientConfig); 498 | client.on('sdpAnswer', (sdpAnswer, senderClientId) => { 499 | expect(sdpAnswer).toEqual(SDP_ANSWER_OBJECT); 500 | expect(senderClientId).toBeFalsy(); 501 | done(); 502 | }); 503 | client.on('open', () => { 504 | MockWebSocket.instance.emit('message', { data: SDP_ANSWER_MASTER_MESSAGE }); 505 | }); 506 | client.open(); 507 | }); 508 | 509 | it('should parse sdpAnswer messages from the viewer', done => { 510 | config.role = Role.MASTER; 511 | delete config.clientId; 512 | const client = new SignalingClient(config as SignalingClientConfig); 513 | client.on('sdpAnswer', (sdpAnswer, senderClientId) => { 514 | expect(sdpAnswer).toEqual(SDP_ANSWER_OBJECT); 515 | expect(senderClientId).toEqual(CLIENT_ID); 516 | done(); 517 | }); 518 | client.on('open', () => { 519 | MockWebSocket.instance.emit('message', { data: SDP_ANSWER_VIEWER_MESSAGE }); 520 | }); 521 | client.open(); 522 | }); 523 | 524 | it('should parse sdpAnswer messages from the master and release pending ICE candidates', done => { 525 | const client = new SignalingClient(config as SignalingClientConfig); 526 | client.on('sdpAnswer', (sdpAnswer, senderClientId) => { 527 | expect(sdpAnswer).toEqual(SDP_ANSWER_OBJECT); 528 | expect(senderClientId).toBeFalsy(); 529 | client.on('iceCandidate', (iceCandidate, senderClientId) => { 530 | expect(iceCandidate).toEqual(ICE_CANDIDATE_OBJECT); 531 | expect(senderClientId).toBeFalsy(); 532 | done(); 533 | }); 534 | }); 535 | client.on('open', () => { 536 | MockWebSocket.instance.emit('message', { data: ICE_CANDIDATE_MASTER_MESSAGE }); 537 | MockWebSocket.instance.emit('message', { data: ICE_CANDIDATE_MASTER_MESSAGE }); 538 | MockWebSocket.instance.emit('message', { data: SDP_ANSWER_MASTER_MESSAGE }); 539 | }); 540 | client.open(); 541 | }); 542 | }); 543 | 544 | describe('iceCandidate', () => { 545 | it('should parse iceCandidate messages from the master', done => { 546 | const client = new SignalingClient(config as SignalingClientConfig); 547 | client.on('iceCandidate', (iceCandidate, senderClientId) => { 548 | expect(iceCandidate).toEqual(ICE_CANDIDATE_OBJECT); 549 | expect(senderClientId).toBeFalsy(); 550 | done(); 551 | }); 552 | client.on('open', () => { 553 | MockWebSocket.instance.emit('message', { data: SDP_ANSWER_MASTER_MESSAGE }); 554 | MockWebSocket.instance.emit('message', { data: ICE_CANDIDATE_MASTER_MESSAGE }); 555 | }); 556 | client.open(); 557 | }); 558 | 559 | it('should parse iceCandidate messages from the viewer', done => { 560 | config.role = Role.MASTER; 561 | delete config.clientId; 562 | const client = new SignalingClient(config as SignalingClientConfig); 563 | client.on('iceCandidate', (iceCandidate, senderClientId) => { 564 | expect(iceCandidate).toEqual(ICE_CANDIDATE_OBJECT); 565 | expect(senderClientId).toEqual(CLIENT_ID); 566 | done(); 567 | }); 568 | client.on('open', () => { 569 | MockWebSocket.instance.emit('message', { data: SDP_ANSWER_VIEWER_MESSAGE }); 570 | MockWebSocket.instance.emit('message', { data: ICE_CANDIDATE_VIEWER_MESSAGE }); 571 | }); 572 | client.open(); 573 | }); 574 | }); 575 | }); 576 | }); 577 | -------------------------------------------------------------------------------- /src/SignalingClient.ts: -------------------------------------------------------------------------------- 1 | import { EventEmitter } from 'events'; 2 | 3 | import { Role } from './Role'; 4 | import { SigV4RequestSigner } from './SigV4RequestSigner'; 5 | import { validateValueNil, validateValueNonNil } from './internal/utils'; 6 | 7 | export type QueryParams = { [queryParam: string]: string }; 8 | 9 | export interface RequestSigner { 10 | getSignedURL: (signalingEndpoint: string, queryParams: QueryParams) => Promise; 11 | } 12 | 13 | /** 14 | * A partial copy of the credentials from the AWS SDK for JS: https://github.com/aws/aws-sdk-js/blob/master/lib/credentials.d.ts 15 | * The interface is copied here so that a dependency on the AWS SDK for JS is not needed. 16 | */ 17 | export interface Credentials { 18 | accessKeyId: string; 19 | secretAccessKey: string; 20 | sessionToken?: string; 21 | getPromise?(): Promise; 22 | } 23 | 24 | export interface SignalingClientConfig { 25 | channelARN: string; 26 | channelEndpoint: string; 27 | credentials?: Credentials; 28 | region: string; 29 | requestSigner?: RequestSigner; 30 | role: Role; 31 | clientId?: string; 32 | } 33 | 34 | enum MessageType { 35 | SDP_ANSWER = 'SDP_ANSWER', 36 | SDP_OFFER = 'SDP_OFFER', 37 | ICE_CANDIDATE = 'ICE_CANDIDATE', 38 | } 39 | 40 | enum ReadyState { 41 | CONNECTING, 42 | OPEN, 43 | CLOSING, 44 | CLOSED, 45 | } 46 | 47 | interface WebSocketMessage { 48 | messageType: MessageType; 49 | messagePayload: string; 50 | senderClientId?: string; 51 | } 52 | 53 | /** 54 | * Client for sending and receiving messages from a KVS Signaling Channel. The client can operate as either the 'MASTER' or a 'VIEWER'. 55 | * 56 | * Typically, the 'MASTER' listens for ICE candidates and SDP offers and responds with and SDP answer and its own ICE candidates. 57 | * 58 | * Typically, the 'VIEWER' sends an SDP offer and its ICE candidates and then listens for ICE candidates and SDP answers from the 'MASTER'. 59 | */ 60 | export class SignalingClient extends EventEmitter { 61 | private static DEFAULT_CLIENT_ID = 'MASTER'; 62 | 63 | private websocket: WebSocket = null; 64 | private readyState = ReadyState.CLOSED; 65 | private readonly requestSigner: RequestSigner; 66 | private readonly config: SignalingClientConfig; 67 | private readonly pendingIceCandidatesByClientId: { [clientId: string]: object[] } = {}; 68 | private readonly hasReceivedRemoteSDPByClientId: { [clientId: string]: boolean } = {}; 69 | 70 | /** 71 | * Creates a new SignalingClient. The connection with the signaling service must be opened with the 'open' method. 72 | * @param {SignalingClientConfig} config - Configuration options and parameters. 73 | * is not provided, it will be loaded from the global scope. 74 | */ 75 | public constructor(config: SignalingClientConfig) { 76 | super(); 77 | 78 | // Validate config 79 | validateValueNonNil(config, 'SignalingClientConfig'); 80 | validateValueNonNil(config.role, 'role'); 81 | if (config.role === Role.VIEWER) { 82 | validateValueNonNil(config.clientId, 'clientId'); 83 | } else { 84 | validateValueNil(config.clientId, 'clientId'); 85 | } 86 | validateValueNonNil(config.channelARN, 'channelARN'); 87 | validateValueNonNil(config.region, 'region'); 88 | validateValueNonNil(config.channelEndpoint, 'channelEndpoint'); 89 | 90 | this.config = { ...config }; // Copy config to new object for immutability. 91 | 92 | if (config.requestSigner) { 93 | this.requestSigner = config.requestSigner; 94 | } else { 95 | validateValueNonNil(config.credentials, 'credentials'); 96 | this.requestSigner = new SigV4RequestSigner(config.region, config.credentials); 97 | } 98 | 99 | // Bind event handlers 100 | this.onOpen = this.onOpen.bind(this); 101 | this.onMessage = this.onMessage.bind(this); 102 | this.onError = this.onError.bind(this); 103 | this.onClose = this.onClose.bind(this); 104 | } 105 | 106 | /** 107 | * Opens the connection with the signaling service. Listen to the 'open' event to be notified when the connection has been opened. 108 | */ 109 | public open(): void { 110 | if (this.readyState !== ReadyState.CLOSED) { 111 | throw new Error('Client is already open, opening, or closing'); 112 | } 113 | this.readyState = ReadyState.CONNECTING; 114 | 115 | // The process of opening the connection is asynchronous via promises, but the interaction model is to handle asynchronous actions via events. 116 | // Therefore, we just kick off the asynchronous process and then return and let it fire events. 117 | this.asyncOpen() 118 | .then() 119 | .catch(err => this.onError(err)); 120 | } 121 | 122 | /** 123 | * Asynchronous implementation of `open`. 124 | */ 125 | private async asyncOpen(): Promise { 126 | const queryParams: QueryParams = { 127 | 'X-Amz-ChannelARN': this.config.channelARN, 128 | }; 129 | if (this.config.role === Role.VIEWER) { 130 | queryParams['X-Amz-ClientId'] = this.config.clientId; 131 | } 132 | const signedURL = await this.requestSigner.getSignedURL(this.config.channelEndpoint, queryParams); 133 | 134 | // If something caused the state to change from CONNECTING, then don't create the WebSocket instance. 135 | if (this.readyState !== ReadyState.CONNECTING) { 136 | return; 137 | } 138 | 139 | this.websocket = new WebSocket(signedURL); 140 | 141 | this.websocket.addEventListener('open', this.onOpen); 142 | this.websocket.addEventListener('message', this.onMessage); 143 | this.websocket.addEventListener('error', this.onError); 144 | this.websocket.addEventListener('close', this.onClose); 145 | } 146 | 147 | /** 148 | * Closes the connection to the KVS Signaling Service. If already closed or closing, no action is taken. Listen to the 'close' event to be notified when the 149 | * connection has been closed. 150 | */ 151 | public close(): void { 152 | if (this.websocket !== null) { 153 | this.readyState = ReadyState.CLOSING; 154 | this.websocket.close(); 155 | } else if (this.readyState !== ReadyState.CLOSED) { 156 | this.onClose(); 157 | } 158 | } 159 | 160 | /** 161 | * Sends the given SDP offer to the signaling service. 162 | * 163 | * Typically, only the 'VIEWER' role should send an SDP offer. 164 | * @param {RTCSessionDescription} sdpOffer - SDP offer to send. 165 | * @param {string} [recipientClientId] - ID of the client to send the message to. Required for 'MASTER' role. Should not be present for 'VIEWER' role. 166 | */ 167 | public sendSdpOffer(sdpOffer: RTCSessionDescription, recipientClientId?: string): void { 168 | this.sendMessage(MessageType.SDP_OFFER, sdpOffer.toJSON(), recipientClientId); 169 | } 170 | 171 | /** 172 | * Sends the given SDP answer to the signaling service. 173 | * 174 | * Typically, only the 'MASTER' role should send an SDP answer. 175 | * @param {RTCSessionDescription} sdpAnswer - SDP answer to send. 176 | * @param {string} [recipientClientId] - ID of the client to send the message to. Required for 'MASTER' role. Should not be present for 'VIEWER' role. 177 | */ 178 | public sendSdpAnswer(sdpAnswer: RTCSessionDescription, recipientClientId?: string): void { 179 | this.sendMessage(MessageType.SDP_ANSWER, sdpAnswer.toJSON(), recipientClientId); 180 | } 181 | 182 | /** 183 | * Sends the given ICE candidate to the signaling service. 184 | * 185 | * Typically, both the 'VIEWER' role and 'MASTER' role should send ICE candidates. 186 | * @param {RTCIceCandidate} iceCandidate - ICE candidate to send. 187 | * @param {string} [recipientClientId] - ID of the client to send the message to. Required for 'MASTER' role. Should not be present for 'VIEWER' role. 188 | */ 189 | public sendIceCandidate(iceCandidate: RTCIceCandidate, recipientClientId?: string): void { 190 | this.sendMessage(MessageType.ICE_CANDIDATE, iceCandidate.toJSON(), recipientClientId); 191 | } 192 | 193 | /** 194 | * Validates the WebSocket connection is open and that the recipient client id is present if sending as the 'MASTER'. Encodes the given message payload 195 | * and sends the message to the signaling service. 196 | */ 197 | private sendMessage(action: MessageType, messagePayload: object, recipientClientId?: string): void { 198 | if (this.readyState !== ReadyState.OPEN) { 199 | throw new Error('Could not send message because the connection to the signaling service is not open.'); 200 | } 201 | this.validateRecipientClientId(recipientClientId); 202 | 203 | this.websocket.send( 204 | JSON.stringify({ 205 | action, 206 | messagePayload: SignalingClient.serializeJSONObjectAsBase64String(messagePayload), 207 | recipientClientId: recipientClientId || undefined, 208 | }), 209 | ); 210 | } 211 | 212 | /** 213 | * Removes all event listeners from the WebSocket and removes the reference to the WebSocket object. 214 | */ 215 | private cleanupWebSocket(): void { 216 | if (this.websocket === null) { 217 | return; 218 | } 219 | this.websocket.removeEventListener('open', this.onOpen); 220 | this.websocket.removeEventListener('message', this.onMessage); 221 | this.websocket.removeEventListener('error', this.onError); 222 | this.websocket.removeEventListener('close', this.onClose); 223 | this.websocket = null; 224 | } 225 | 226 | /** 227 | * WebSocket 'open' event handler. Forwards the event on to listeners. 228 | */ 229 | private onOpen(): void { 230 | this.readyState = ReadyState.OPEN; 231 | this.emit('open'); 232 | } 233 | 234 | /** 235 | * WebSocket 'message' event handler. Attempts to parse the message and handle it according to the message type. 236 | */ 237 | private onMessage(event: MessageEvent): void { 238 | let parsedEventData: WebSocketMessage; 239 | let parsedMessagePayload: object; 240 | try { 241 | parsedEventData = JSON.parse(event.data) as WebSocketMessage; 242 | parsedMessagePayload = SignalingClient.parseJSONObjectFromBase64String(parsedEventData.messagePayload); 243 | } catch (e) { 244 | // For forwards compatibility we ignore messages that are not able to be parsed. 245 | // TODO: Consider how to make it easier for users to be aware of dropped messages. 246 | return; 247 | } 248 | const { messageType, senderClientId } = parsedEventData; 249 | switch (messageType) { 250 | case MessageType.SDP_OFFER: 251 | this.emit('sdpOffer', parsedMessagePayload, senderClientId); 252 | this.emitPendingIceCandidates(senderClientId); 253 | return; 254 | case MessageType.SDP_ANSWER: 255 | this.emit('sdpAnswer', parsedMessagePayload, senderClientId); 256 | this.emitPendingIceCandidates(senderClientId); 257 | return; 258 | case MessageType.ICE_CANDIDATE: 259 | this.emitOrQueueIceCandidate(parsedMessagePayload, senderClientId); 260 | return; 261 | } 262 | } 263 | 264 | /** 265 | * Takes the given base64 encoded string and decodes it into a JSON object. 266 | */ 267 | private static parseJSONObjectFromBase64String(base64EncodedString: string): object { 268 | return JSON.parse(atob(base64EncodedString)); 269 | } 270 | 271 | /** 272 | * Takes the given JSON object and encodes it into a base64 string. 273 | */ 274 | private static serializeJSONObjectAsBase64String(object: object): string { 275 | return btoa(JSON.stringify(object)); 276 | } 277 | 278 | /** 279 | * If an SDP offer or answer has already been received from the given client, then the given ICE candidate is emitted. Otherwise, it is queued up for when 280 | * an SDP offer or answer is received. 281 | */ 282 | private emitOrQueueIceCandidate(iceCandidate: object, clientId?: string): void { 283 | const clientIdKey = clientId || SignalingClient.DEFAULT_CLIENT_ID; 284 | if (this.hasReceivedRemoteSDPByClientId[clientIdKey]) { 285 | this.emit('iceCandidate', iceCandidate, clientId); 286 | } else { 287 | if (!this.pendingIceCandidatesByClientId[clientIdKey]) { 288 | this.pendingIceCandidatesByClientId[clientIdKey] = []; 289 | } 290 | this.pendingIceCandidatesByClientId[clientIdKey].push(iceCandidate); 291 | } 292 | } 293 | 294 | /** 295 | * Emits any pending ICE candidates for the given client and records that an SDP offer or answer has been received from the client. 296 | */ 297 | private emitPendingIceCandidates(clientId?: string): void { 298 | const clientIdKey = clientId || SignalingClient.DEFAULT_CLIENT_ID; 299 | this.hasReceivedRemoteSDPByClientId[clientIdKey] = true; 300 | const pendingIceCandidates = this.pendingIceCandidatesByClientId[clientIdKey]; 301 | if (!pendingIceCandidates) { 302 | return; 303 | } 304 | delete this.pendingIceCandidatesByClientId[clientIdKey]; 305 | pendingIceCandidates.forEach(iceCandidate => { 306 | this.emit('iceCandidate', iceCandidate, clientId); 307 | }); 308 | } 309 | 310 | /** 311 | * Throws an error if the recipient client id is null and the current role is 'MASTER' as all messages sent as 'MASTER' should have a recipient client id. 312 | */ 313 | private validateRecipientClientId(recipientClientId?: string): void { 314 | if (this.config.role === Role.MASTER && !recipientClientId) { 315 | throw new Error('Missing recipient client id. As the MASTER, all messages must be sent with a recipient client id.'); 316 | } else if (this.config.role === Role.VIEWER && recipientClientId) { 317 | throw new Error('Unexpected recipient client id. As the VIEWER, messages must not be sent with a recipient client id.'); 318 | } 319 | } 320 | 321 | /** 322 | * 'error' event handler. Forwards the error onto listeners. 323 | */ 324 | private onError(error: Error | Event): void { 325 | this.emit('error', error); 326 | } 327 | 328 | /** 329 | * 'close' event handler. Forwards the error onto listeners and cleans up the connection. 330 | */ 331 | private onClose(): void { 332 | this.readyState = ReadyState.CLOSED; 333 | this.cleanupWebSocket(); 334 | this.emit('close'); 335 | } 336 | } 337 | -------------------------------------------------------------------------------- /src/index.spec.ts: -------------------------------------------------------------------------------- 1 | import { VERSION } from './index'; 2 | 3 | describe('index', () => { 4 | it('should export the version', () => { 5 | expect(VERSION).not.toBeFalsy(); 6 | }); 7 | }); 8 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | /*! 2 | Amazon Kinesis Video Streams WebRTC SDK for JavaScript 3 | Copyright 2019-2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. 4 | 5 | This product includes software developed at 6 | Amazon Web Services, Inc. (http://aws.amazon.com/). 7 | */ 8 | export { Role } from './Role'; 9 | export { SignalingClient } from './SignalingClient'; 10 | export { SigV4RequestSigner } from './SigV4RequestSigner'; 11 | 12 | export const VERSION = process.env.PACKAGE_VERSION; 13 | -------------------------------------------------------------------------------- /src/internal/utils.spec.ts: -------------------------------------------------------------------------------- 1 | import { validateValueNil, validateValueNonNil } from './utils'; 2 | 3 | describe('utils', () => { 4 | describe('validateValueNil', () => { 5 | it('should not throw for null value', () => { 6 | validateValueNil(null, 'test'); 7 | }); 8 | 9 | it('should not throw for undefined value', () => { 10 | validateValueNil(undefined, 'test'); 11 | }); 12 | 13 | it('should not throw for empty value', () => { 14 | validateValueNil('', 'test'); 15 | }); 16 | 17 | it('should throw for non-nil value', () => { 18 | expect(() => validateValueNil('not null', 'test')).toThrow('test should be null'); 19 | }); 20 | }); 21 | 22 | describe('validateValueNonNil', () => { 23 | it('should throw for null value', () => { 24 | expect(() => validateValueNonNil(null, 'test')).toThrow('test cannot be null'); 25 | }); 26 | 27 | it('should throw for undefined value', () => { 28 | expect(() => validateValueNonNil(undefined, 'test')).toThrow('test cannot be undefined'); 29 | }); 30 | 31 | it('should throw for empty value', () => { 32 | expect(() => validateValueNonNil('', 'test')).toThrow('test cannot be empty'); 33 | }); 34 | 35 | it('should not throw for non-nil value', () => { 36 | validateValueNonNil('not null', 'test'); 37 | }); 38 | }); 39 | }); 40 | -------------------------------------------------------------------------------- /src/internal/utils.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Validates that the given value is not null, undefined, or empty string and throws an error if the condition is not met. 3 | */ 4 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 5 | export function validateValueNonNil(value: any, valueName: string): void { 6 | if (value === null) { 7 | throw new Error(`${valueName} cannot be null`); 8 | } else if (value === undefined) { 9 | throw new Error(`${valueName} cannot be undefined`); 10 | } else if (value === '') { 11 | throw new Error(`${valueName} cannot be empty`); 12 | } 13 | } 14 | 15 | /** 16 | * Validates that the given value is null, undefined, or empty string and throws an error if the condition is not met. 17 | */ 18 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 19 | export function validateValueNil(value: any, valueName: string): void { 20 | if (value !== null && value !== undefined && value !== '') { 21 | throw new Error(`${valueName} should be null`); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "declaration": true, 5 | "sourceMap": true, 6 | "importHelpers": true, 7 | "moduleResolution": "node", 8 | "esModuleInterop": true, 9 | "target": "es5", 10 | "lib": [ 11 | "dom", 12 | "es5", 13 | "es2015" 14 | ], 15 | "outDir": "./lib", 16 | "baseUrl": ".", 17 | "noImplicitAny": true, 18 | "noFallthroughCasesInSwitch": true, 19 | "noImplicitReturns": true, 20 | "noImplicitThis": true, 21 | "noUnusedLocals": true, 22 | "noUnusedParameters": true, 23 | "removeComments": false 24 | }, 25 | "formatCodeOptions": { 26 | "indentSize": 4, 27 | "tabSize": 4 28 | }, 29 | "files": [ 30 | "src/index.ts" 31 | ], 32 | "include": [ 33 | "src/typings/**/*" 34 | ] 35 | } -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const ForkTsCheckerWebpackPlugin = require('fork-ts-checker-webpack-plugin'); 2 | const LicenseWebpackPlugin = require('license-webpack-plugin').LicenseWebpackPlugin; 3 | const path = require('path'); 4 | const webpack = require('webpack'); 5 | const packageJson = require('./package.json'); 6 | const fs = require('fs'); 7 | 8 | const version = packageJson.version; 9 | console.log(`Package version: ${version}`); 10 | 11 | module.exports = { 12 | entry: { 13 | main: path.resolve(__dirname, 'src/index.ts'), 14 | }, 15 | output: { 16 | library: 'KVSWebRTC', 17 | libraryTarget: 'window', 18 | }, 19 | resolve: { 20 | extensions: ['.ts', '.js'], 21 | }, 22 | module: { 23 | rules: [ 24 | { 25 | test: /\.ts$/, 26 | loaders: [ 27 | { 28 | loader: 'ts-loader', 29 | options: { 30 | // disable type checker - we will use it in fork plugin 31 | transpileOnly: true, 32 | }, 33 | }, 34 | ], 35 | }, 36 | ], 37 | }, 38 | plugins: [ 39 | new ForkTsCheckerWebpackPlugin(), 40 | 41 | new webpack.EnvironmentPlugin({ 42 | PACKAGE_VERSION: version, 43 | }), 44 | ], 45 | 46 | // Fail if there are any errors (such as a TypeScript type issue) 47 | bail: true, 48 | }; 49 | -------------------------------------------------------------------------------- /webpack.debug.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const merge = require('webpack-merge'); 3 | 4 | module.exports = merge.smart(require('./webpack.config'), { 5 | mode: 'development', 6 | 7 | output: { 8 | path: path.resolve(__dirname, 'dist'), 9 | filename: 'kvs-webrtc.js', 10 | }, 11 | 12 | // Include sourcemaps 13 | devtool: 'inline-source-map', 14 | }); 15 | -------------------------------------------------------------------------------- /webpack.dev.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const merge = require('webpack-merge'); 3 | 4 | module.exports = merge.smart(require('./webpack.config'), { 5 | mode: 'development', 6 | 7 | output: { 8 | path: path.resolve(__dirname, 'dist'), 9 | filename: 'kvs-webrtc.js', 10 | }, 11 | 12 | devServer: { 13 | contentBase: path.join(__dirname, 'examples'), 14 | publicPath: '/', 15 | host: '0.0.0.0', 16 | disableHostCheck: true, 17 | port: 3001, 18 | }, 19 | 20 | // Include sourcemaps 21 | devtool: 'inline-source-map', 22 | 23 | // Keep running even if there are errors 24 | bail: false, 25 | }); 26 | -------------------------------------------------------------------------------- /webpack.dist.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const TerserPlugin = require('terser-webpack-plugin'); 3 | const merge = require('webpack-merge'); 4 | 5 | // Define maximum asset size before gzipping 6 | const MAX_ASSET_SIZE_KB = 22; 7 | const MAX_ASSET_SIZE_BYTES = MAX_ASSET_SIZE_KB * 1024; 8 | 9 | module.exports = merge.smart(require('./webpack.config'), { 10 | mode: 'production', 11 | 12 | output: { 13 | path: path.resolve(__dirname, 'dist'), 14 | filename: 'kvs-webrtc.min.js', 15 | }, 16 | 17 | // Make sure the asset is not accidentally growing in size 18 | // Make sure that size impact of adding new code is known 19 | performance: { 20 | hints: 'error', 21 | maxAssetSize: MAX_ASSET_SIZE_BYTES, 22 | maxEntrypointSize: MAX_ASSET_SIZE_BYTES, 23 | }, 24 | 25 | optimization: { 26 | minimizer: [ 27 | new TerserPlugin({ 28 | terserOptions: { 29 | output: { 30 | comments: /kvs-webrtc\.LICENSE/i, 31 | }, 32 | }, 33 | extractComments: false, 34 | }), 35 | ], 36 | }, 37 | }); 38 | --------------------------------------------------------------------------------