diff --git a/.env.example b/.env.example index d2dc9b2..b7eb288 100644 --- a/.env.example +++ b/.env.example @@ -1,16 +1,20 @@ TWILIO_ACCOUNT_SID=ACXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX + +# The Twilio number for making outbound and receiving inbound calls +# Purchase a Twilio phone number in the console +# https://www.twilio.com/console/phone-numbers/search TWILIO_CALLER_ID=+1XXXYYYZZZZ # SID of your TwiML Application +# Create a new TwiML app in the console # https://www.twilio.com/console/voice/twiml/apps -# OR +# OR with the Twilio CLI # twilio api:core:applications:create --friendly-name=voice-client-javascript TWILIO_TWIML_APP_SID=APXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX # Your REST API Key information +# Create a new key in the console and copy the SID and secret. # https://www.twilio.com/console/project/api-keys -# OR -# twilio api:core:keys:create --friendly-name=voice-client-javascript -o json -# NOTE: Make sure to copy the secret, it'll will only be displayed once -API_KEY=SKXXXXXXXXXXXX +# NOTE: Make sure to copy the secret, it will only be displayed once +API_KEY=SKXXXXXXXXXXXX # the SID of the key API_SECRET=XXXXXXXXXXXXXX diff --git a/.gitignore b/.gitignore index aacfac8..87109df 100644 --- a/.gitignore +++ b/.gitignore @@ -16,3 +16,4 @@ venv __pycache__ .pytest_cache .tool-versions +node_modules/ diff --git a/Makefile b/Makefile index 8b2dff0..7d27e3b 100644 --- a/Makefile +++ b/Makefile @@ -16,6 +16,13 @@ else . venv/bin/activate; \ pip3 install -r requirements.txt; endif + cd static && npm install serve: +ifeq ($(UNAME), Windows) + venv\Scripts\activate.bat; \ + python3 app.py +else + . venv/bin/activate; \ python3 app.py +endif diff --git a/README.md b/README.md index a5a5776..aecc544 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,9 @@ ## About -This application should give you a ready-made starting point for writing your own voice apps with Twilio Client. This application uses the lightweight [Flask Framework](http://flask.pocoo.org/). +This application should give you a ready-made starting point for writing your own voice apps with Twilio Client. This application uses the lightweight [Flask Framework](http://flask.pocoo.org/). Once you set up the application, you will be able to make and receive calls from your browser. You will also be able to switch between audio input/output devices, and see dynamic volume levels on the call. + +![screenshot of application homepage](./screenshot_on_call.png) Implementations in other languages: @@ -23,6 +25,8 @@ Implementations in other languages: ### Requirements - [Python](https://www.python.org/) **3.6**, **3.7** or **3.8** version. +- [Node](https://nodejs.org/en/), version **14.0** or above. +- [ngrok](https://ngrok.com/download) ### Twilio Account Settings @@ -44,13 +48,13 @@ Before we begin, we need to collect all the config values we need to run the app cd client-quickstart-python ``` -2. Create the virtual environment, load it and install the dependencies. +2. Run `make install`. This command will create a Python virtual environment, load it, and install the Python dependencies. It will also download the `@twilio/voice-sdk` npm package and create a `node_modules` directory inside the `static` directory. ```bash make install ``` -3. Create a configuration file for your application and edit the `.env` file. +3. Create a configuration file for your application by copying the `.env.example` file to a new file called `.env`. Then, edit the `.env` file to include your account and application details. ```bash cp .env.example .env @@ -75,7 +79,7 @@ Before we begin, we need to collect all the config values we need to run the app After re-running the script, the environment variables will be peramently set for your user account. -4. Run the application, will run on port 5000. +4. Run the application. It will run locally on port 5000. ```bash make serve @@ -83,7 +87,7 @@ Before we begin, we need to collect all the config values we need to run the app 5. Navigate to [http://localhost:5000](http://localhost:5000) -6. Expose your application to the wider internet using [ngrok](https://ngrok.com/download). You can click [here](https://www.twilio.com/blog/2015/09/6-awesome-reasons-to-use-ngrok-when-testing-webhooks.html) for more details. This step **is important** because the application won't work as expected if you run it through localhost. +6. Expose your application to the wider internet using [ngrok](https://ngrok.com/download). You can click [here](https://www.twilio.com/blog/2015/09/6-awesome-reasons-to-use-ngrok-when-testing-webhooks.html) for more details. This step **is important** and your application won't work if you only run the server on localhost. ```bash ngrok http 5000 @@ -98,14 +102,23 @@ Voice "REQUEST URL" to be your ngrok URL plus `/voice`. For example: ![screenshot of twiml app](https://s3.amazonaws.com/com.twilio.prod.twilio-docs/images/TwilioClientRequestUrl.original.png) > **Note:** You must set your webhook urls to the `https` ngrok tunnel created. - You should now be ready to rock! Make some phone calls. - Open it on another device and call yourself. Note that Twilio Client requires - WebRTC enabled browsers, so Edge and Internet Explorer will not work for testing. - We'd recommend Google Chrome or Mozilla Firefox instead. - ![screenshot of phone app](https://s3.amazonaws.com/com.twilio.prod.twilio-docs/images/TwilioClientQuickstart.original.png) +You should now be ready to rock! Make some phone calls or receiving incoming calls in the application. +Note that Twilio Client requires WebRTC enabled browsers, so Edge and Internet Explorer will not work for testing. +We'd recommend Google Chrome or Mozilla Firefox instead. + +![screenshot of application homepage](./screenshot_homepage.png) + +When the server starts, you will be assigned a random client name, which will appear in the top left corner of the homepage. This client name is used as the identity field when generating an access token for the client, and is also used to route incoming calls to the correct client device. + +You can make outbound calls by entering a phone number or a client name. If you would like to test browser-to-browser calls, open one browser page to `localhost:5000` and then stop and restart the server, which will generate a new client identity. Open a new browser to `localhost:5000`, and you should see the new client name. You can make calls between these two clients by entering one client's name in the box for making an outbound call. + +![screenshot of application homepage](./screenshot_two_calls.png) + +You can also receiving an incoming call to your browser by calling the Twilio number you specified as your `TWILIO_CALLER_ID` in your `.env` file. -That's it! +If you see "Unknown Audio Output Device 1" in the "Ringtone" or "Speaker" devices lists, click the button below the boxes (Seeing "Unknown" Devices?) to have your browser identify your input and output devices. +![screenshot of unknown devices](./screenshot_unknown_devices.png) ### Docker @@ -121,7 +134,8 @@ If you have [Docker](https://www.docker.com/) already installed on your machine, You can run the tests locally with the following command. Before running, make sure the virtual environment is activated. ```bash -$ python3 -m pytest +source venv/bin/activate +python3 -m pytest ``` ### Cloud deployment diff --git a/app.py b/app.py index d8e2f2b..594e966 100755 --- a/app.py +++ b/app.py @@ -2,37 +2,40 @@ import os import re -from flask import Flask, jsonify, request, Response + +from dotenv import load_dotenv from faker import Faker +from flask import Flask, Response, jsonify, redirect, request from twilio.jwt.access_token import AccessToken from twilio.jwt.access_token.grants import VoiceGrant -from twilio.twiml.voice_response import VoiceResponse, Dial +from twilio.twiml.voice_response import Dial, VoiceResponse -from dotenv import load_dotenv load_dotenv() app = Flask(__name__) fake = Faker() -alphanumeric_only = re.compile('[\W_]+') +alphanumeric_only = re.compile("[\W_]+") phone_pattern = re.compile(r"^[\d\+\-\(\) ]+$") +twilio_number = os.environ["TWILIO_CALLER_ID"] + +# Generate a random user name +identity = alphanumeric_only.sub("", fake.user_name()) + -@app.route('/') +@app.route("/") def index(): - return app.send_static_file('index.html') + return app.send_static_file("index.html") -@app.route('/token', methods=['GET']) +@app.route("/token", methods=["GET"]) def token(): # get credentials for environment variables - account_sid = os.environ['TWILIO_ACCOUNT_SID'] - application_sid = os.environ['TWILIO_TWIML_APP_SID'] - api_key = os.environ['API_KEY'] - api_secret = os.environ['API_SECRET'] - - # Generate a random user name - identity = alphanumeric_only.sub('', fake.user_name()) - + account_sid = os.environ["TWILIO_ACCOUNT_SID"] + application_sid = os.environ["TWILIO_TWIML_APP_SID"] + api_key = os.environ["API_KEY"] + api_secret = os.environ["API_SECRET"] + # Create access token with credentials token = AccessToken(account_sid, api_key, api_secret, identity=identity) @@ -44,30 +47,40 @@ def token(): token.add_grant(voice_grant) # Return token info as JSON - token=token.to_jwt() - + token = token.to_jwt() # Return token info as JSON - return jsonify(identity=identity, token=token.decode('utf-8')) + return jsonify(identity=identity, token=token) -@app.route("/voice", methods=['POST']) +@app.route("/voice", methods=["POST"]) def voice(): resp = VoiceResponse() - if "To" in request.form and request.form["To"] != '': - dial = Dial(caller_id=os.environ['TWILIO_CALLER_ID']) + if request.form.get("To") == twilio_number: + # Receiving an incoming call to our Twilio number + dial = Dial() + dial.client(identity) + resp.append(dial) + elif request.form.get("phone"): + # Placing an outbound call from the Twilio client + dial = Dial(caller_id=twilio_number) # wrap the phone number or client name in the appropriate TwiML verb # by checking if the number given has only digits and format symbols - if phone_pattern.match(request.form["To"]): - dial.number(request.form["To"]) + if phone_pattern.match(request.form["phone"]): + dial.number(request.form["phone"]) else: - dial.client(request.form["To"]) + dial.client(request.form["phone"]) resp.append(dial) else: resp.say("Thanks for calling!") - return Response(str(resp), mimetype='text/xml') + return Response(str(resp), mimetype="text/xml") + + +@app.route("/static/twilio.min.js") +def static_files(): + return redirect("/static/node_modules/@twilio/voice-sdk/dist/twilio.min.js") -if __name__ == '__main__': - app.run(debug=True, host='0.0.0.0') +if __name__ == "__main__": + app.run(debug=True, host="0.0.0.0") diff --git a/screenshot_homepage.png b/screenshot_homepage.png new file mode 100644 index 0000000..57dca04 Binary files /dev/null and b/screenshot_homepage.png differ diff --git a/screenshot_on_call.png b/screenshot_on_call.png new file mode 100644 index 0000000..1d6bc6f Binary files /dev/null and b/screenshot_on_call.png differ diff --git a/screenshot_two_calls.png b/screenshot_two_calls.png new file mode 100644 index 0000000..a3f9382 Binary files /dev/null and b/screenshot_two_calls.png differ diff --git a/screenshot_unknown_devices.png b/screenshot_unknown_devices.png new file mode 100644 index 0000000..3a9fab9 Binary files /dev/null and b/screenshot_unknown_devices.png differ diff --git a/static/index.html b/static/index.html index 91f0cce..e4eb479 100644 --- a/static/index.html +++ b/static/index.html @@ -1,39 +1,64 @@ - - Twilio Client Quickstart - - - -
-
-

Twilio Client

-
-
- - - -
- Seeing unknown devices? -
-
-
-

Make a Call:

- - - -
- -


- -
-
-
-
-
+ + Twilio Voice JavaScript SDK Quickstart + + + +
+

Twilio Voice JavaScript SDK Quickstart

+ +
+
+
+

Your Device Info

+
+
+ + + +
+ +
+
+
+

Make a Call

+
+
+ + + +
+ +
+

Incoming Call Controls

+

+ Incoming Call from +

+ + + +
+
+ +
+

+ +
+
+
+
+
+

Event Log

+
+
+
- - - - + + + + diff --git a/static/package-lock.json b/static/package-lock.json new file mode 100644 index 0000000..2b2c9b1 --- /dev/null +++ b/static/package-lock.json @@ -0,0 +1,1458 @@ +{ + "name": "twilio-voice-sdk-flask", + "version": "1.0.0", + "lockfileVersion": 2, + "requires": true, + "packages": { + "": { + "name": "twilio-voice-sdk-flask", + "version": "1.0.0", + "license": "ISC", + "dependencies": { + "@twilio/voice-sdk": "git+https://github.com/twilio/twilio-voice.js.git#2.0.0" + } + }, + "node_modules/@twilio/audioplayer": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/@twilio/audioplayer/-/audioplayer-1.0.6.tgz", + "integrity": "sha512-c9cjX/ifICgXqShtyAQdVMqfe7odnxougiuRMXBJtn3dZ320mFdt7kmuKedpNnc3ZJ6irOZ9M9MZi9/vuEqHiw==", + "dependencies": { + "babel-runtime": "^6.26.0" + } + }, + "node_modules/@twilio/voice-errors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@twilio/voice-errors/-/voice-errors-1.1.1.tgz", + "integrity": "sha512-3IJzRhgAqsS3uW2PO7crUXEFxuFhggHeLvt/Q4hz7lrTLFChl37hWiImCMIaM5VHiybQi6ECVQsId2X8UdTr2A==", + "dependencies": { + "npm-run-all": "^4.1.5" + } + }, + "node_modules/@twilio/voice-sdk": { + "version": "2.0.0", + "resolved": "git+ssh://git@github.com/twilio/twilio-voice.js.git#4e7135bb029ac00b90b9fd136f3c0914159402eb", + "license": "Apache-2.0", + "dependencies": { + "@twilio/audioplayer": "1.0.6", + "@twilio/voice-errors": "1.1.1", + "backoff": "2.5.0", + "loglevel": "1.6.7", + "rtcpeerconnection-shim": "1.2.8", + "ws": "6.1.3", + "xmlhttprequest": "1.8.0" + } + }, + "node_modules/ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dependencies": { + "color-convert": "^1.9.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/async-limiter": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/async-limiter/-/async-limiter-1.0.1.tgz", + "integrity": "sha512-csOlWGAcRFJaI6m+F2WKdnMKr4HhdhFVBk0H/QbJFMCr+uO2kwohwXQPxw/9OCxp05r5ghVBFSyioixx3gfkNQ==" + }, + "node_modules/babel-runtime": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/babel-runtime/-/babel-runtime-6.26.0.tgz", + "integrity": "sha1-llxwWGaOgrVde/4E/yM3vItWR/4=", + "dependencies": { + "core-js": "^2.4.0", + "regenerator-runtime": "^0.11.0" + } + }, + "node_modules/backoff": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/backoff/-/backoff-2.5.0.tgz", + "integrity": "sha1-9hbtqdPktmuMp/ynn2lXIsX44m8=", + "dependencies": { + "precond": "0.2" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" + }, + "node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/call-bind": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz", + "integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==", + "dependencies": { + "function-bind": "^1.1.1", + "get-intrinsic": "^1.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dependencies": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dependencies": { + "color-name": "1.1.3" + } + }, + "node_modules/color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=" + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=" + }, + "node_modules/core-js": { + "version": "2.6.12", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-2.6.12.tgz", + "integrity": "sha512-Kb2wC0fvsWfQrgk8HU5lW6U/Lcs8+9aaYcy4ZFc6DDlo4nZ7n70dEgE5rtR0oG6ufKDUnrwfWL1mXR5ljDatrQ==", + "deprecated": "core-js@<3.3 is no longer maintained and not recommended for usage due to the number of issues. Because of the V8 engine whims, feature detection in old core-js versions could cause a slowdown up to 100x even if nothing is polyfilled. Please, upgrade your dependencies to the actual version of core-js.", + "hasInstallScript": true + }, + "node_modules/cross-spawn": { + "version": "6.0.5", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz", + "integrity": "sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==", + "dependencies": { + "nice-try": "^1.0.4", + "path-key": "^2.0.1", + "semver": "^5.5.0", + "shebang-command": "^1.2.0", + "which": "^1.2.9" + }, + "engines": { + "node": ">=4.8" + } + }, + "node_modules/define-properties": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.3.tgz", + "integrity": "sha512-3MqfYKj2lLzdMSf8ZIZE/V+Zuy+BgD6f164e8K2w7dgnpKArBDerGYpM46IYYcjnkdPNMjPk9A6VFB8+3SKlXQ==", + "dependencies": { + "object-keys": "^1.0.12" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/error-ex": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", + "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", + "dependencies": { + "is-arrayish": "^0.2.1" + } + }, + "node_modules/es-abstract": { + "version": "1.18.3", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.18.3.tgz", + "integrity": "sha512-nQIr12dxV7SSxE6r6f1l3DtAeEYdsGpps13dR0TwJg1S8gyp4ZPgy3FZcHBgbiQqnoqSTb+oC+kO4UQ0C/J8vw==", + "dependencies": { + "call-bind": "^1.0.2", + "es-to-primitive": "^1.2.1", + "function-bind": "^1.1.1", + "get-intrinsic": "^1.1.1", + "has": "^1.0.3", + "has-symbols": "^1.0.2", + "is-callable": "^1.2.3", + "is-negative-zero": "^2.0.1", + "is-regex": "^1.1.3", + "is-string": "^1.0.6", + "object-inspect": "^1.10.3", + "object-keys": "^1.1.1", + "object.assign": "^4.1.2", + "string.prototype.trimend": "^1.0.4", + "string.prototype.trimstart": "^1.0.4", + "unbox-primitive": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/es-to-primitive": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.1.tgz", + "integrity": "sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==", + "dependencies": { + "is-callable": "^1.1.4", + "is-date-object": "^1.0.1", + "is-symbol": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=", + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", + "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==" + }, + "node_modules/get-intrinsic": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.1.1.tgz", + "integrity": "sha512-kWZrnVM42QCiEA2Ig1bG8zjoIMOgxWwYCEeNdwY6Tv/cOSeGpcoX4pXHfKUxNKVoArnrEr2e9srnAxxGIraS9Q==", + "dependencies": { + "function-bind": "^1.1.1", + "has": "^1.0.3", + "has-symbols": "^1.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.6", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.6.tgz", + "integrity": "sha512-nTnJ528pbqxYanhpDYsi4Rd8MAeaBA67+RZ10CM1m3bTAVFEDcd5AuA4a6W5YkGZ1iNXHzZz8T6TBKLeBuNriQ==" + }, + "node_modules/has": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", + "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", + "dependencies": { + "function-bind": "^1.1.1" + }, + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/has-bigints": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.0.1.tgz", + "integrity": "sha512-LSBS2LjbNBTf6287JEbEzvJgftkF5qFkmCo9hDRpAzKhUOlJ+hx8dd4USs00SgsUNwc4617J9ki5YtEClM2ffA==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", + "engines": { + "node": ">=4" + } + }, + "node_modules/has-symbols": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.2.tgz", + "integrity": "sha512-chXa79rL/UC2KlX17jo3vRGz0azaWEx5tGqZg5pO3NUyEJVB17dMruQlzCCOfUvElghKcm5194+BCRvi2Rv/Gw==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hosted-git-info": { + "version": "2.8.9", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.9.tgz", + "integrity": "sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==" + }, + "node_modules/is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0=" + }, + "node_modules/is-bigint": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.0.2.tgz", + "integrity": "sha512-0JV5+SOCQkIdzjBK9buARcV804Ddu7A0Qet6sHi3FimE9ne6m4BGQZfRn+NZiXbBk4F4XmHfDZIipLj9pX8dSA==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-boolean-object": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.1.1.tgz", + "integrity": "sha512-bXdQWkECBUIAcCkeH1unwJLIpZYaa5VvuygSyS/c2lf719mTKZDU5UdDRlpd01UjADgmW8RfqaP+mRaVPdr/Ng==", + "dependencies": { + "call-bind": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-callable": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.3.tgz", + "integrity": "sha512-J1DcMe8UYTBSrKezuIUTUwjXsho29693unXM2YhJUTR2txK/eG47bvNa/wipPFmZFgr/N6f1GA66dv0mEyTIyQ==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-core-module": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.4.0.tgz", + "integrity": "sha512-6A2fkfq1rfeQZjxrZJGerpLCTHRNEBiSgnu0+obeJpEPZRUooHgsizvzv0ZjJwOz3iWIHdJtVWJ/tmPr3D21/A==", + "dependencies": { + "has": "^1.0.3" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-date-object": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.4.tgz", + "integrity": "sha512-/b4ZVsG7Z5XVtIxs/h9W8nvfLgSAyKYdtGWQLbqy6jA1icmgjf8WCoTKgeS4wy5tYaPePouzFMANbnj94c2Z+A==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-negative-zero": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.1.tgz", + "integrity": "sha512-2z6JzQvZRa9A2Y7xC6dQQm4FSTSTNWjKIYYTt4246eMTJmIo0Q+ZyOsU66X8lxK1AbB92dFeglPLrhwpeRKO6w==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-number-object": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.0.5.tgz", + "integrity": "sha512-RU0lI/n95pMoUKu9v1BZP5MBcZuNSVJkMkAG2dJqC4z2GlkGUNeH68SuHuBKBD/XFe+LHZ+f9BKkLET60Niedw==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-regex": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.3.tgz", + "integrity": "sha512-qSVXFz28HM7y+IWX6vLCsexdlvzT1PJNFSBuaQLQ5o0IEw8UDYW6/2+eCMVyIsbM8CNLX2a/QWmSpyxYEHY7CQ==", + "dependencies": { + "call-bind": "^1.0.2", + "has-symbols": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-string": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.0.6.tgz", + "integrity": "sha512-2gdzbKUuqtQ3lYNrUTQYoClPhm7oQu4UdpSZMp1/DGgkHBT8E2Z1l0yMdb6D4zNAxwDiMv8MdulKROJGNl0Q0w==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-symbol": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.4.tgz", + "integrity": "sha512-C/CPBqKWnvdcxqIARxyOh4v1UUEOCHpgDa0WYgpKDFMszcrPcffg5uhwSgPCLD2WWxmq6isisz87tzT01tuGhg==", + "dependencies": { + "has-symbols": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=" + }, + "node_modules/json-parse-better-errors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz", + "integrity": "sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw==" + }, + "node_modules/load-json-file": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-4.0.0.tgz", + "integrity": "sha1-L19Fq5HjMhYjT9U62rZo607AmTs=", + "dependencies": { + "graceful-fs": "^4.1.2", + "parse-json": "^4.0.0", + "pify": "^3.0.0", + "strip-bom": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/loglevel": { + "version": "1.6.7", + "resolved": "https://registry.npmjs.org/loglevel/-/loglevel-1.6.7.tgz", + "integrity": "sha512-cY2eLFrQSAfVPhCgH1s7JI73tMbg9YC3v3+ZHVW67sBS7UxWzNEk/ZBbSfLykBWHp33dqqtOv82gjhKEi81T/A==", + "engines": { + "node": ">= 0.6.0" + }, + "funding": { + "type": "tidelift", + "url": "https://tidelift.com/subscription/pkg/npm-loglevel?utm_medium=referral&utm_source=npm_fund" + } + }, + "node_modules/memorystream": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/memorystream/-/memorystream-0.3.1.tgz", + "integrity": "sha1-htcJCzDORV1j+64S3aUaR93K+bI=", + "engines": { + "node": ">= 0.10.0" + } + }, + "node_modules/minimatch": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", + "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/nice-try": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz", + "integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==" + }, + "node_modules/normalize-package-data": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz", + "integrity": "sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==", + "dependencies": { + "hosted-git-info": "^2.1.4", + "resolve": "^1.10.0", + "semver": "2 || 3 || 4 || 5", + "validate-npm-package-license": "^3.0.1" + } + }, + "node_modules/npm-run-all": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/npm-run-all/-/npm-run-all-4.1.5.tgz", + "integrity": "sha512-Oo82gJDAVcaMdi3nuoKFavkIHBRVqQ1qvMb+9LHk/cF4P6B2m8aP04hGf7oL6wZ9BuGwX1onlLhpuoofSyoQDQ==", + "dependencies": { + "ansi-styles": "^3.2.1", + "chalk": "^2.4.1", + "cross-spawn": "^6.0.5", + "memorystream": "^0.3.1", + "minimatch": "^3.0.4", + "pidtree": "^0.3.0", + "read-pkg": "^3.0.0", + "shell-quote": "^1.6.1", + "string.prototype.padend": "^3.0.0" + }, + "bin": { + "npm-run-all": "bin/npm-run-all/index.js", + "run-p": "bin/run-p/index.js", + "run-s": "bin/run-s/index.js" + }, + "engines": { + "node": ">= 4" + } + }, + "node_modules/object-inspect": { + "version": "1.10.3", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.10.3.tgz", + "integrity": "sha512-e5mCJlSH7poANfC8z8S9s9S2IN5/4Zb3aZ33f5s8YqoazCFzNLloLU8r5VCG+G7WoqLvAAZoVMcy3tp/3X0Plw==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object.assign": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.2.tgz", + "integrity": "sha512-ixT2L5THXsApyiUPYKmW+2EHpXXe5Ii3M+f4e+aJFAHao5amFRW6J0OO6c/LU8Be47utCx2GL89hxGB6XSmKuQ==", + "dependencies": { + "call-bind": "^1.0.0", + "define-properties": "^1.1.3", + "has-symbols": "^1.0.1", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/parse-json": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-4.0.0.tgz", + "integrity": "sha1-vjX1Qlvh9/bHRxhPmKeIy5lHfuA=", + "dependencies": { + "error-ex": "^1.3.1", + "json-parse-better-errors": "^1.0.1" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/path-key": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz", + "integrity": "sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A=", + "engines": { + "node": ">=4" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==" + }, + "node_modules/path-type": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-3.0.0.tgz", + "integrity": "sha512-T2ZUsdZFHgA3u4e5PfPbjd7HDDpxPnQb5jN0SrDsjNSuVXHJqtwTnWqG0B1jZrgmJ/7lj1EmVIByWt1gxGkWvg==", + "dependencies": { + "pify": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/pidtree": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/pidtree/-/pidtree-0.3.1.tgz", + "integrity": "sha512-qQbW94hLHEqCg7nhby4yRC7G2+jYHY4Rguc2bjw7Uug4GIJuu1tvf2uHaZv5Q8zdt+WKJ6qK1FOI6amaWUo5FA==", + "bin": { + "pidtree": "bin/pidtree.js" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/pify": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", + "integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=", + "engines": { + "node": ">=4" + } + }, + "node_modules/precond": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/precond/-/precond-0.2.3.tgz", + "integrity": "sha1-qpWRvKokkj8eD0hJ0kD0fvwQdaw=", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/read-pkg": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-3.0.0.tgz", + "integrity": "sha1-nLxoaXj+5l0WwA4rGcI3/Pbjg4k=", + "dependencies": { + "load-json-file": "^4.0.0", + "normalize-package-data": "^2.3.2", + "path-type": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/regenerator-runtime": { + "version": "0.11.1", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.11.1.tgz", + "integrity": "sha512-MguG95oij0fC3QV3URf4V2SDYGJhJnJGqvIIgdECeODCT98wSWDAJ94SSuVpYQUoTcGUIL6L4yNB7j1DFFHSBg==" + }, + "node_modules/resolve": { + "version": "1.20.0", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.20.0.tgz", + "integrity": "sha512-wENBPt4ySzg4ybFQW2TT1zMQucPK95HSh/nq2CFTZVOGut2+pQvSsgtda4d26YrYcr067wjbmzOG8byDPBX63A==", + "dependencies": { + "is-core-module": "^2.2.0", + "path-parse": "^1.0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/rtcpeerconnection-shim": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/rtcpeerconnection-shim/-/rtcpeerconnection-shim-1.2.8.tgz", + "integrity": "sha512-5Sx90FGru1sQw9aGOM+kHU4i6mbP8eJPgxliu2X3Syhg8qgDybx8dpDTxUwfJvPnubXFnZeRNl59DWr4AttJKQ==", + "dependencies": { + "sdp": "^2.6.0" + }, + "engines": { + "node": ">=6.0.0", + "npm": ">=3.10.0" + } + }, + "node_modules/sdp": { + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/sdp/-/sdp-2.12.0.tgz", + "integrity": "sha512-jhXqQAQVM+8Xj5EjJGVweuEzgtGWb3tmEEpl3CLP3cStInSbVHSg0QWOGQzNq8pSID4JkpeV2mPqlMDLrm0/Vw==" + }, + "node_modules/semver": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "bin": { + "semver": "bin/semver" + } + }, + "node_modules/shebang-command": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz", + "integrity": "sha1-RKrGW2lbAzmJaMOfNj/uXer98eo=", + "dependencies": { + "shebang-regex": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/shebang-regex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz", + "integrity": "sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM=", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/shell-quote": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.7.2.tgz", + "integrity": "sha512-mRz/m/JVscCrkMyPqHc/bczi3OQHkLTqXHEFu0zDhK/qfv3UcOA4SVmRCLmos4bhjr9ekVQubj/R7waKapmiQg==" + }, + "node_modules/spdx-correct": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.1.1.tgz", + "integrity": "sha512-cOYcUWwhCuHCXi49RhFRCyJEK3iPj1Ziz9DpViV3tbZOwXD49QzIN3MpOLJNxh2qwq2lJJZaKMVw9qNi4jTC0w==", + "dependencies": { + "spdx-expression-parse": "^3.0.0", + "spdx-license-ids": "^3.0.0" + } + }, + "node_modules/spdx-exceptions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.3.0.tgz", + "integrity": "sha512-/tTrYOC7PPI1nUAgx34hUpqXuyJG+DTHJTnIULG4rDygi4xu/tfgmq1e1cIRwRzwZgo4NLySi+ricLkZkw4i5A==" + }, + "node_modules/spdx-expression-parse": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz", + "integrity": "sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==", + "dependencies": { + "spdx-exceptions": "^2.1.0", + "spdx-license-ids": "^3.0.0" + } + }, + "node_modules/spdx-license-ids": { + "version": "3.0.9", + "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.9.tgz", + "integrity": "sha512-Ki212dKK4ogX+xDo4CtOZBVIwhsKBEfsEEcwmJfLQzirgc2jIWdzg40Unxz/HzEUqM1WFzVlQSMF9kZZ2HboLQ==" + }, + "node_modules/string.prototype.padend": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/string.prototype.padend/-/string.prototype.padend-3.1.2.tgz", + "integrity": "sha512-/AQFLdYvePENU3W5rgurfWSMU6n+Ww8n/3cUt7E+vPBB/D7YDG8x+qjoFs4M/alR2bW7Qg6xMjVwWUOvuQ0XpQ==", + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.3", + "es-abstract": "^1.18.0-next.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.trimend": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.4.tgz", + "integrity": "sha512-y9xCjw1P23Awk8EvTpcyL2NIr1j7wJ39f+k6lvRnSMz+mz9CGz9NYPelDk42kOz6+ql8xjfK8oYzy3jAP5QU5A==", + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.3" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.trimstart": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.4.tgz", + "integrity": "sha512-jh6e984OBfvxS50tdY2nRZnoC5/mLFKOREQfw8t5yytkoUsJRNxvI/E39qu1sD0OtWI3OC0XgKSmcWwziwYuZw==", + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.3" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/strip-bom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", + "integrity": "sha1-IzTBjpx1n3vdVv3vfprj1YjmjtM=", + "engines": { + "node": ">=4" + } + }, + "node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/unbox-primitive": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.1.tgz", + "integrity": "sha512-tZU/3NqK3dA5gpE1KtyiJUrEB0lxnGkMFHptJ7q6ewdZ8s12QrODwNbhIJStmJkd1QDXa1NRA8aF2A1zk/Ypyw==", + "dependencies": { + "function-bind": "^1.1.1", + "has-bigints": "^1.0.1", + "has-symbols": "^1.0.2", + "which-boxed-primitive": "^1.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/validate-npm-package-license": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz", + "integrity": "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==", + "dependencies": { + "spdx-correct": "^3.0.0", + "spdx-expression-parse": "^3.0.0" + } + }, + "node_modules/which": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", + "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "which": "bin/which" + } + }, + "node_modules/which-boxed-primitive": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.0.2.tgz", + "integrity": "sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg==", + "dependencies": { + "is-bigint": "^1.0.1", + "is-boolean-object": "^1.1.0", + "is-number-object": "^1.0.4", + "is-string": "^1.0.5", + "is-symbol": "^1.0.3" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/ws": { + "version": "6.1.3", + "resolved": "https://registry.npmjs.org/ws/-/ws-6.1.3.tgz", + "integrity": "sha512-tbSxiT+qJI223AP4iLfQbkbxkwdFcneYinM2+x46Gx2wgvbaOMO36czfdfVUBRTHvzAMRhDd98sA5d/BuWbQdg==", + "dependencies": { + "async-limiter": "~1.0.0" + } + }, + "node_modules/xmlhttprequest": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/xmlhttprequest/-/xmlhttprequest-1.8.0.tgz", + "integrity": "sha1-Z/4HXFwk/vOfnWX197f+dRcZaPw=", + "engines": { + "node": ">=0.4.0" + } + } + }, + "dependencies": { + "@twilio/audioplayer": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/@twilio/audioplayer/-/audioplayer-1.0.6.tgz", + "integrity": "sha512-c9cjX/ifICgXqShtyAQdVMqfe7odnxougiuRMXBJtn3dZ320mFdt7kmuKedpNnc3ZJ6irOZ9M9MZi9/vuEqHiw==", + "requires": { + "babel-runtime": "^6.26.0" + } + }, + "@twilio/voice-errors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@twilio/voice-errors/-/voice-errors-1.1.1.tgz", + "integrity": "sha512-3IJzRhgAqsS3uW2PO7crUXEFxuFhggHeLvt/Q4hz7lrTLFChl37hWiImCMIaM5VHiybQi6ECVQsId2X8UdTr2A==", + "requires": { + "npm-run-all": "^4.1.5" + } + }, + "@twilio/voice-sdk": { + "version": "git+ssh://git@github.com/twilio/twilio-voice.js.git#4e7135bb029ac00b90b9fd136f3c0914159402eb", + "from": "@twilio/voice-sdk@git+https://github.com/twilio/twilio-voice.js.git#2.0.0", + "requires": { + "@twilio/audioplayer": "1.0.6", + "@twilio/voice-errors": "1.1.1", + "backoff": "2.5.0", + "loglevel": "1.6.7", + "rtcpeerconnection-shim": "1.2.8", + "ws": "6.1.3", + "xmlhttprequest": "1.8.0" + } + }, + "ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "requires": { + "color-convert": "^1.9.0" + } + }, + "async-limiter": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/async-limiter/-/async-limiter-1.0.1.tgz", + "integrity": "sha512-csOlWGAcRFJaI6m+F2WKdnMKr4HhdhFVBk0H/QbJFMCr+uO2kwohwXQPxw/9OCxp05r5ghVBFSyioixx3gfkNQ==" + }, + "babel-runtime": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/babel-runtime/-/babel-runtime-6.26.0.tgz", + "integrity": "sha1-llxwWGaOgrVde/4E/yM3vItWR/4=", + "requires": { + "core-js": "^2.4.0", + "regenerator-runtime": "^0.11.0" + } + }, + "backoff": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/backoff/-/backoff-2.5.0.tgz", + "integrity": "sha1-9hbtqdPktmuMp/ynn2lXIsX44m8=", + "requires": { + "precond": "0.2" + } + }, + "balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" + }, + "brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "requires": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "call-bind": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz", + "integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==", + "requires": { + "function-bind": "^1.1.1", + "get-intrinsic": "^1.0.2" + } + }, + "chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "requires": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + } + }, + "color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "requires": { + "color-name": "1.1.3" + } + }, + "color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=" + }, + "concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=" + }, + "core-js": { + "version": "2.6.12", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-2.6.12.tgz", + "integrity": "sha512-Kb2wC0fvsWfQrgk8HU5lW6U/Lcs8+9aaYcy4ZFc6DDlo4nZ7n70dEgE5rtR0oG6ufKDUnrwfWL1mXR5ljDatrQ==" + }, + "cross-spawn": { + "version": "6.0.5", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz", + "integrity": "sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==", + "requires": { + "nice-try": "^1.0.4", + "path-key": "^2.0.1", + "semver": "^5.5.0", + "shebang-command": "^1.2.0", + "which": "^1.2.9" + } + }, + "define-properties": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.3.tgz", + "integrity": "sha512-3MqfYKj2lLzdMSf8ZIZE/V+Zuy+BgD6f164e8K2w7dgnpKArBDerGYpM46IYYcjnkdPNMjPk9A6VFB8+3SKlXQ==", + "requires": { + "object-keys": "^1.0.12" + } + }, + "error-ex": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", + "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", + "requires": { + "is-arrayish": "^0.2.1" + } + }, + "es-abstract": { + "version": "1.18.3", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.18.3.tgz", + "integrity": "sha512-nQIr12dxV7SSxE6r6f1l3DtAeEYdsGpps13dR0TwJg1S8gyp4ZPgy3FZcHBgbiQqnoqSTb+oC+kO4UQ0C/J8vw==", + "requires": { + "call-bind": "^1.0.2", + "es-to-primitive": "^1.2.1", + "function-bind": "^1.1.1", + "get-intrinsic": "^1.1.1", + "has": "^1.0.3", + "has-symbols": "^1.0.2", + "is-callable": "^1.2.3", + "is-negative-zero": "^2.0.1", + "is-regex": "^1.1.3", + "is-string": "^1.0.6", + "object-inspect": "^1.10.3", + "object-keys": "^1.1.1", + "object.assign": "^4.1.2", + "string.prototype.trimend": "^1.0.4", + "string.prototype.trimstart": "^1.0.4", + "unbox-primitive": "^1.0.1" + } + }, + "es-to-primitive": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.1.tgz", + "integrity": "sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==", + "requires": { + "is-callable": "^1.1.4", + "is-date-object": "^1.0.1", + "is-symbol": "^1.0.2" + } + }, + "escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=" + }, + "function-bind": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", + "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==" + }, + "get-intrinsic": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.1.1.tgz", + "integrity": "sha512-kWZrnVM42QCiEA2Ig1bG8zjoIMOgxWwYCEeNdwY6Tv/cOSeGpcoX4pXHfKUxNKVoArnrEr2e9srnAxxGIraS9Q==", + "requires": { + "function-bind": "^1.1.1", + "has": "^1.0.3", + "has-symbols": "^1.0.1" + } + }, + "graceful-fs": { + "version": "4.2.6", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.6.tgz", + "integrity": "sha512-nTnJ528pbqxYanhpDYsi4Rd8MAeaBA67+RZ10CM1m3bTAVFEDcd5AuA4a6W5YkGZ1iNXHzZz8T6TBKLeBuNriQ==" + }, + "has": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", + "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", + "requires": { + "function-bind": "^1.1.1" + } + }, + "has-bigints": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.0.1.tgz", + "integrity": "sha512-LSBS2LjbNBTf6287JEbEzvJgftkF5qFkmCo9hDRpAzKhUOlJ+hx8dd4USs00SgsUNwc4617J9ki5YtEClM2ffA==" + }, + "has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=" + }, + "has-symbols": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.2.tgz", + "integrity": "sha512-chXa79rL/UC2KlX17jo3vRGz0azaWEx5tGqZg5pO3NUyEJVB17dMruQlzCCOfUvElghKcm5194+BCRvi2Rv/Gw==" + }, + "hosted-git-info": { + "version": "2.8.9", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.9.tgz", + "integrity": "sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==" + }, + "is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0=" + }, + "is-bigint": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.0.2.tgz", + "integrity": "sha512-0JV5+SOCQkIdzjBK9buARcV804Ddu7A0Qet6sHi3FimE9ne6m4BGQZfRn+NZiXbBk4F4XmHfDZIipLj9pX8dSA==" + }, + "is-boolean-object": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.1.1.tgz", + "integrity": "sha512-bXdQWkECBUIAcCkeH1unwJLIpZYaa5VvuygSyS/c2lf719mTKZDU5UdDRlpd01UjADgmW8RfqaP+mRaVPdr/Ng==", + "requires": { + "call-bind": "^1.0.2" + } + }, + "is-callable": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.3.tgz", + "integrity": "sha512-J1DcMe8UYTBSrKezuIUTUwjXsho29693unXM2YhJUTR2txK/eG47bvNa/wipPFmZFgr/N6f1GA66dv0mEyTIyQ==" + }, + "is-core-module": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.4.0.tgz", + "integrity": "sha512-6A2fkfq1rfeQZjxrZJGerpLCTHRNEBiSgnu0+obeJpEPZRUooHgsizvzv0ZjJwOz3iWIHdJtVWJ/tmPr3D21/A==", + "requires": { + "has": "^1.0.3" + } + }, + "is-date-object": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.4.tgz", + "integrity": "sha512-/b4ZVsG7Z5XVtIxs/h9W8nvfLgSAyKYdtGWQLbqy6jA1icmgjf8WCoTKgeS4wy5tYaPePouzFMANbnj94c2Z+A==" + }, + "is-negative-zero": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.1.tgz", + "integrity": "sha512-2z6JzQvZRa9A2Y7xC6dQQm4FSTSTNWjKIYYTt4246eMTJmIo0Q+ZyOsU66X8lxK1AbB92dFeglPLrhwpeRKO6w==" + }, + "is-number-object": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.0.5.tgz", + "integrity": "sha512-RU0lI/n95pMoUKu9v1BZP5MBcZuNSVJkMkAG2dJqC4z2GlkGUNeH68SuHuBKBD/XFe+LHZ+f9BKkLET60Niedw==" + }, + "is-regex": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.3.tgz", + "integrity": "sha512-qSVXFz28HM7y+IWX6vLCsexdlvzT1PJNFSBuaQLQ5o0IEw8UDYW6/2+eCMVyIsbM8CNLX2a/QWmSpyxYEHY7CQ==", + "requires": { + "call-bind": "^1.0.2", + "has-symbols": "^1.0.2" + } + }, + "is-string": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.0.6.tgz", + "integrity": "sha512-2gdzbKUuqtQ3lYNrUTQYoClPhm7oQu4UdpSZMp1/DGgkHBT8E2Z1l0yMdb6D4zNAxwDiMv8MdulKROJGNl0Q0w==" + }, + "is-symbol": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.4.tgz", + "integrity": "sha512-C/CPBqKWnvdcxqIARxyOh4v1UUEOCHpgDa0WYgpKDFMszcrPcffg5uhwSgPCLD2WWxmq6isisz87tzT01tuGhg==", + "requires": { + "has-symbols": "^1.0.2" + } + }, + "isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=" + }, + "json-parse-better-errors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz", + "integrity": "sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw==" + }, + "load-json-file": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-4.0.0.tgz", + "integrity": "sha1-L19Fq5HjMhYjT9U62rZo607AmTs=", + "requires": { + "graceful-fs": "^4.1.2", + "parse-json": "^4.0.0", + "pify": "^3.0.0", + "strip-bom": "^3.0.0" + } + }, + "loglevel": { + "version": "1.6.7", + "resolved": "https://registry.npmjs.org/loglevel/-/loglevel-1.6.7.tgz", + "integrity": "sha512-cY2eLFrQSAfVPhCgH1s7JI73tMbg9YC3v3+ZHVW67sBS7UxWzNEk/ZBbSfLykBWHp33dqqtOv82gjhKEi81T/A==" + }, + "memorystream": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/memorystream/-/memorystream-0.3.1.tgz", + "integrity": "sha1-htcJCzDORV1j+64S3aUaR93K+bI=" + }, + "minimatch": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", + "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", + "requires": { + "brace-expansion": "^1.1.7" + } + }, + "nice-try": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz", + "integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==" + }, + "normalize-package-data": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz", + "integrity": "sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==", + "requires": { + "hosted-git-info": "^2.1.4", + "resolve": "^1.10.0", + "semver": "2 || 3 || 4 || 5", + "validate-npm-package-license": "^3.0.1" + } + }, + "npm-run-all": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/npm-run-all/-/npm-run-all-4.1.5.tgz", + "integrity": "sha512-Oo82gJDAVcaMdi3nuoKFavkIHBRVqQ1qvMb+9LHk/cF4P6B2m8aP04hGf7oL6wZ9BuGwX1onlLhpuoofSyoQDQ==", + "requires": { + "ansi-styles": "^3.2.1", + "chalk": "^2.4.1", + "cross-spawn": "^6.0.5", + "memorystream": "^0.3.1", + "minimatch": "^3.0.4", + "pidtree": "^0.3.0", + "read-pkg": "^3.0.0", + "shell-quote": "^1.6.1", + "string.prototype.padend": "^3.0.0" + } + }, + "object-inspect": { + "version": "1.10.3", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.10.3.tgz", + "integrity": "sha512-e5mCJlSH7poANfC8z8S9s9S2IN5/4Zb3aZ33f5s8YqoazCFzNLloLU8r5VCG+G7WoqLvAAZoVMcy3tp/3X0Plw==" + }, + "object-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==" + }, + "object.assign": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.2.tgz", + "integrity": "sha512-ixT2L5THXsApyiUPYKmW+2EHpXXe5Ii3M+f4e+aJFAHao5amFRW6J0OO6c/LU8Be47utCx2GL89hxGB6XSmKuQ==", + "requires": { + "call-bind": "^1.0.0", + "define-properties": "^1.1.3", + "has-symbols": "^1.0.1", + "object-keys": "^1.1.1" + } + }, + "parse-json": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-4.0.0.tgz", + "integrity": "sha1-vjX1Qlvh9/bHRxhPmKeIy5lHfuA=", + "requires": { + "error-ex": "^1.3.1", + "json-parse-better-errors": "^1.0.1" + } + }, + "path-key": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz", + "integrity": "sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A=" + }, + "path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==" + }, + "path-type": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-3.0.0.tgz", + "integrity": "sha512-T2ZUsdZFHgA3u4e5PfPbjd7HDDpxPnQb5jN0SrDsjNSuVXHJqtwTnWqG0B1jZrgmJ/7lj1EmVIByWt1gxGkWvg==", + "requires": { + "pify": "^3.0.0" + } + }, + "pidtree": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/pidtree/-/pidtree-0.3.1.tgz", + "integrity": "sha512-qQbW94hLHEqCg7nhby4yRC7G2+jYHY4Rguc2bjw7Uug4GIJuu1tvf2uHaZv5Q8zdt+WKJ6qK1FOI6amaWUo5FA==" + }, + "pify": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", + "integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=" + }, + "precond": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/precond/-/precond-0.2.3.tgz", + "integrity": "sha1-qpWRvKokkj8eD0hJ0kD0fvwQdaw=" + }, + "read-pkg": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-3.0.0.tgz", + "integrity": "sha1-nLxoaXj+5l0WwA4rGcI3/Pbjg4k=", + "requires": { + "load-json-file": "^4.0.0", + "normalize-package-data": "^2.3.2", + "path-type": "^3.0.0" + } + }, + "regenerator-runtime": { + "version": "0.11.1", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.11.1.tgz", + "integrity": "sha512-MguG95oij0fC3QV3URf4V2SDYGJhJnJGqvIIgdECeODCT98wSWDAJ94SSuVpYQUoTcGUIL6L4yNB7j1DFFHSBg==" + }, + "resolve": { + "version": "1.20.0", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.20.0.tgz", + "integrity": "sha512-wENBPt4ySzg4ybFQW2TT1zMQucPK95HSh/nq2CFTZVOGut2+pQvSsgtda4d26YrYcr067wjbmzOG8byDPBX63A==", + "requires": { + "is-core-module": "^2.2.0", + "path-parse": "^1.0.6" + } + }, + "rtcpeerconnection-shim": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/rtcpeerconnection-shim/-/rtcpeerconnection-shim-1.2.8.tgz", + "integrity": "sha512-5Sx90FGru1sQw9aGOM+kHU4i6mbP8eJPgxliu2X3Syhg8qgDybx8dpDTxUwfJvPnubXFnZeRNl59DWr4AttJKQ==", + "requires": { + "sdp": "^2.6.0" + } + }, + "sdp": { + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/sdp/-/sdp-2.12.0.tgz", + "integrity": "sha512-jhXqQAQVM+8Xj5EjJGVweuEzgtGWb3tmEEpl3CLP3cStInSbVHSg0QWOGQzNq8pSID4JkpeV2mPqlMDLrm0/Vw==" + }, + "semver": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==" + }, + "shebang-command": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz", + "integrity": "sha1-RKrGW2lbAzmJaMOfNj/uXer98eo=", + "requires": { + "shebang-regex": "^1.0.0" + } + }, + "shebang-regex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz", + "integrity": "sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM=" + }, + "shell-quote": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.7.2.tgz", + "integrity": "sha512-mRz/m/JVscCrkMyPqHc/bczi3OQHkLTqXHEFu0zDhK/qfv3UcOA4SVmRCLmos4bhjr9ekVQubj/R7waKapmiQg==" + }, + "spdx-correct": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.1.1.tgz", + "integrity": "sha512-cOYcUWwhCuHCXi49RhFRCyJEK3iPj1Ziz9DpViV3tbZOwXD49QzIN3MpOLJNxh2qwq2lJJZaKMVw9qNi4jTC0w==", + "requires": { + "spdx-expression-parse": "^3.0.0", + "spdx-license-ids": "^3.0.0" + } + }, + "spdx-exceptions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.3.0.tgz", + "integrity": "sha512-/tTrYOC7PPI1nUAgx34hUpqXuyJG+DTHJTnIULG4rDygi4xu/tfgmq1e1cIRwRzwZgo4NLySi+ricLkZkw4i5A==" + }, + "spdx-expression-parse": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz", + "integrity": "sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==", + "requires": { + "spdx-exceptions": "^2.1.0", + "spdx-license-ids": "^3.0.0" + } + }, + "spdx-license-ids": { + "version": "3.0.9", + "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.9.tgz", + "integrity": "sha512-Ki212dKK4ogX+xDo4CtOZBVIwhsKBEfsEEcwmJfLQzirgc2jIWdzg40Unxz/HzEUqM1WFzVlQSMF9kZZ2HboLQ==" + }, + "string.prototype.padend": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/string.prototype.padend/-/string.prototype.padend-3.1.2.tgz", + "integrity": "sha512-/AQFLdYvePENU3W5rgurfWSMU6n+Ww8n/3cUt7E+vPBB/D7YDG8x+qjoFs4M/alR2bW7Qg6xMjVwWUOvuQ0XpQ==", + "requires": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.3", + "es-abstract": "^1.18.0-next.2" + } + }, + "string.prototype.trimend": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.4.tgz", + "integrity": "sha512-y9xCjw1P23Awk8EvTpcyL2NIr1j7wJ39f+k6lvRnSMz+mz9CGz9NYPelDk42kOz6+ql8xjfK8oYzy3jAP5QU5A==", + "requires": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.3" + } + }, + "string.prototype.trimstart": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.4.tgz", + "integrity": "sha512-jh6e984OBfvxS50tdY2nRZnoC5/mLFKOREQfw8t5yytkoUsJRNxvI/E39qu1sD0OtWI3OC0XgKSmcWwziwYuZw==", + "requires": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.3" + } + }, + "strip-bom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", + "integrity": "sha1-IzTBjpx1n3vdVv3vfprj1YjmjtM=" + }, + "supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "requires": { + "has-flag": "^3.0.0" + } + }, + "unbox-primitive": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.1.tgz", + "integrity": "sha512-tZU/3NqK3dA5gpE1KtyiJUrEB0lxnGkMFHptJ7q6ewdZ8s12QrODwNbhIJStmJkd1QDXa1NRA8aF2A1zk/Ypyw==", + "requires": { + "function-bind": "^1.1.1", + "has-bigints": "^1.0.1", + "has-symbols": "^1.0.2", + "which-boxed-primitive": "^1.0.2" + } + }, + "validate-npm-package-license": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz", + "integrity": "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==", + "requires": { + "spdx-correct": "^3.0.0", + "spdx-expression-parse": "^3.0.0" + } + }, + "which": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", + "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", + "requires": { + "isexe": "^2.0.0" + } + }, + "which-boxed-primitive": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.0.2.tgz", + "integrity": "sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg==", + "requires": { + "is-bigint": "^1.0.1", + "is-boolean-object": "^1.1.0", + "is-number-object": "^1.0.4", + "is-string": "^1.0.5", + "is-symbol": "^1.0.3" + } + }, + "ws": { + "version": "6.1.3", + "resolved": "https://registry.npmjs.org/ws/-/ws-6.1.3.tgz", + "integrity": "sha512-tbSxiT+qJI223AP4iLfQbkbxkwdFcneYinM2+x46Gx2wgvbaOMO36czfdfVUBRTHvzAMRhDd98sA5d/BuWbQdg==", + "requires": { + "async-limiter": "~1.0.0" + } + }, + "xmlhttprequest": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/xmlhttprequest/-/xmlhttprequest-1.8.0.tgz", + "integrity": "sha1-Z/4HXFwk/vOfnWX197f+dRcZaPw=" + } + } +} diff --git a/static/package.json b/static/package.json new file mode 100644 index 0000000..3de154e --- /dev/null +++ b/static/package.json @@ -0,0 +1,14 @@ +{ + "name": "twilio-voice-sdk-flask", + "version": "1.0.0", + "main": "quickstart.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "dependencies": { + "@twilio/voice-sdk": "git+https://github.com/twilio/twilio-voice.js.git#2.0.0" + }, + "author": "", + "license": "ISC", + "description": "" +} diff --git a/static/quickstart.js b/static/quickstart.js index 2360da9..4479ab6 100644 --- a/static/quickstart.js +++ b/static/quickstart.js @@ -1,202 +1,307 @@ $(function () { - var speakerDevices = document.getElementById('speaker-devices'); - var ringtoneDevices = document.getElementById('ringtone-devices'); - var outputVolumeBar = document.getElementById('output-volume'); - var inputVolumeBar = document.getElementById('input-volume'); - var volumeIndicators = document.getElementById('volume-indicators'); - - log('Requesting Capability Token...'); - $.getJSON('/token') - .then(function (data) { - log('Got a token.'); - console.log('Token: ' + data.token); - - // Setup Twilio.Device - device = new Twilio.Device(data.token, { - // Set Opus as our preferred codec. Opus generally performs better, requiring less bandwidth and - // providing better audio quality in restrained network conditions. Opus will be default in 2.0. - codecPreferences: ["opus", "pcmu"], - // Use fake DTMF tones client-side. Real tones are still sent to the other end of the call, - // but the client-side DTMF tones are fake. This prevents the local mic capturing the DTMF tone - // a second time and sending the tone twice. This will be default in 2.0. - fakeLocalDTMF: true, - // Use `enableRingingState` to enable the device to emit the `ringing` - // state. The TwiML backend also needs to have the attribute - // `answerOnBridge` also set to true in the `Dial` verb. This option - // changes the behavior of the SDK to consider a call `ringing` starting - // from the connection to the TwiML backend to when the recipient of - // the `Dial` verb answers. - enableRingingState: true - }); - - device.on("ready", function (device) { - log("Twilio.Device Ready!"); - document.getElementById("call-controls").style.display = "block"; - }); + const speakerDevices = document.getElementById("speaker-devices"); + const ringtoneDevices = document.getElementById("ringtone-devices"); + const outputVolumeBar = document.getElementById("output-volume"); + const inputVolumeBar = document.getElementById("input-volume"); + const volumeIndicators = document.getElementById("volume-indicators"); + const callButton = document.getElementById("button-call"); + const hangupButton = document.getElementById("button-hangup"); + const callControlsDiv = document.getElementById("call-controls"); + const audioSelectionDiv = document.getElementById("output-selection"); + const getAudioDevicesButton = document.getElementById("get-devices"); + const logDiv = document.getElementById("log"); + const incomingCallDiv = document.getElementById("incoming-call"); + const incomingCallHangupButton = document.getElementById( + "button-hangup-incoming" + ); + const incomingCallAcceptButton = document.getElementById( + "button-accept-incoming" + ); + const incomingCallRejectButton = document.getElementById( + "button-reject-incoming" + ); + const phoneNumberInput = document.getElementById("phone-number"); + const incomingPhoneNumberEl = document.getElementById("incoming-number"); + const startupButton = document.getElementById("startup-button"); - device.on("error", function (error) { - log("Twilio.Device Error: " + error.message); - }); + let device; + let token; - device.on("connect", function (conn) { - log("Successfully established call!"); - document.getElementById("button-call").style.display = "none"; - document.getElementById("button-hangup").style.display = "inline"; - volumeIndicators.style.display = 'block'; - bindVolumeIndicators(conn); - }); + // Event Listeners - device.on("disconnect", function (conn) { - log("Call ended."); - document.getElementById("button-call").style.display = "inline"; - document.getElementById("button-hangup").style.display = "none"; - volumeIndicators.style.display = 'none'; - }); + callButton.onclick = (e) => { + e.preventDefault(); + makeOutgoingCall(); + }; + getAudioDevicesButton.onclick = getAudioDevices; + speakerDevices.addEventListener("change", updateOutputDevice); + ringtoneDevices.addEventListener("change", updateRingtoneDevice); + hangupButton.onclick = hangup; - device.on("incoming", function (conn) { - log("Incoming connection from " + conn.parameters.From); - var archEnemyPhoneNumber = "+12093373517"; + // SETUP STEP 1: + // Browser client should be started after a user gesture + // to avoid errors in the browser console re: AudioContext + startupButton.addEventListener("click", startupClient); - if (conn.parameters.From === archEnemyPhoneNumber) { - conn.reject(); - log("It's your nemesis. Rejected call."); - } else { - // accept the incoming connection and start two-way audio - conn.accept(); - } - }); + // SETUP STEP 2: Request an Access Token + async function startupClient() { + log("Requesting Access Token..."); + try { + const data = await $.getJSON("/token"); + log("Got a token."); + token = data.token; setClientNameUI(data.identity); + intitializeDevice(); + } catch (err) { + console.log(err); + log("An error occurred. See your browser console for more information."); + } + } - device.audio.on("deviceChange", updateAllDevices.bind(device)); + // SETUP STEP 3: + // Instantiate a new Twilio.Device + function intitializeDevice() { + logDiv.classList.remove("hide"); + log("Initializing device"); + device = new Twilio.Device(token, { + debug: true, + answerOnBridge: true, + // Set Opus as our preferred codec. Opus generally performs better, requiring less bandwidth and + // providing better audio quality in restrained network conditions. Opus will be default in 2.0. + codecPreferences: ["opus", "pcmu"], + }); - // Show audio selection UI if it is supported by the browser. - if (device.audio.isOutputSelectionSupported) { - document.getElementById("output-selection").style.display = "block"; - } - }) - .catch(function (err) { - console.log(err); - log("Could not get a token from server!"); + addDeviceListeners(device); + + // Device must be registered in order to receive incoming calls + device.register(); + } + + // SETUP STEP 4: + // Listen for Twilio.Device states + function addDeviceListeners(device) { + device.on("registered", function () { + log("Twilio.Device Ready to make and receive calls!"); + callControlsDiv.classList.remove("hide"); }); - - // Bind button to make call - document.getElementById('button-call').onclick = function () { - // get the phone number to connect the call to + + device.on("error", function (error) { + log("Twilio.Device Error: " + error.message); + }); + + device.on("incoming", handleIncomingCall); + + device.audio.on("deviceChange", updateAllAudioDevices.bind(device)); + + // Show audio selection UI if it is supported by the browser. + if (device.audio.isOutputSelectionSupported) { + audioSelectionDiv.classList.remove("hide"); + } + } + + // MAKE AN OUTGOING CALL + + async function makeOutgoingCall() { var params = { - To: document.getElementById('phone-number').value + // get the phone number to call from the DOM + phone: phoneNumberInput.value, }; - console.log('Calling ' + params.To + '...'); if (device) { - var outgoingConnection = device.connect(params); - outgoingConnection.on("ringing", function () { - log("Ringing..."); - }); + log(`Attempting to call ${params.phone} ...`); + + // Twilio.Device.connect() returns a Call object + const call = await device.connect({ params }); + + // add listeners to the Call + // "accepted" means the call has finished connecting and the state is now "open" + call.addListener("accept", updateUIAcceptedCall); + call.addListener("disconnect", updateUIDisconnectedCall); + } else { + log("Unable to make call."); } - }; + } + + function updateUIAcceptedCall(call) { + log("Call in progress ..."); + callButton.disabled = true; + hangupButton.classList.remove("hide"); + volumeIndicators.classList.remove("hide"); + bindVolumeIndicators(call); + } + + function updateUIDisconnectedCall() { + log("Call disconnected."); + callButton.disabled = false; + hangupButton.classList.add("hide"); + volumeIndicators.classList.add("hide"); + } - // Bind button to hangup call - document.getElementById('button-hangup').onclick = function () { - log('Hanging up...'); + // HANG UP A CALL + + function hangup() { + log("Hanging up ..."); if (device) { device.disconnectAll(); } - }; + } - document.getElementById('get-devices').onclick = function() { - navigator.mediaDevices.getUserMedia({ audio: true }) - .then(updateAllDevices); - }; + // HANDLE INCOMING CALL - speakerDevices.addEventListener("change", function () { - var selectedDevices = [].slice - .call(speakerDevices.children) - .filter(function (node) { - return node.selected; - }) - .map(function (node) { - return node.getAttribute("data-id"); - }); + function handleIncomingCall(call) { + log(`Incoming call from ${call.parameters.From}`); + + //show incoming call div and incoming phone number + incomingCallDiv.classList.remove("hide"); + incomingPhoneNumberEl.innerHTML = call.parameters.From; + + //add event listeners for Accept, Reject, and Hangup buttons + incomingCallAcceptButton.onclick = () => { + acceptIncomingCall(call); + }; + + incomingCallRejectButton.onclick = () => { + rejectIncomingCall(call); + }; + + incomingCallHangupButton.onclick = () => { + hangupIncomingCall(call); + }; + + // add event listener to call object + call.addListener("cancel", handleDisconnectedIncomingCall); + } + + // ACCEPT INCOMING CALL + + function acceptIncomingCall(call) { + call.accept(); + + //update UI + log("Accepted incoming call."); + incomingCallAcceptButton.classList.add("hide"); + incomingCallRejectButton.classList.add("hide"); + incomingCallHangupButton.classList.remove("hide"); + } + + // REJECT INCOMING CALL + + function rejectIncomingCall(call) { + call.reject(); + + log("Rejected incoming call"); + resetIncomingCallUI(); + } + + // HANG UP INCOMING CALL + + function hangupIncomingCall() { + hangup(); + + resetIncomingCallUI(); + } + + // HANDLE CANCELLED INCOMING CALL + + function handleDisconnectedIncomingCall(call) { + device.disconnectAll(); + console.log("handledisconnected : ", call); + log("Incoming call ended."); + resetIncomingCallUI(); + } + + // MISC USER INTERFACE + + // Activity log + function log(message) { + logDiv.innerHTML += `

>  ${message}

`; + logDiv.scrollTop = logDiv.scrollHeight; + } + + function setClientNameUI(clientName) { + var div = document.getElementById("client-name"); + div.innerHTML = `Your client name: ${clientName}`; + } + + function resetIncomingCallUI() { + incomingPhoneNumberEl.innerHTML = ""; + incomingCallDiv.classList.add("hide"); + } + + // AUDIO CONTROLS + + async function getAudioDevices() { + await navigator.mediaDevices.getUserMedia({ audio: true }); + updateAllAudioDevices.bind(device); + } + + function updateAllAudioDevices() { + if (device) { + updateDevices(speakerDevices, device.audio.speakerDevices.get()); + updateDevices(ringtoneDevices, device.audio.ringtoneDevices.get()); + } + } + + function updateOutputDevice() { + const selectedDevices = Array.from(speakerDevices.children) + .filter((node) => node.selected) + .map((node) => node.getAttribute("data-id")); device.audio.speakerDevices.set(selectedDevices); - }); - - ringtoneDevices.addEventListener("change", function () { - var selectedDevices = [].slice - .call(ringtoneDevices.children) - .filter(function (node) { - return node.selected; - }) - .map(function (node) { - return node.getAttribute("data-id"); - }); + } + + function updateRingtoneDevice() { + const selectedDevices = Array.from(ringtoneDevices.children) + .filter((node) => node.selected) + .map((node) => node.getAttribute("data-id")); device.audio.ringtoneDevices.set(selectedDevices); - }); - - function bindVolumeIndicators(connection) { - connection.volume(function (inputVolume, outputVolume) { - var inputColor = 'red'; - if (inputVolume < .50) { - inputColor = 'green'; - } else if (inputVolume < .75) { - inputColor = 'yellow'; + } + + function bindVolumeIndicators(call) { + call.on("volume", function (inputVolume, outputVolume) { + var inputColor = "red"; + if (inputVolume < 0.5) { + inputColor = "green"; + } else if (inputVolume < 0.75) { + inputColor = "yellow"; } - inputVolumeBar.style.width = Math.floor(inputVolume * 300) + 'px'; + inputVolumeBar.style.width = Math.floor(inputVolume * 300) + "px"; inputVolumeBar.style.background = inputColor; - var outputColor = 'red'; - if (outputVolume < .50) { - outputColor = 'green'; - } else if (outputVolume < .75) { - outputColor = 'yellow'; + var outputColor = "red"; + if (outputVolume < 0.5) { + outputColor = "green"; + } else if (outputVolume < 0.75) { + outputColor = "yellow"; } - outputVolumeBar.style.width = Math.floor(outputVolume * 300) + 'px'; + outputVolumeBar.style.width = Math.floor(outputVolume * 300) + "px"; outputVolumeBar.style.background = outputColor; }); } - function updateAllDevices() { - updateDevices(speakerDevices, device.audio.speakerDevices.get()); - updateDevices(ringtoneDevices, device.audio.ringtoneDevices.get()); - } -}); + // Update the available ringtone and speaker devices + function updateDevices(selectEl, selectedDevices) { + selectEl.innerHTML = ""; -// Update the available ringtone and speaker devices -function updateDevices(selectEl, selectedDevices) { - selectEl.innerHTML = ""; + device.audio.availableOutputDevices.forEach(function (device, id) { + var isActive = selectedDevices.size === 0 && id === "default"; + selectedDevices.forEach(function (device) { + if (device.deviceId === id) { + isActive = true; + } + }); - device.audio.availableOutputDevices.forEach(function (device, id) { - var isActive = selectedDevices.size === 0 && id === "default"; - selectedDevices.forEach(function (device) { - if (device.deviceId === id) { - isActive = true; + var option = document.createElement("option"); + option.label = device.label; + option.setAttribute("data-id", id); + if (isActive) { + option.setAttribute("selected", "selected"); } + selectEl.appendChild(option); }); - - var option = document.createElement("option"); - option.label = device.label; - option.setAttribute("data-id", id); - if (isActive) { - option.setAttribute("selected", "selected"); - } - selectEl.appendChild(option); - }); -} - -// Activity log -function log(message) { - var logDiv = document.getElementById('log'); - logDiv.innerHTML += '

> ' + message + '

'; - logDiv.scrollTop = logDiv.scrollHeight; -} - -// Set the client name in the UI -function setClientNameUI(clientName) { - var div = document.getElementById('client-name'); - div.innerHTML = 'Your client name: ' + clientName + - ''; -} + } +}); diff --git a/static/site.css b/static/site.css index 66c6f31..4d9341f 100644 --- a/static/site.css +++ b/static/site.css @@ -3,146 +3,122 @@ body, p { padding: 0; - margin: 0; + margin: auto; + font-family: Arial, Helvetica, sans-serif; } -body { - background: #272726; +h1 { + text-align: center; +} + +h2 { + margin-top: 0; + border-bottom: 1px solid black; +} + +button { + margin-bottom: 10px; } label { text-align: left; - font-family: Helvetica, sans-serif; font-size: 1.25em; color: #777776; display: block; } -div#controls { +header { + text-align: center; +} + +main { padding: 3em; max-width: 1200px; margin: 0 auto; + display: flex; + justify-content: space-between; + align-items: flex-start; +} + +.left-column, +.center-column, +.right-column { + width: 30%; + min-width: 16em; + margin: 0 1.5em; + text-align: center; +} + +/* Left Column */ +#client-name { + text-align: left; + margin-bottom: 1em; + font-family: "Helvetica Light", Helvetica, sans-serif; + font-size: 1.25em; + color: #777776; +} + +select { + width: 300px; + height: 60px; + margin-bottom: 10px; +} + +/* Center Column */ +input { + font-family: Helvetica-LightOblique, Helvetica, sans-serif; + font-style: oblique; + font-size: 1em; + width: 100%; + height: 2.5em; + padding: 0; + display: block; + margin: 10px 0; +} + +div#volume-indicators { + padding: 10px; + margin-top: 20px; + width: 500px; + text-align: left; +} + +div#volume-indicators > div { + display: block; + height: 20px; + width: 0; +} + +/* Right Column */ +.right-column { + padding: 0 1.5em; } - div#controls div { - float: left; - } - - div#controls div#call-controls, - div#controls div#info { - width: 16em; - margin: 0 1.5em; - text-align: center; - } - div#controls div#info div#output-selection { - display: none; - } - - div#controls div#info a { - font-size: 1.1em; - color: khaki; - text-decoration: underline; - cursor: pointer; - } - - div#controls div#info select { - width: 300px; - height: 60px; - margin-bottom: 2em; - } - - div#controls div#info label { - width: 300px; - } - - div#controls div#call-controls div#volume-indicators { - display: none; - padding: 10px; - margin-top: 20px; - width: 500px; - text-align: left; - } - - div#controls div#call-controls div#volume-indicators > div { - display: block; - height: 20px; - width: 0; - } - - div#controls p.instructions { - text-align: left; - margin-bottom: 1em; - font-family: Helvetica-LightOblique, Helvetica, sans-serif; - font-style: oblique; - font-size: 1.25em; - color: #777776; - } - - div#controls div#info #client-name { - text-align: left; - margin-bottom: 1em; - font-family: "Helvetica Light", Helvetica, sans-serif; - font-size: 1.25em; - color: #777776; - } - - div#controls button { - width: 15em; - height: 2.5em; - margin-top: 1.75em; - border-radius: 1em; - font-family: "Helvetica Light", Helvetica, sans-serif; - font-size: .8em; - font-weight: lighter; - outline: 0; - } - - div#controls button:active { - position: relative; - top: 1px; - } - - div#controls div#call-controls { - display: none; - } - - div#controls div#call-controls input { - font-family: Helvetica-LightOblique, Helvetica, sans-serif; - font-style: oblique; - font-size: 1em; - width: 100%; - height: 2.5em; - padding: .5em; - display: block; - } - - div#controls div#call-controls button { - color: #fff; - background: 0 0; - border: 1px solid #686865; - } - - div#controls div#call-controls button#button-hangup { - display: none; - } - - div#controls div#log { - border: 1px solid #686865; - width: 35%; - height: 9.5em; - margin-top: 2.75em; - text-align: left; - padding: 1.5em; - float: right; - overflow-y: scroll; - } - - div#controls div#log p { - color: #686865; - font-family: 'Share Tech Mono', 'Courier New', Courier, fixed-width; - font-size: 1.25em; - line-height: 1.25em; - margin-left: 1em; - text-indent: -1.25em; - width: 90%; - } +#log { + text-align: left; + border: 1px solid #686865; + padding: 10px; + height: 9.5em; + overflow-y: scroll; +} + +.log-entry { + color: #686865; + font-family: "Share Tech Mono", "Courier New", Courier, fixed-width; + font-size: 1.25em; + line-height: 1.25em; + margin-left: 1em; + text-indent: -1.25em; + width: 90%; +} + +/* Other Styles */ +.hide { + position: absolute !important; + top: -9999px !important; + left: -9999px !important; +} + +button:disabled { + cursor: not-allowed; +} diff --git a/tests/conftest.py b/tests/conftest.py index 50a7f2e..0df2575 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,4 +1,5 @@ import os + import pytest from app import app as flask_app @@ -8,6 +9,7 @@ def app(): yield flask_app + @pytest.fixture def client(app): return app.test_client() diff --git a/tests/test_routes.py b/tests/test_routes.py index 7ccb6c8..4982186 100644 --- a/tests/test_routes.py +++ b/tests/test_routes.py @@ -1,26 +1,36 @@ from unittest.mock import patch +import app + def test_index(app, client): - res = client.get('/') + res = client.get("/") + assert res.status_code == 200 + assert "Make a Call" in res.get_data(as_text=True) + + +def test_voice_has_phone_parameter(app, client): + res = client.post("/voice", data=dict(phone="+593XXXXXXX")) assert res.status_code == 200 - assert 'Make a Call:' in res.get_data(as_text=True) + assert "+593XXXXXXX" in res.get_data(as_text=True) +@patch.object(app, "twilio_number", "+593XXXXXXX") +@patch.object(app, "identity", "nezuko") def test_voice_has_to_parameter(app, client): - res = client.post('/voice', data=dict(To='+593XXXXXXX')) + res = client.post("/voice", data=dict(To="+593XXXXXXX")) assert res.status_code == 200 - assert '+593XXXXXXX' in res.get_data(as_text=True) + assert "nezuko" in res.get_data(as_text=True) -def test_voice_has_not_to_parameter(app, client): - res = client.post('/voice') +def test_voice_has_no_phone_or_to_parameter(app, client): + res = client.post("/voice") assert res.status_code == 200 - assert 'Thanks for calling!' in res.get_data(as_text=True) + assert "Thanks for calling!" in res.get_data(as_text=True) -@patch('app.fake', **{'user_name.return_value': 'nezuko'}) +@patch.object(app, "identity", "nezuko") def test_generate_token(app, client): - res = client.get('/token') + res = client.get("/token") assert '"identity":"nezuko"' in res.get_data(as_text=True) assert '"token"' in res.get_data(as_text=True)