From 1ca215cb9689dae84c778b27856b333bb75f5586 Mon Sep 17 00:00:00 2001 From: Charlie Weems Date: Thu, 18 Jul 2024 18:09:36 -0700 Subject: [PATCH 01/26] Initial Voxray server implementation --- app.js | 87 ++++++++-------------------- services/gpt-service.js | 29 +++++----- services/recording-service.js | 4 +- services/stream-service.js | 60 -------------------- services/text-service.js | 21 +++++++ services/transcription-service.js | 94 ------------------------------- services/tts-service.js | 53 ----------------- 7 files changed, 62 insertions(+), 286 deletions(-) delete mode 100644 services/stream-service.js create mode 100644 services/text-service.js delete mode 100644 services/transcription-service.js delete mode 100644 services/tts-service.js diff --git a/app.js b/app.js index c862debc..094c769a 100644 --- a/app.js +++ b/app.js @@ -5,13 +5,9 @@ const express = require('express'); const ExpressWs = require('express-ws'); const { GptService } = require('./services/gpt-service'); -const { StreamService } = require('./services/stream-service'); -const { TranscriptionService } = require('./services/transcription-service'); -const { TextToSpeechService } = require('./services/tts-service'); +const { TextService } = require('./services/text-service'); const { recordingService } = require('./services/recording-service'); -const VoiceResponse = require('twilio').twiml.VoiceResponse; - const app = express(); ExpressWs(app); @@ -19,10 +15,11 @@ const PORT = process.env.PORT || 3000; app.post('/incoming', (req, res) => { try { - const response = new VoiceResponse(); - const connect = response.connect(); - connect.stream({ url: `wss://${process.env.SERVER}/connection` }); - + const response = ` + + + + `; res.type('text/xml'); res.end(response.toString()); } catch (err) { @@ -30,7 +27,7 @@ app.post('/incoming', (req, res) => { } }); -app.ws('/connection', (ws) => { +app.ws('/sockets', (ws) => { try { ws.on('error', console.error); // Filled in from start message @@ -38,72 +35,34 @@ app.ws('/connection', (ws) => { let callSid; const gptService = new GptService(); - const streamService = new StreamService(ws); - const transcriptionService = new TranscriptionService(); - const ttsService = new TextToSpeechService({}); - - let marks = []; + const textService = new TextService(ws); + let interactionCount = 0; - + // Incoming from MediaStream ws.on('message', function message(data) { const msg = JSON.parse(data); - if (msg.event === 'start') { - streamSid = msg.start.streamSid; - callSid = msg.start.callSid; - - streamService.setStreamSid(streamSid); + console.log(msg); + if (msg.type === 'setup') { + callSid = msg.callSid; gptService.setCallSid(callSid); // Set RECORDING_ENABLED='true' in .env to record calls - recordingService(ttsService, callSid).then(() => { + recordingService(textService, callSid).then(() => { console.log(`Twilio -> Starting Media Stream for ${streamSid}`.underline.red); - ttsService.generate({partialResponseIndex: null, partialResponse: 'Hello! I understand you\'re looking for a pair of AirPods, is that correct?'}, 0); }); - } else if (msg.event === 'media') { - transcriptionService.send(msg.media.payload); - } else if (msg.event === 'mark') { - const label = msg.mark.name; - console.log(`Twilio -> Audio completed mark (${msg.sequenceNumber}): ${label}`.red); - marks = marks.filter(m => m !== msg.mark.name); - } else if (msg.event === 'stop') { - console.log(`Twilio -> Media stream ${streamSid} ended.`.underline.red); - } - }); - - transcriptionService.on('utterance', async (text) => { - // This is a bit of a hack to filter out empty utterances - if(marks.length > 0 && text?.length > 5) { - console.log('Twilio -> Interruption, Clearing stream'.red); - ws.send( - JSON.stringify({ - streamSid, - event: 'clear', - }) - ); + } else if (msg.type === 'prompt') { + gptService.completion(msg.voicePrompt, interactionCount); + interactionCount += 1; + } else if (msg.type === 'interrupt') { + gptService.interrupt(); + console.log('Todo: add interruption handling'); } }); - - transcriptionService.on('transcription', async (text) => { - if (!text) { return; } - console.log(`Interaction ${interactionCount} – STT -> GPT: ${text}`.yellow); - gptService.completion(text, interactionCount); - interactionCount += 1; - }); - - gptService.on('gptreply', async (gptReply, icount) => { + + gptService.on('gptreply', async (gptReply, final, icount) => { console.log(`Interaction ${icount}: GPT -> TTS: ${gptReply.partialResponse}`.green ); - ttsService.generate(gptReply, icount); - }); - - ttsService.on('speech', (responseIndex, audio, label, icount) => { - console.log(`Interaction ${icount}: TTS -> TWILIO: ${label}`.blue); - - streamService.buffer(responseIndex, audio); - }); - - streamService.on('audiosent', (markLabel) => { - marks.push(markLabel); + textService.sendText(gptReply, final); }); } catch (err) { console.log(err); diff --git a/services/gpt-service.js b/services/gpt-service.js index 4defbb09..dfe38202 100644 --- a/services/gpt-service.js +++ b/services/gpt-service.js @@ -20,6 +20,7 @@ class GptService extends EventEmitter { { 'role': 'assistant', 'content': 'Hello! I understand you\'re looking for a pair of AirPods, is that correct?' }, ], this.partialResponseIndex = 0; + this.isInterrupted = false; } // Add the callSid to the chat context in case @@ -28,6 +29,10 @@ class GptService extends EventEmitter { this.userContext.push({ 'role': 'system', 'content': `callSid: ${callSid}` }); } + interrupt () { + this.isInterrupted = true; + } + validateFunctionArgs (args) { try { return JSON.parse(args); @@ -49,10 +54,11 @@ class GptService extends EventEmitter { } async completion(text, interactionCount, role = 'user', name = 'user') { + this.isInterrupted = false; this.updateUserContext(name, role, text); // Step 1: Send user transcription to Chat GPT - const stream = await this.openai.chat.completions.create({ + let stream = await this.openai.chat.completions.create({ model: 'gpt-4-1106-preview', messages: this.userContext, tools: tools, @@ -78,6 +84,10 @@ class GptService extends EventEmitter { } for await (const chunk of stream) { + if (this.isInterrupted) { + break; + } + let content = chunk.choices[0]?.delta?.content || ''; let deltas = chunk.choices[0].delta; finishReason = chunk.choices[0].finish_reason; @@ -100,10 +110,7 @@ class GptService extends EventEmitter { const toolData = tools.find(tool => tool.function.name === functionName); const say = toolData.function.say; - this.emit('gptreply', { - partialResponseIndex: null, - partialResponse: say - }, interactionCount); + this.emit('gptreply', say, false, interactionCount); let functionResponse = await functionToCall(validatedArgs); @@ -118,15 +125,11 @@ class GptService extends EventEmitter { // We use partialResponse to provide a chunk for TTS partialResponse += content; // Emit last partial response and add complete response to userContext - if (content.trim().slice(-1) === '•' || finishReason === 'stop') { - const gptReply = { - partialResponseIndex: this.partialResponseIndex, - partialResponse - }; - - this.emit('gptreply', gptReply, interactionCount); - this.partialResponseIndex++; + if (content.trim().slice(-1) === '•') { + this.emit('gptreply', partialResponse, false, interactionCount); partialResponse = ''; + } else if (finishReason === 'stop') { + this.emit('gptreply', partialResponse, true, interactionCount); } } } diff --git a/services/recording-service.js b/services/recording-service.js index 9900abbb..9e49c2cf 100644 --- a/services/recording-service.js +++ b/services/recording-service.js @@ -1,12 +1,12 @@ require('colors'); -async function recordingService(ttsService, callSid) { +async function recordingService(textService, callSid) { try { if (process.env.RECORDING_ENABLED === 'true') { const client = require('twilio')(process.env.TWILIO_ACCOUNT_SID, process.env.TWILIO_AUTH_TOKEN); - ttsService.generate({partialResponseIndex: null, partialResponse: 'This call will be recorded.'}, 0); + textService.sendText({partialResponseIndex: null, partialResponse: 'This call will be recorded.'}, 0); const recording = await client.calls(callSid) .recordings .create({ diff --git a/services/stream-service.js b/services/stream-service.js deleted file mode 100644 index e8b844e5..00000000 --- a/services/stream-service.js +++ /dev/null @@ -1,60 +0,0 @@ -const EventEmitter = require('events'); -const uuid = require('uuid'); - -class StreamService extends EventEmitter { - constructor(websocket) { - super(); - this.ws = websocket; - this.expectedAudioIndex = 0; - this.audioBuffer = {}; - this.streamSid = ''; - } - - setStreamSid (streamSid) { - this.streamSid = streamSid; - } - - buffer (index, audio) { - // Escape hatch for intro message, which doesn't have an index - if(index === null) { - this.sendAudio(audio); - } else if(index === this.expectedAudioIndex) { - this.sendAudio(audio); - this.expectedAudioIndex++; - - while(Object.prototype.hasOwnProperty.call(this.audioBuffer, this.expectedAudioIndex)) { - const bufferedAudio = this.audioBuffer[this.expectedAudioIndex]; - this.sendAudio(bufferedAudio); - this.expectedAudioIndex++; - } - } else { - this.audioBuffer[index] = audio; - } - } - - sendAudio (audio) { - this.ws.send( - JSON.stringify({ - streamSid: this.streamSid, - event: 'media', - media: { - payload: audio, - }, - }) - ); - // When the media completes you will receive a `mark` message with the label - const markLabel = uuid.v4(); - this.ws.send( - JSON.stringify({ - streamSid: this.streamSid, - event: 'mark', - mark: { - name: markLabel - } - }) - ); - this.emit('audiosent', markLabel); - } -} - -module.exports = {StreamService}; \ No newline at end of file diff --git a/services/text-service.js b/services/text-service.js new file mode 100644 index 00000000..7dbca9f6 --- /dev/null +++ b/services/text-service.js @@ -0,0 +1,21 @@ +const EventEmitter = require('events'); + +class TextService extends EventEmitter { + constructor(websocket) { + super(); + this.ws = websocket; + } + + sendText (text, last) { + console.log('Sending text: ', text, last); + this.ws.send( + JSON.stringify({ + type: 'text', + token: text, + last: last, + }) + ); + } +} + +module.exports = {TextService}; \ No newline at end of file diff --git a/services/transcription-service.js b/services/transcription-service.js deleted file mode 100644 index 578fd80b..00000000 --- a/services/transcription-service.js +++ /dev/null @@ -1,94 +0,0 @@ -require('colors'); -const { createClient, LiveTranscriptionEvents } = require('@deepgram/sdk'); -const { Buffer } = require('node:buffer'); -const EventEmitter = require('events'); - - -class TranscriptionService extends EventEmitter { - constructor() { - super(); - const deepgram = createClient(process.env.DEEPGRAM_API_KEY); - this.dgConnection = deepgram.listen.live({ - encoding: 'mulaw', - sample_rate: '8000', - model: 'nova-2', - punctuate: true, - interim_results: true, - endpointing: 200, - utterance_end_ms: 1000 - }); - - this.finalResult = ''; - this.speechFinal = false; // used to determine if we have seen speech_final=true indicating that deepgram detected a natural pause in the speakers speech. - - this.dgConnection.on(LiveTranscriptionEvents.Open, () => { - this.dgConnection.on(LiveTranscriptionEvents.Transcript, (transcriptionEvent) => { - const alternatives = transcriptionEvent.channel?.alternatives; - let text = ''; - if (alternatives) { - text = alternatives[0]?.transcript; - } - - // if we receive an UtteranceEnd and speech_final has not already happened then we should consider this the end of of the human speech and emit the transcription - if (transcriptionEvent.type === 'UtteranceEnd') { - if (!this.speechFinal) { - console.log(`UtteranceEnd received before speechFinal, emit the text collected so far: ${this.finalResult}`.yellow); - this.emit('transcription', this.finalResult); - return; - } else { - console.log('STT -> Speech was already final when UtteranceEnd recevied'.yellow); - return; - } - } - - // console.log(text, "is_final: ", transcription?.is_final, "speech_final: ", transcription.speech_final); - // if is_final that means that this chunk of the transcription is accurate and we need to add it to the finalResult - if (transcriptionEvent.is_final === true && text.trim().length > 0) { - this.finalResult += ` ${text}`; - // if speech_final and is_final that means this text is accurate and it's a natural pause in the speakers speech. We need to send this to the assistant for processing - if (transcriptionEvent.speech_final === true) { - this.speechFinal = true; // this will prevent a utterance end which shows up after speechFinal from sending another response - this.emit('transcription', this.finalResult); - this.finalResult = ''; - } else { - // if we receive a message without speechFinal reset speechFinal to false, this will allow any subsequent utteranceEnd messages to properly indicate the end of a message - this.speechFinal = false; - } - } else { - this.emit('utterance', text); - } - }); - - this.dgConnection.on(LiveTranscriptionEvents.Error, (error) => { - console.error('STT -> deepgram error'); - console.error(error); - }); - - this.dgConnection.on(LiveTranscriptionEvents.Warning, (warning) => { - console.error('STT -> deepgram warning'); - console.error(warning); - }); - - this.dgConnection.on(LiveTranscriptionEvents.Metadata, (metadata) => { - console.error('STT -> deepgram metadata'); - console.error(metadata); - }); - - this.dgConnection.on(LiveTranscriptionEvents.Close, () => { - console.log('STT -> Deepgram connection closed'.yellow); - }); - }); - } - - /** - * Send the payload to Deepgram - * @param {String} payload A base64 MULAW/8000 audio stream - */ - send(payload) { - if (this.dgConnection.getReadyState() === 1) { - this.dgConnection.send(Buffer.from(payload, 'base64')); - } - } -} - -module.exports = { TranscriptionService }; \ No newline at end of file diff --git a/services/tts-service.js b/services/tts-service.js deleted file mode 100644 index 7ca1aed6..00000000 --- a/services/tts-service.js +++ /dev/null @@ -1,53 +0,0 @@ -require('dotenv').config(); -const { Buffer } = require('node:buffer'); -const EventEmitter = require('events'); -const fetch = require('node-fetch'); - -class TextToSpeechService extends EventEmitter { - constructor() { - super(); - this.nextExpectedIndex = 0; - this.speechBuffer = {}; - } - - async generate(gptReply, interactionCount) { - const { partialResponseIndex, partialResponse } = gptReply; - - if (!partialResponse) { return; } - - try { - const response = await fetch( - `https://api.deepgram.com/v1/speak?model=${process.env.VOICE_MODEL}&encoding=mulaw&sample_rate=8000&container=none`, - { - method: 'POST', - headers: { - 'Authorization': `Token ${process.env.DEEPGRAM_API_KEY}`, - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ - text: partialResponse, - }), - } - ); - - if (response.status === 200) { - try { - const blob = await response.blob(); - const audioArrayBuffer = await blob.arrayBuffer(); - const base64String = Buffer.from(audioArrayBuffer).toString('base64'); - this.emit('speech', partialResponseIndex, base64String, partialResponse, interactionCount); - } catch (err) { - console.log(err); - } - } else { - console.log('Deepgram TTS error:'); - console.log(response); - } - } catch (err) { - console.error('Error occurred in TextToSpeech service'); - console.error(err); - } - } -} - -module.exports = { TextToSpeechService }; \ No newline at end of file From 1f37424f7552e8164158998845a953067ac5b213 Mon Sep 17 00:00:00 2001 From: Charlie Weems Date: Fri, 2 Aug 2024 13:11:20 -0700 Subject: [PATCH 02/26] Add voice SDK for testing --- app.js | 21 ++- config.js | 28 ++++ name_generator.js | 90 +++++++++++ public/index.html | 62 ++++++++ public/quickstart.js | 310 ++++++++++++++++++++++++++++++++++++ public/site.css | 124 +++++++++++++++ public/twilio.min.js | 1 + services/token-generator.js | 73 +++++++++ 8 files changed, 707 insertions(+), 2 deletions(-) create mode 100644 config.js create mode 100644 name_generator.js create mode 100644 public/index.html create mode 100644 public/quickstart.js create mode 100644 public/site.css create mode 100644 public/twilio.min.js create mode 100644 services/token-generator.js diff --git a/app.js b/app.js index 094c769a..cca3113b 100644 --- a/app.js +++ b/app.js @@ -1,23 +1,40 @@ require('dotenv').config(); require('colors'); +const path = require('path'); + const express = require('express'); const ExpressWs = require('express-ws'); const { GptService } = require('./services/gpt-service'); const { TextService } = require('./services/text-service'); const { recordingService } = require('./services/recording-service'); +const { tokenGenerator } = require('./services/token-generator'); const app = express(); ExpressWs(app); const PORT = process.env.PORT || 3000; +app.use(express.static('public')); + + +app.get('/index.html', (req, res) => { + res.sendFile(path.join(__dirname, './index.html')); +}); + +app.get('/token', (req, res) => { + res.send(tokenGenerator()); +}); app.post('/incoming', (req, res) => { try { const response = ` - + `; res.type('text/xml'); @@ -59,7 +76,7 @@ app.ws('/sockets', (ws) => { console.log('Todo: add interruption handling'); } }); - + gptService.on('gptreply', async (gptReply, final, icount) => { console.log(`Interaction ${icount}: GPT -> TTS: ${gptReply.partialResponse}`.green ); textService.sendText(gptReply, final); diff --git a/config.js b/config.js new file mode 100644 index 00000000..7e10a985 --- /dev/null +++ b/config.js @@ -0,0 +1,28 @@ +const dotenv = require("dotenv"); +const cfg = {}; + +if (process.env.NODE_ENV !== "test") { + dotenv.config({ path: ".env" }); +} else { + dotenv.config({ path: ".env.example", silent: true }); +} + +// HTTP Port to run our web application +cfg.port = process.env.PORT || 3000; + +// Your Twilio account SID and auth token, both found at: +// https://www.twilio.com/user/account +// +// A good practice is to store these string values as system environment +// variables, and load them from there as we are doing below. Alternately, +// you could hard code these values here as strings. +cfg.accountSid = process.env.TWILIO_ACCOUNT_SID; + +cfg.twimlAppSid = process.env.TWILIO_TWIML_APP_SID; +cfg.callerId = process.env.TWILIO_CALLER_ID; + +cfg.apiKey = process.env.TWILIO_API_KEY; +cfg.apiSecret = process.env.TWILIO_API_SECRET; + +// Export configuration object +module.exports = cfg; diff --git a/name_generator.js b/name_generator.js new file mode 100644 index 00000000..30682f16 --- /dev/null +++ b/name_generator.js @@ -0,0 +1,90 @@ +const ADJECTIVES = [ + "Awesome", + "Bold", + "Creative", + "Dapper", + "Eccentric", + "Fiesty", + "Golden", + "Holy", + "Ignominious", + "Jolly", + "Kindly", + "Lucky", + "Mushy", + "Natural", + "Oaken", + "Precise", + "Quiet", + "Rowdy", + "Sunny", + "Tall", + "Unique", + "Vivid", + "Wonderful", + "Xtra", + "Yawning", + "Zesty", +]; + +const FIRST_NAMES = [ + "Anna", + "Bobby", + "Cameron", + "Danny", + "Emmett", + "Frida", + "Gracie", + "Hannah", + "Isaac", + "Jenova", + "Kendra", + "Lando", + "Mufasa", + "Nate", + "Owen", + "Penny", + "Quincy", + "Roddy", + "Samantha", + "Tammy", + "Ulysses", + "Victoria", + "Wendy", + "Xander", + "Yolanda", + "Zelda", +]; + +const LAST_NAMES = [ + "Anchorage", + "Berlin", + "Cucamonga", + "Davenport", + "Essex", + "Fresno", + "Gunsight", + "Hanover", + "Indianapolis", + "Jamestown", + "Kane", + "Liberty", + "Minneapolis", + "Nevis", + "Oakland", + "Portland", + "Quantico", + "Raleigh", + "SaintPaul", + "Tulsa", + "Utica", + "Vail", + "Warsaw", + "XiaoJin", + "Yale", + "Zimmerman", +]; + +const rand = (arr) => arr[Math.floor(Math.random() * arr.length)]; + +module.exports = () => rand(ADJECTIVES) + rand(FIRST_NAMES) + rand(LAST_NAMES); diff --git a/public/index.html b/public/index.html new file mode 100644 index 00000000..291e198d --- /dev/null +++ b/public/index.html @@ -0,0 +1,62 @@ + + + + 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/public/quickstart.js b/public/quickstart.js new file mode 100644 index 00000000..2c0d7053 --- /dev/null +++ b/public/quickstart.js @@ -0,0 +1,310 @@ +$(function () { + 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 outgoingCallHangupButton = document.getElementById("button-hangup-outgoing"); + 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"); + + let device; + let token; + + // Event Listeners + + callButton.onclick = (e) => { + e.preventDefault(); + makeOutgoingCall(); + }; + getAudioDevicesButton.onclick = getAudioDevices; + speakerDevices.addEventListener("change", updateOutputDevice); + ringtoneDevices.addEventListener("change", updateRingtoneDevice); + + + // 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); + + // 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."); + } + } + + // SETUP STEP 3: + // Instantiate a new Twilio.Device + function intitializeDevice() { + logDiv.classList.remove("hide"); + log("Initializing device"); + device = new Twilio.Device(token, { + logLevel:1, + // Set Opus as our preferred codec. Opus generally performs better, requiring less bandwidth and + // providing better audio quality in restrained network conditions. + chunderw: "voice-js.ashburn.stage.twilio.com", + codecPreferences: ["opus", "pcmu"], + }); + + 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"); + }); + + 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 = { + // get the phone number to call from the DOM + To: phoneNumberInput.value, + }; + + if (device) { + log(`Attempting to call ${params.To} ...`); + + // 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.on("accept", updateUIAcceptedOutgoingCall); + call.on("disconnect", updateUIDisconnectedOutgoingCall); + call.on("cancel", updateUIDisconnectedOutgoingCall); + + outgoingCallHangupButton.onclick = () => { + log("Hanging up ..."); + call.disconnect(); + }; + + } else { + log("Unable to make call."); + } + } + + function updateUIAcceptedOutgoingCall(call) { + log("Call in progress ..."); + callButton.disabled = true; + outgoingCallHangupButton.classList.remove("hide"); + volumeIndicators.classList.remove("hide"); + bindVolumeIndicators(call); + } + + function updateUIDisconnectedOutgoingCall() { + log("Call disconnected."); + callButton.disabled = false; + outgoingCallHangupButton.classList.add("hide"); + volumeIndicators.classList.add("hide"); + } + + // HANDLE INCOMING CALL + + 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.on("cancel", handleDisconnectedIncomingCall); + call.on("disconnect", handleDisconnectedIncomingCall); + call.on("reject", 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(call) { + call.disconnect(); + log("Hanging up incoming call"); + resetIncomingCallUI(); + } + + // HANDLE CANCELLED INCOMING CALL + + function handleDisconnectedIncomingCall() { + 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}`; + + var input = document.getElementById("phone-number"); + input.value = clientName; + } + + function resetIncomingCallUI() { + incomingPhoneNumberEl.innerHTML = ""; + incomingCallAcceptButton.classList.remove("hide"); + incomingCallRejectButton.classList.remove("hide"); + incomingCallHangupButton.classList.add("hide"); + 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); + } + + 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(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.background = inputColor; + + 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.background = outputColor; + }); + } + + // 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; + } + }); + + var option = document.createElement("option"); + option.label = device.label; + option.setAttribute("data-id", id); + if (isActive) { + option.setAttribute("selected", "selected"); + } + selectEl.appendChild(option); + }); + } +}); diff --git a/public/site.css b/public/site.css new file mode 100644 index 00000000..4d9341f8 --- /dev/null +++ b/public/site.css @@ -0,0 +1,124 @@ +@import url(https://fonts.googleapis.com/css?family=Share+Tech+Mono); + +body, +p { + padding: 0; + margin: auto; + font-family: Arial, Helvetica, sans-serif; +} + +h1 { + text-align: center; +} + +h2 { + margin-top: 0; + border-bottom: 1px solid black; +} + +button { + margin-bottom: 10px; +} + +label { + text-align: left; + font-size: 1.25em; + color: #777776; + display: block; +} + +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; +} + +#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/public/twilio.min.js b/public/twilio.min.js new file mode 100644 index 00000000..73b15721 --- /dev/null +++ b/public/twilio.min.js @@ -0,0 +1 @@ +(function(root){var bundle=function(){function r(e,n,t){function o(i,f){if(!n[i]){if(!e[i]){var c="function"==typeof require&&require;if(!f&&c)return c(i,!0);if(u)return u(i,!0);var a=new Error("Cannot find module '"+i+"'");throw a.code="MODULE_NOT_FOUND",a}var p=n[i]={exports:{}};e[i][0].call(p.exports,function(r){var n=e[i][1][r];return o(n||r)},p,p.exports,r,e,n,t)}return n[i].exports}for(var u="function"==typeof require&&require,i=0;i0&&t[t.length-1])&&(op[0]===6||op[0]===2)){_=0;continue}if(op[0]===3&&(!t||op[1]>t[0]&&op[1]BACKOFF_CONFIG.maxDelay){_this._log.info("Exceeded max ICE retries");return _this._mediaHandler.onerror(MEDIA_DISCONNECT_ERROR)}try{_this._mediaReconnectBackoff.backoff()}catch(error){if(!(error.message&&error.message==="Backoff in progress.")){throw error}}}return}var pc=_this._mediaHandler.version.pc;var isIceDisconnected=pc&&pc.iceConnectionState==="disconnected";var hasLowBytesWarning=_this._monitor.hasActiveWarning("bytesSent","min")||_this._monitor.hasActiveWarning("bytesReceived","min");if(type===LowBytes&&isIceDisconnected||type===ConnectionDisconnected&&hasLowBytesWarning||isEndOfIceCycle){var mediaReconnectionError=new errors_1.MediaErrors.ConnectionError("Media connection failed.");_this._log.warn("ICE Connection disconnected.");_this._publisher.warn("connection","error",mediaReconnectionError,_this);_this._publisher.info("connection","reconnecting",null,_this);_this._mediaReconnectStartTime=Date.now();_this._status=Call.State.Reconnecting;_this._mediaStatus=Call.State.Reconnecting;_this._mediaReconnectBackoff.reset();_this._mediaReconnectBackoff.backoff();_this.emit("reconnecting",mediaReconnectionError)}};_this._onMediaReconnected=function(){if(_this._mediaStatus!==Call.State.Reconnecting){return}_this._log.info("ICE Connection reestablished.");_this._mediaStatus=Call.State.Open;if(_this._signalingStatus===Call.State.Open){_this._publisher.info("connection","reconnected",null,_this);_this.emit("reconnected");_this._status=Call.State.Open}};_this._onMessageReceived=function(payload){var callsid=payload.callsid,content=payload.content,contenttype=payload.contenttype,messagetype=payload.messagetype,voiceeventsid=payload.voiceeventsid;if(_this.parameters.CallSid!==callsid){_this._log.warn("Received a message from a different callsid: "+callsid);return}_this.emit("messageReceived",{content:content,contentType:contenttype,messageType:messagetype,voiceEventSid:voiceeventsid})};_this._onMessageSent=function(voiceEventSid){if(!_this._messages.has(voiceEventSid)){_this._log.warn("Received a messageSent with a voiceEventSid that doesn't exists: "+voiceEventSid);return}var message=_this._messages.get(voiceEventSid);_this._messages.delete(voiceEventSid);_this.emit("messageSent",message)};_this._onRinging=function(payload){_this._setCallSid(payload);if(_this._status!==Call.State.Connecting&&_this._status!==Call.State.Ringing){return}var hasEarlyMedia=!!payload.sdp;_this._status=Call.State.Ringing;_this._publisher.info("connection","outgoing-ringing",{hasEarlyMedia:hasEarlyMedia},_this);_this.emit("ringing",hasEarlyMedia)};_this._onRTCSample=function(sample){var callMetrics=__assign(__assign({},sample),{inputVolume:_this._latestInputVolume,outputVolume:_this._latestOutputVolume});_this._codec=callMetrics.codecName;_this._metricsSamples.push(callMetrics);if(_this._metricsSamples.length>=METRICS_BATCH_SIZE){_this._publishMetrics()}_this.emit("sample",sample)};_this._onSignalingError=function(payload){var callsid=payload.callsid,voiceeventsid=payload.voiceeventsid;if(_this.parameters.CallSid!==callsid){_this._log.warn("Received an error from a different callsid: "+callsid);return}if(voiceeventsid&&_this._messages.has(voiceeventsid)){_this._messages.delete(voiceeventsid);_this._log.warn("Received an error while sending a message.",payload)}};_this._onSignalingReconnected=function(){if(_this._signalingStatus!==Call.State.Reconnecting){return}_this._log.info("Signaling Connection reestablished.");_this._signalingStatus=Call.State.Open;if(_this._mediaStatus===Call.State.Open){_this._publisher.info("connection","reconnected",null,_this);_this.emit("reconnected");_this._status=Call.State.Open}};_this._onTransportClose=function(){_this._log.error("Received transportClose from pstream");_this.emit("transportClose");if(_this._signalingReconnectToken){_this._status=Call.State.Reconnecting;_this._signalingStatus=Call.State.Reconnecting;_this.emit("reconnecting",new errors_1.SignalingErrors.ConnectionDisconnected)}else{_this._status=Call.State.Closed;_this._signalingStatus=Call.State.Closed}};_this._reemitWarning=function(warningData,wasCleared){var groupPrefix=/^audio/.test(warningData.name)?"audio-level-":"network-quality-";var warningPrefix=WARNING_PREFIXES[warningData.threshold.name];var warningName;if(warningData.name in MULTIPLE_THRESHOLD_WARNING_NAMES){warningName=MULTIPLE_THRESHOLD_WARNING_NAMES[warningData.name][warningData.threshold.name]}else if(warningData.name in WARNING_NAMES){warningName=WARNING_NAMES[warningData.name]}var warning=warningPrefix+warningName;_this._emitWarning(groupPrefix,warning,warningData.threshold.value,warningData.values||warningData.value,wasCleared,warningData)};_this._reemitWarningCleared=function(warningData){_this._reemitWarning(warningData,true)};_this._isUnifiedPlanDefault=config.isUnifiedPlanDefault;_this._soundcache=config.soundcache;if(typeof config.onIgnore==="function"){_this._onIgnore=config.onIgnore}var message=options&&options.twimlParams||{};_this.customParameters=new Map(Object.entries(message).map(function(_a){var key=_a[0],val=_a[1];return[key,String(val)]}));Object.assign(_this._options,options);if(_this._options.callParameters){_this.parameters=_this._options.callParameters}if(_this._options.reconnectToken){_this._signalingReconnectToken=_this._options.reconnectToken}_this._voiceEventSidGenerator=_this._options.voiceEventSidGenerator||uuid_1.generateVoiceEventSid;_this._direction=_this.parameters.CallSid?Call.CallDirection.Incoming:Call.CallDirection.Outgoing;if(_this._direction===Call.CallDirection.Incoming&&_this.parameters){_this.callerInfo=_this.parameters.StirStatus?{isVerified:_this.parameters.StirStatus==="TN-Validation-Passed-A"}:null}else{_this.callerInfo=null}_this._mediaReconnectBackoff=Backoff.exponential(BACKOFF_CONFIG);_this._mediaReconnectBackoff.on("ready",function(){return _this._mediaHandler.iceRestart()});_this.outboundConnectionId=generateTempCallSid();var publisher=_this._publisher=config.publisher;if(_this._direction===Call.CallDirection.Incoming){publisher.info("connection","incoming",null,_this)}else{publisher.info("connection","outgoing",{preflight:_this._options.preflight},_this)}var monitor=_this._monitor=new(_this._options.StatsMonitor||statsMonitor_1.default);monitor.on("sample",_this._onRTCSample);monitor.disableWarnings();setTimeout(function(){return monitor.enableWarnings()},METRICS_DELAY);monitor.on("warning",function(data,wasCleared){if(data.name==="bytesSent"||data.name==="bytesReceived"){_this._onMediaFailure(Call.MediaFailure.LowBytes)}_this._reemitWarning(data,wasCleared)});monitor.on("warning-cleared",function(data){_this._reemitWarningCleared(data)});_this._mediaHandler=new _this._options.MediaHandler(config.audioHelper,config.pstream,config.getUserMedia,{codecPreferences:_this._options.codecPreferences,dscp:_this._options.dscp,forceAggressiveIceNomination:_this._options.forceAggressiveIceNomination,isUnifiedPlan:_this._isUnifiedPlanDefault,maxAverageBitrate:_this._options.maxAverageBitrate,preflight:_this._options.preflight});_this.on("volume",function(inputVolume,outputVolume){_this._inputVolumeStreak=_this._checkVolume(inputVolume,_this._inputVolumeStreak,_this._latestInputVolume,"input");_this._outputVolumeStreak=_this._checkVolume(outputVolume,_this._outputVolumeStreak,_this._latestOutputVolume,"output");_this._latestInputVolume=inputVolume;_this._latestOutputVolume=outputVolume});_this._mediaHandler.onvolume=function(inputVolume,outputVolume,internalInputVolume,internalOutputVolume){monitor.addVolumes(internalInputVolume/255*32767,internalOutputVolume/255*32767);_this.emit("volume",inputVolume,outputVolume)};_this._mediaHandler.ondtlstransportstatechange=function(state){var level=state==="failed"?"error":"debug";_this._publisher.post(level,"dtls-transport-state",state,null,_this)};_this._mediaHandler.onpcconnectionstatechange=function(state){var level="debug";var dtlsTransport=_this._mediaHandler.getRTCDtlsTransport();if(state==="failed"){level=dtlsTransport&&dtlsTransport.state==="failed"?"error":"warning"}_this._publisher.post(level,"pc-connection-state",state,null,_this)};_this._mediaHandler.onicecandidate=function(candidate){var payload=new icecandidate_1.IceCandidate(candidate).toPayload();_this._publisher.debug("ice-candidate","ice-candidate",payload,_this)};_this._mediaHandler.onselectedcandidatepairchange=function(pair){var localCandidatePayload=new icecandidate_1.IceCandidate(pair.local).toPayload();var remoteCandidatePayload=new icecandidate_1.IceCandidate(pair.remote,true).toPayload();_this._publisher.debug("ice-candidate","selected-ice-candidate-pair",{local_candidate:localCandidatePayload,remote_candidate:remoteCandidatePayload},_this)};_this._mediaHandler.oniceconnectionstatechange=function(state){var level=state==="failed"?"error":"debug";_this._publisher.post(level,"ice-connection-state",state,null,_this)};_this._mediaHandler.onicegatheringfailure=function(type){_this._publisher.warn("ice-gathering-state",type,null,_this);_this._onMediaFailure(Call.MediaFailure.IceGatheringFailed)};_this._mediaHandler.onicegatheringstatechange=function(state){_this._publisher.debug("ice-gathering-state",state,null,_this)};_this._mediaHandler.onsignalingstatechange=function(state){_this._publisher.debug("signaling-state",state,null,_this)};_this._mediaHandler.ondisconnected=function(msg){_this._log.info(msg);_this._publisher.warn("network-quality-warning-raised","ice-connectivity-lost",{message:msg},_this);_this.emit("warning","ice-connectivity-lost");_this._onMediaFailure(Call.MediaFailure.ConnectionDisconnected)};_this._mediaHandler.onfailed=function(msg){_this._onMediaFailure(Call.MediaFailure.ConnectionFailed)};_this._mediaHandler.onconnected=function(){if(_this._status===Call.State.Reconnecting){_this._onMediaReconnected()}};_this._mediaHandler.onreconnected=function(msg){_this._log.info(msg);_this._publisher.info("network-quality-warning-cleared","ice-connectivity-lost",{message:msg},_this);_this.emit("warning-cleared","ice-connectivity-lost");_this._onMediaReconnected()};_this._mediaHandler.onerror=function(e){if(e.disconnect===true){_this._disconnect(e.info&&e.info.message)}var error=e.info.twilioError||new errors_1.GeneralErrors.UnknownError(e.info.message);_this._log.error("Received an error from MediaStream:",e);_this.emit("error",error)};_this._mediaHandler.onopen=function(){if(_this._status===Call.State.Open||_this._status===Call.State.Reconnecting){return}else if(_this._status===Call.State.Ringing||_this._status===Call.State.Connecting){_this.mute(false);_this._mediaStatus=Call.State.Open;_this._maybeTransitionToOpen()}else{_this._mediaHandler.close()}};_this._mediaHandler.onclose=function(){_this._status=Call.State.Closed;if(_this._options.shouldPlayDisconnect&&_this._options.shouldPlayDisconnect()&&!_this._isCancelled){_this._soundcache.get(device_1.default.SoundName.Disconnect).play()}monitor.disable();_this._publishMetrics();if(!_this._isCancelled){_this.emit("disconnect",_this)}};_this._pstream=config.pstream;_this._pstream.on("ack",_this._onAck);_this._pstream.on("cancel",_this._onCancel);_this._pstream.on("error",_this._onSignalingError);_this._pstream.on("ringing",_this._onRinging);_this._pstream.on("transportClose",_this._onTransportClose);_this._pstream.on("connected",_this._onConnected);_this._pstream.on("message",_this._onMessageReceived);_this.on("error",function(error){_this._publisher.error("connection","error",{code:error.code,message:error.message},_this);if(_this._pstream&&_this._pstream.status==="disconnected"){_this._cleanupEventListeners()}});_this.on("disconnect",function(){_this._cleanupEventListeners()});return _this}Object.defineProperty(Call.prototype,"direction",{get:function(){return this._direction},enumerable:true,configurable:true});Object.defineProperty(Call.prototype,"codec",{get:function(){return this._codec},enumerable:true,configurable:true});Call.prototype._setInputTracksFromStream=function(stream){return this._mediaHandler.setInputTracksFromStream(stream)};Call.prototype._setSinkIds=function(sinkIds){return this._mediaHandler._setSinkIds(sinkIds)};Call.prototype.accept=function(options){var _this=this;if(this._status!==Call.State.Pending){return}options=options||{};var rtcConfiguration=options.rtcConfiguration||this._options.rtcConfiguration;var rtcConstraints=options.rtcConstraints||this._options.rtcConstraints||{};var audioConstraints=rtcConstraints.audio||{audio:true};this._status=Call.State.Connecting;var connect=function(){if(_this._status!==Call.State.Connecting){_this._cleanupEventListeners();_this._mediaHandler.close();return}var onAnswer=function(pc,reconnectToken){var eventName=_this._direction===Call.CallDirection.Incoming?"accepted-by-local":"accepted-by-remote";_this._publisher.info("connection",eventName,null,_this);if(typeof reconnectToken==="string"){_this._signalingReconnectToken=reconnectToken}var _a=getPreferredCodecInfo(_this._mediaHandler.version.getSDP()),codecName=_a.codecName,codecParams=_a.codecParams;_this._publisher.info("settings","codec",{codec_params:codecParams,selected_codec:codecName},_this);_this._monitor.enable(pc)};var sinkIds=typeof _this._options.getSinkIds==="function"&&_this._options.getSinkIds();if(Array.isArray(sinkIds)){_this._mediaHandler._setSinkIds(sinkIds).catch(function(){})}_this._pstream.addListener("hangup",_this._onHangup);if(_this._direction===Call.CallDirection.Incoming){_this._isAnswered=true;_this._pstream.on("answer",_this._onAnswer.bind(_this));_this._mediaHandler.answerIncomingCall(_this.parameters.CallSid,_this._options.offerSdp,rtcConstraints,rtcConfiguration,onAnswer)}else{var params=Array.from(_this.customParameters.entries()).map(function(pair){return encodeURIComponent(pair[0])+"="+encodeURIComponent(pair[1])}).join("&");_this._pstream.on("answer",_this._onAnswer.bind(_this));_this._mediaHandler.makeOutgoingCall(_this._pstream.token,params,_this.outboundConnectionId,rtcConstraints,rtcConfiguration,onAnswer)}};if(this._options.beforeAccept){this._options.beforeAccept(this)}var inputStream=typeof this._options.getInputStream==="function"&&this._options.getInputStream();var promise=inputStream?this._mediaHandler.setInputTracksFromStream(inputStream):this._mediaHandler.openWithConstraints(audioConstraints);promise.then(function(){_this._publisher.info("get-user-media","succeeded",{data:{audioConstraints:audioConstraints}},_this);connect()},function(error){var twilioError;if(error.code===31208||["PermissionDeniedError","NotAllowedError"].indexOf(error.name)!==-1){twilioError=new errors_1.UserMediaErrors.PermissionDeniedError;_this._publisher.error("get-user-media","denied",{data:{audioConstraints:audioConstraints,error:error}},_this)}else{twilioError=new errors_1.UserMediaErrors.AcquisitionFailedError;_this._publisher.error("get-user-media","failed",{data:{audioConstraints:audioConstraints,error:error}},_this)}_this._disconnect();_this.emit("error",twilioError)})};Call.prototype.disconnect=function(){this._disconnect()};Call.prototype.getLocalStream=function(){return this._mediaHandler&&this._mediaHandler.stream};Call.prototype.getRemoteStream=function(){return this._mediaHandler&&this._mediaHandler._remoteStream};Call.prototype.ignore=function(){if(this._status!==Call.State.Pending){return}this._status=Call.State.Closed;this._mediaHandler.ignore(this.parameters.CallSid);this._publisher.info("connection","ignored-by-local",null,this);if(this._onIgnore){this._onIgnore()}};Call.prototype.isMuted=function(){return this._mediaHandler.isMuted};Call.prototype.mute=function(shouldMute){if(shouldMute===void 0){shouldMute=true}var wasMuted=this._mediaHandler.isMuted;this._mediaHandler.mute(shouldMute);var isMuted=this._mediaHandler.isMuted;if(wasMuted!==isMuted){this._publisher.info("connection",isMuted?"muted":"unmuted",null,this);this.emit("mute",isMuted,this)}};Call.prototype.postFeedback=function(score,issue){if(typeof score==="undefined"||score===null){return this._postFeedbackDeclined()}if(!Object.values(Call.FeedbackScore).includes(score)){throw new errors_1.InvalidArgumentError("Feedback score must be one of: "+Object.values(Call.FeedbackScore))}if(typeof issue!=="undefined"&&issue!==null&&!Object.values(Call.FeedbackIssue).includes(issue)){throw new errors_1.InvalidArgumentError("Feedback issue must be one of: "+Object.values(Call.FeedbackIssue))}return this._publisher.info("feedback","received",{issue_name:issue,quality_score:score},this,true)};Call.prototype.reject=function(){if(this._status!==Call.State.Pending){return}this._pstream.reject(this.parameters.CallSid);this._status=Call.State.Closed;this.emit("reject");this._mediaHandler.reject(this.parameters.CallSid);this._publisher.info("connection","rejected-by-local",null,this)};Call.prototype.sendDigits=function(digits){if(digits.match(/[^0-9*#w]/)){throw new errors_1.InvalidArgumentError("Illegal character passed into sendDigits")}var sequence=[];digits.split("").forEach(function(digit){var dtmf=digit!=="w"?"dtmf"+digit:"";if(dtmf==="dtmf*"){dtmf="dtmfs"}if(dtmf==="dtmf#"){dtmf="dtmfh"}sequence.push(dtmf)});(function playNextDigit(soundCache,dialtonePlayer){var digit=sequence.shift();if(digit){if(dialtonePlayer){dialtonePlayer.play(digit)}else{soundCache.get(digit).play()}}if(sequence.length){setTimeout(playNextDigit.bind(null,soundCache),200)}})(this._soundcache,this._options.dialtonePlayer);var dtmfSender=this._mediaHandler.getOrCreateDTMFSender();function insertDTMF(dtmfs){if(!dtmfs.length){return}var dtmf=dtmfs.shift();if(dtmf&&dtmf.length){dtmfSender.insertDTMF(dtmf,DTMF_TONE_DURATION,DTMF_INTER_TONE_GAP)}setTimeout(insertDTMF.bind(null,dtmfs),DTMF_PAUSE_DURATION)}if(dtmfSender){if(!("canInsertDTMF"in dtmfSender)||dtmfSender.canInsertDTMF){this._log.info("Sending digits using RTCDTMFSender");insertDTMF(digits.split("w"));return}this._log.info("RTCDTMFSender cannot insert DTMF")}this._log.info("Sending digits over PStream");if(this._pstream!==null&&this._pstream.status!=="disconnected"){this._pstream.dtmf(this.parameters.CallSid,digits)}else{var error=new errors_1.GeneralErrors.ConnectionError("Could not send DTMF: Signaling channel is disconnected");this.emit("error",error)}};Call.prototype.sendMessage=function(message){var content=message.content,contentType=message.contentType,messageType=message.messageType;if(typeof content==="undefined"||content===null){throw new errors_1.InvalidArgumentError("`content` is empty")}if(typeof messageType!=="string"){throw new errors_1.InvalidArgumentError("`messageType` must be an enumeration value of `Call.MessageType` or "+"a string.")}if(messageType.length===0){throw new errors_1.InvalidArgumentError("`messageType` must be a non-empty string.")}if(this._pstream===null){throw new errors_1.InvalidStateError("Could not send CallMessage; Signaling channel is disconnected")}var callSid=this.parameters.CallSid;if(typeof this.parameters.CallSid==="undefined"){throw new errors_1.InvalidStateError("Could not send CallMessage; Call has no CallSid")}var voiceEventSid=this._voiceEventSidGenerator();this._messages.set(voiceEventSid,{content:content,contentType:contentType,messageType:messageType,voiceEventSid:voiceEventSid});this._pstream.sendMessage(callSid,content,contentType,messageType,voiceEventSid);return voiceEventSid};Call.prototype.status=function(){return this._status};Call.prototype._checkVolume=function(currentVolume,currentStreak,lastValue,direction){var wasWarningRaised=currentStreak>=10;var newStreak=0;if(lastValue===currentVolume){newStreak=currentStreak}if(newStreak>=10){this._emitWarning("audio-level-","constant-audio-"+direction+"-level",10,newStreak,false)}else if(wasWarningRaised){this._emitWarning("audio-level-","constant-audio-"+direction+"-level",10,newStreak,true)}return newStreak};Call.prototype._cleanupEventListeners=function(){var _this=this;var cleanup=function(){if(!_this._pstream){return}_this._pstream.removeListener("ack",_this._onAck);_this._pstream.removeListener("answer",_this._onAnswer);_this._pstream.removeListener("cancel",_this._onCancel);_this._pstream.removeListener("error",_this._onSignalingError);_this._pstream.removeListener("hangup",_this._onHangup);_this._pstream.removeListener("ringing",_this._onRinging);_this._pstream.removeListener("transportClose",_this._onTransportClose);_this._pstream.removeListener("connected",_this._onConnected);_this._pstream.removeListener("message",_this._onMessageReceived)};cleanup();setTimeout(cleanup,0)};Call.prototype._createMetricPayload=function(){var payload={call_sid:this.parameters.CallSid,dscp:!!this._options.dscp,sdk_version:C.RELEASE_VERSION,selected_region:this._options.selectedRegion};if(this._options.gateway){payload.gateway=this._options.gateway}if(this._options.region){payload.region=this._options.region}payload.direction=this._direction;return payload};Call.prototype._disconnect=function(message,wasRemote){message=typeof message==="string"?message:null;if(this._status!==Call.State.Open&&this._status!==Call.State.Connecting&&this._status!==Call.State.Reconnecting&&this._status!==Call.State.Ringing){return}this._log.info("Disconnecting...");if(this._pstream!==null&&this._pstream.status!=="disconnected"&&this._shouldSendHangup){var callsid=this.parameters.CallSid||this.outboundConnectionId;if(callsid){this._pstream.hangup(callsid,message)}}this._cleanupEventListeners();this._mediaHandler.close();if(!wasRemote){this._publisher.info("connection","disconnected-by-local",null,this)}};Call.prototype._maybeTransitionToOpen=function(){var wasConnected=this._wasConnected;if(this._isAnswered){this._onSignalingReconnected();this._signalingStatus=Call.State.Open;if(this._mediaHandler&&this._mediaHandler.status==="open"){this._status=Call.State.Open;if(!this._wasConnected){this._wasConnected=true;this.emit("accept",this)}}}};Call.prototype._postFeedbackDeclined=function(){return this._publisher.info("feedback","received-none",null,this,true)};Call.prototype._publishMetrics=function(){var _this=this;if(this._metricsSamples.length===0){return}this._publisher.postMetrics("quality-metrics-samples","metrics-sample",this._metricsSamples.splice(0),this._createMetricPayload(),this).catch(function(e){_this._log.warn("Unable to post metrics to Insights. Received error:",e)})};Call.prototype._setCallSid=function(payload){var callSid=payload.callsid;if(!callSid){return}this.parameters.CallSid=callSid;this._mediaHandler.callSid=callSid};Call.toString=function(){return"[Twilio.Call class]"};return Call}(events_1.EventEmitter);(function(Call){var State;(function(State){State["Closed"]="closed";State["Connecting"]="connecting";State["Open"]="open";State["Pending"]="pending";State["Reconnecting"]="reconnecting";State["Ringing"]="ringing"})(State=Call.State||(Call.State={}));var FeedbackIssue;(function(FeedbackIssue){FeedbackIssue["AudioLatency"]="audio-latency";FeedbackIssue["ChoppyAudio"]="choppy-audio";FeedbackIssue["DroppedCall"]="dropped-call";FeedbackIssue["Echo"]="echo";FeedbackIssue["NoisyCall"]="noisy-call";FeedbackIssue["OneWayAudio"]="one-way-audio"})(FeedbackIssue=Call.FeedbackIssue||(Call.FeedbackIssue={}));var FeedbackScore;(function(FeedbackScore){FeedbackScore[FeedbackScore["One"]=1]="One";FeedbackScore[FeedbackScore["Two"]=2]="Two";FeedbackScore[FeedbackScore["Three"]=3]="Three";FeedbackScore[FeedbackScore["Four"]=4]="Four";FeedbackScore[FeedbackScore["Five"]=5]="Five"})(FeedbackScore=Call.FeedbackScore||(Call.FeedbackScore={}));var CallDirection;(function(CallDirection){CallDirection["Incoming"]="INCOMING";CallDirection["Outgoing"]="OUTGOING"})(CallDirection=Call.CallDirection||(Call.CallDirection={}));var Codec;(function(Codec){Codec["Opus"]="opus";Codec["PCMU"]="pcmu"})(Codec=Call.Codec||(Call.Codec={}));var IceGatheringFailureReason;(function(IceGatheringFailureReason){IceGatheringFailureReason["None"]="none";IceGatheringFailureReason["Timeout"]="timeout"})(IceGatheringFailureReason=Call.IceGatheringFailureReason||(Call.IceGatheringFailureReason={}));var MediaFailure;(function(MediaFailure){MediaFailure["ConnectionDisconnected"]="ConnectionDisconnected";MediaFailure["ConnectionFailed"]="ConnectionFailed";MediaFailure["IceGatheringFailed"]="IceGatheringFailed";MediaFailure["LowBytes"]="LowBytes"})(MediaFailure=Call.MediaFailure||(Call.MediaFailure={}));var MessageType;(function(MessageType){MessageType["UserDefinedMessage"]="user-defined-message"})(MessageType=Call.MessageType||(Call.MessageType={}))})(Call||(Call={}));function generateTempCallSid(){return"TJSxxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g,function(c){var r=Math.random()*16|0;var v=c==="x"?r:r&3|8;return v.toString(16)})}exports.default=Call},{"./constants":7,"./device":9,"./errors":12,"./log":15,"./rtc":23,"./rtc/icecandidate":22,"./rtc/sdp":28,"./statsMonitor":34,"./util":35,"./uuid":36,backoff:45,events:53}],7:[function(require,module,exports){var PACKAGE_NAME="@twilio/voice-sdk";var RELEASE_VERSION="2.2.0";var SOUNDS_BASE_URL="https://sdk.twilio.com/js/client/sounds/releases/1.0.0";module.exports.COWBELL_AUDIO_URL=SOUNDS_BASE_URL+"/cowbell.mp3?cache="+RELEASE_VERSION;module.exports.ECHO_TEST_DURATION=2e4;module.exports.PACKAGE_NAME=PACKAGE_NAME;module.exports.RELEASE_VERSION=RELEASE_VERSION;module.exports.SOUNDS_BASE_URL=SOUNDS_BASE_URL;module.exports.USED_ERRORS=["AuthorizationErrors.AccessTokenExpired","AuthorizationErrors.AccessTokenInvalid","AuthorizationErrors.AuthenticationFailed","AuthorizationErrors.PayloadSizeExceededError","AuthorizationErrors.RateExceededError","ClientErrors.BadRequest","GeneralErrors.CallCancelledError","GeneralErrors.ConnectionError","GeneralErrors.TransportError","GeneralErrors.UnknownError","MalformedRequestErrors.MalformedRequestError","MediaErrors.ClientLocalDescFailed","MediaErrors.ClientRemoteDescFailed","MediaErrors.ConnectionError","SignalingErrors.ConnectionDisconnected","SignalingErrors.ConnectionError","UserMediaErrors.PermissionDeniedError","UserMediaErrors.AcquisitionFailedError"]},{}],8:[function(require,module,exports){"use strict";Object.defineProperty(exports,"__esModule",{value:true});var Deferred=function(){function Deferred(){var _this=this;this._promise=new Promise(function(resolve,reject){_this._resolve=resolve;_this._reject=reject})}Object.defineProperty(Deferred.prototype,"promise",{get:function(){return this._promise},enumerable:true,configurable:true});Deferred.prototype.reject=function(reason){this._reject(reason)};Deferred.prototype.resolve=function(value){this._resolve(value)};return Deferred}();exports.default=Deferred},{}],9:[function(require,module,exports){"use strict";var __extends=this&&this.__extends||function(){var extendStatics=function(d,b){extendStatics=Object.setPrototypeOf||{__proto__:[]}instanceof Array&&function(d,b){d.__proto__=b}||function(d,b){for(var p in b)if(b.hasOwnProperty(p))d[p]=b[p]};return extendStatics(d,b)};return function(d,b){extendStatics(d,b);function __(){this.constructor=d}d.prototype=b===null?Object.create(b):(__.prototype=b.prototype,new __)}}();var __assign=this&&this.__assign||function(){__assign=Object.assign||function(t){for(var s,i=1,n=arguments.length;i0&&t[t.length-1])&&(op[0]===6||op[0]===2)){_=0;continue}if(op[0]===3&&(!t||op[1]>t[0]&&op[1]0){var preferredURI=preferredURIs[0];_this._preferredURI=regions_1.createSignalingEndpointURL(preferredURI)}else{_this._log.info("Could not parse a preferred URI from the stream#connected event.")}if(_this._shouldReRegister){_this.register()}};_this._onSignalingError=function(payload){if(typeof payload!=="object"){return}var originalError=payload.error,callsid=payload.callsid;if(typeof originalError!=="object"){return}var call=typeof callsid==="string"&&_this._findCall(callsid)||undefined;var code=originalError.code,customMessage=originalError.message;var twilioError=originalError.twilioError;if(typeof code==="number"){if(code===31201){twilioError=new errors_1.AuthorizationErrors.AuthenticationFailed(originalError)}else if(code===31204){twilioError=new errors_1.AuthorizationErrors.AccessTokenInvalid(originalError)}else if(code===31205){_this._stopRegistrationTimer();twilioError=new errors_1.AuthorizationErrors.AccessTokenExpired(originalError)}else if(errors_1.hasErrorByCode(code)){twilioError=new(errors_1.getErrorByCode(code))(originalError)}}if(!twilioError){_this._log.error("Unknown signaling error: ",originalError);twilioError=new errors_1.GeneralErrors.UnknownError(customMessage,originalError)}_this._log.info("Received error: ",twilioError);_this.emit(Device.EventName.Error,twilioError,call)};_this._onSignalingInvite=function(payload){return __awaiter(_this,void 0,void 0,function(){var wasBusy,callParameters,customParameters,call,play;var _this=this;return __generator(this,function(_a){switch(_a.label){case 0:wasBusy=!!this._activeCall;if(wasBusy&&!this._options.allowIncomingWhileBusy){this._log.info("Device busy; ignoring incoming invite");return[2]}if(!payload.callsid||!payload.sdp){this.emit(Device.EventName.Error,new errors_1.ClientErrors.BadRequest("Malformed invite from gateway"));return[2]}callParameters=payload.parameters||{};callParameters.CallSid=callParameters.CallSid||payload.callsid;customParameters=Object.assign({},util_1.queryToJson(callParameters.Params));return[4,this._makeCall(customParameters,{callParameters:callParameters,offerSdp:payload.sdp,reconnectToken:payload.reconnect,voiceEventSidGenerator:this._options.voiceEventSidGenerator})];case 1:call=_a.sent();this._calls.push(call);call.once("accept",function(){_this._soundcache.get(Device.SoundName.Incoming).stop();_this._publishNetworkChange()});play=this._enabledSounds.incoming&&!wasBusy?function(){return _this._soundcache.get(Device.SoundName.Incoming).play()}:function(){return Promise.resolve()};this._showIncomingCall(call,play);return[2]}})})};_this._onSignalingOffline=function(){_this._log.info("Stream is offline");_this._edge=null;_this._region=null;_this._shouldReRegister=_this.state!==Device.State.Unregistered;_this._setState(Device.State.Unregistered)};_this._onSignalingReady=function(){_this._log.info("Stream is ready");_this._setState(Device.State.Registered)};_this._publishNetworkChange=function(){if(!_this._activeCall){return}if(_this._networkInformation){_this._publisher.info("network-information","network-change",{connection_type:_this._networkInformation.type,downlink:_this._networkInformation.downlink,downlinkMax:_this._networkInformation.downlinkMax,effective_type:_this._networkInformation.effectiveType,rtt:_this._networkInformation.rtt},_this._activeCall)}};_this._updateInputStream=function(inputStream){var call=_this._activeCall;if(call&&!inputStream){return Promise.reject(new errors_1.InvalidStateError("Cannot unset input device while a call is in progress."))}_this._callInputStream=inputStream;return call?call._setInputTracksFromStream(inputStream):Promise.resolve()};_this._updateSinkIds=function(type,sinkIds){var promise=type==="ringtone"?_this._updateRingtoneSinkIds(sinkIds):_this._updateSpeakerSinkIds(sinkIds);return promise.then(function(){_this._publisher.info("audio",type+"-devices-set",{audio_device_ids:sinkIds},_this._activeCall)},function(error){_this._publisher.error("audio",type+"-devices-set-failed",{audio_device_ids:sinkIds,message:error.message},_this._activeCall);throw error})};_this.updateToken(token);if(util_1.isLegacyEdge()){throw new errors_1.NotSupportedError("Microsoft Edge Legacy (https://support.microsoft.com/en-us/help/4533505/what-is-microsoft-edge-legacy) "+"is deprecated and will not be able to connect to Twilio to make or receive calls after September 1st, 2020. "+"Please see this documentation for a list of supported browsers "+"https://www.twilio.com/docs/voice/client/javascript#supported-browsers")}if(!Device.isSupported&&options.ignoreBrowserSupport){if(window&&window.location&&window.location.protocol==="http:"){throw new errors_1.NotSupportedError("twilio.js wasn't able to find WebRTC browser support. This is most likely because this page is served over http rather than https, which does not support WebRTC in many browsers. Please load this page over https and try again.")}throw new errors_1.NotSupportedError("twilio.js 1.3+ SDKs require WebRTC browser support. For more information, see . If you have any questions about this announcement, please contact Twilio Support at .")}if(window){var root=window;var browser=root.msBrowser||root.browser||root.chrome;_this._isBrowserExtension=!!browser&&!!browser.runtime&&!!browser.runtime.id||!!root.safari&&!!root.safari.extension}if(_this._isBrowserExtension){_this._log.info("Running as browser extension.")}if(navigator){var n=navigator;_this._networkInformation=n.connection||n.mozConnection||n.webkitConnection}if(_this._networkInformation&&typeof _this._networkInformation.addEventListener==="function"){_this._networkInformation.addEventListener("change",_this._publishNetworkChange)}Device._getOrCreateAudioContext();if(Device._audioContext){if(!Device._dialtonePlayer){Device._dialtonePlayer=new dialtonePlayer_1.default(Device._audioContext)}}if(typeof Device._isUnifiedPlanDefault==="undefined"){Device._isUnifiedPlanDefault=typeof window!=="undefined"&&typeof RTCPeerConnection!=="undefined"&&typeof RTCRtpTransceiver!=="undefined"?util_1.isUnifiedPlanDefault(window,window.navigator,RTCPeerConnection,RTCRtpTransceiver):false}_this._boundDestroy=_this.destroy.bind(_this);_this._boundConfirmClose=_this._confirmClose.bind(_this);if(typeof window!=="undefined"&&window.addEventListener){window.addEventListener("unload",_this._boundDestroy);window.addEventListener("pagehide",_this._boundDestroy)}_this.updateOptions(options);return _this}Object.defineProperty(Device,"audioContext",{get:function(){return Device._audioContext},enumerable:true,configurable:true});Object.defineProperty(Device,"extension",{get:function(){var a=typeof document!=="undefined"?document.createElement("audio"):{canPlayType:false};var canPlayMp3;try{canPlayMp3=a.canPlayType&&!!a.canPlayType("audio/mpeg").replace(/no/,"")}catch(e){canPlayMp3=false}var canPlayVorbis;try{canPlayVorbis=a.canPlayType&&!!a.canPlayType("audio/ogg;codecs='vorbis'").replace(/no/,"")}catch(e){canPlayVorbis=false}return canPlayVorbis&&!canPlayMp3?"ogg":"mp3"},enumerable:true,configurable:true});Object.defineProperty(Device,"isSupported",{get:function(){return rtc.enabled()},enumerable:true,configurable:true});Object.defineProperty(Device,"packageName",{get:function(){return C.PACKAGE_NAME},enumerable:true,configurable:true});Device.runPreflight=function(token,options){return new preflight_1.PreflightTest(token,__assign({audioContext:Device._getOrCreateAudioContext()},options))};Device.toString=function(){return"[Twilio.Device class]"};Object.defineProperty(Device,"version",{get:function(){return C.RELEASE_VERSION},enumerable:true,configurable:true});Device._getOrCreateAudioContext=function(){if(!Device._audioContext){if(typeof AudioContext!=="undefined"){Device._audioContext=new AudioContext}else if(typeof webkitAudioContext!=="undefined"){Device._audioContext=new webkitAudioContext}}return Device._audioContext};Object.defineProperty(Device.prototype,"audio",{get:function(){return this._audio},enumerable:true,configurable:true});Device.prototype.connect=function(options){if(options===void 0){options={}}return __awaiter(this,void 0,void 0,function(){var activeCall,_a;return __generator(this,function(_b){switch(_b.label){case 0:this._throwIfDestroyed();if(this._activeCall){throw new errors_1.InvalidStateError("A Call is already active")}_a=this;return[4,this._makeCall(options.params||{},{rtcConfiguration:options.rtcConfiguration,voiceEventSidGenerator:this._options.voiceEventSidGenerator})];case 1:activeCall=_a._activeCall=_b.sent();this._calls.splice(0).forEach(function(call){return call.ignore()});this._soundcache.get(Device.SoundName.Incoming).stop();activeCall.accept({rtcConstraints:options.rtcConstraints});this._publishNetworkChange();return[2,activeCall]}})})};Object.defineProperty(Device.prototype,"calls",{get:function(){return this._calls},enumerable:true,configurable:true});Device.prototype.destroy=function(){this.disconnectAll();this._stopRegistrationTimer();if(this._audio){this._audio._unbind()}this._destroyStream();this._destroyPublisher();this._destroyAudioHelper();if(this._networkInformation&&typeof this._networkInformation.removeEventListener==="function"){this._networkInformation.removeEventListener("change",this._publishNetworkChange)}if(typeof window!=="undefined"&&window.removeEventListener){window.removeEventListener("beforeunload",this._boundConfirmClose);window.removeEventListener("unload",this._boundDestroy);window.removeEventListener("pagehide",this._boundDestroy)}this._setState(Device.State.Destroyed);events_1.EventEmitter.prototype.removeAllListeners.call(this)};Device.prototype.disconnectAll=function(){var calls=this._calls.splice(0);calls.forEach(function(call){return call.disconnect()});if(this._activeCall){this._activeCall.disconnect()}};Object.defineProperty(Device.prototype,"edge",{get:function(){return this._edge},enumerable:true,configurable:true});Object.defineProperty(Device.prototype,"home",{get:function(){return this._home},enumerable:true,configurable:true});Object.defineProperty(Device.prototype,"identity",{get:function(){return this._identity},enumerable:true,configurable:true});Object.defineProperty(Device.prototype,"isBusy",{get:function(){return!!this._activeCall},enumerable:true,configurable:true});Device.prototype.register=function(){return __awaiter(this,void 0,void 0,function(){var stream,streamReadyPromise;var _this=this;return __generator(this,function(_a){switch(_a.label){case 0:if(this.state!==Device.State.Unregistered){throw new errors_1.InvalidStateError('Attempt to register when device is in state "'+this.state+'". '+('Must be "'+Device.State.Unregistered+'".'))}this._setState(Device.State.Registering);return[4,this._streamConnectedPromise||this._setupStream()];case 1:stream=_a.sent();streamReadyPromise=new Promise(function(resolve){_this.once(Device.State.Registered,resolve)});return[4,this._sendPresence(true)];case 2:_a.sent();return[4,streamReadyPromise];case 3:_a.sent();return[2]}})})};Object.defineProperty(Device.prototype,"state",{get:function(){return this._state},enumerable:true,configurable:true});Object.defineProperty(Device.prototype,"token",{get:function(){return this._token},enumerable:true,configurable:true});Device.prototype.toString=function(){return"[Twilio.Device instance]"};Device.prototype.unregister=function(){return __awaiter(this,void 0,void 0,function(){var stream,streamOfflinePromise;return __generator(this,function(_a){switch(_a.label){case 0:if(this.state!==Device.State.Registered){throw new errors_1.InvalidStateError('Attempt to unregister when device is in state "'+this.state+'". '+('Must be "'+Device.State.Registered+'".'))}this._shouldReRegister=false;return[4,this._streamConnectedPromise];case 1:stream=_a.sent();streamOfflinePromise=new Promise(function(resolve){stream.on("offline",resolve)});return[4,this._sendPresence(false)];case 2:_a.sent();return[4,streamOfflinePromise];case 3:_a.sent();return[2]}})})};Device.prototype.updateOptions=function(options){if(options===void 0){options={}}if(this.state===Device.State.Destroyed){throw new errors_1.InvalidStateError('Attempt to "updateOptions" when device is in state "'+this.state+'".')}this._options=__assign(__assign(__assign({},this._defaultOptions),this._options),options);var originalChunderURIs=new Set(this._chunderURIs);var chunderw=typeof this._options.chunderw==="string"?[this._options.chunderw]:Array.isArray(this._options.chunderw)&&this._options.chunderw;var newChunderURIs=this._chunderURIs=(chunderw||regions_1.getChunderURIs(this._options.edge,undefined,this._log.warn.bind(this._log))).map(regions_1.createSignalingEndpointURL);var hasChunderURIsChanged=originalChunderURIs.size!==newChunderURIs.length;if(!hasChunderURIsChanged){for(var _i=0,newChunderURIs_1=newChunderURIs;_i=0;i--){if(call===this._calls[i]){this._calls.splice(i,1)}}};Device.prototype._sendPresence=function(presence){return __awaiter(this,void 0,void 0,function(){var stream;return __generator(this,function(_a){switch(_a.label){case 0:return[4,this._streamConnectedPromise];case 1:stream=_a.sent();if(!stream){return[2]}stream.register({audio:presence});if(presence){this._startRegistrationTimer()}else{this._stopRegistrationTimer()}return[2]}})})};Device.prototype._setState=function(state){if(state===this.state){return}this._state=state;this.emit(this._stateEventMapping[state])};Device.prototype._setupAudioHelper=function(){var _this=this;if(this._audio){this._log.info("Found existing audio helper; destroying...");this._destroyAudioHelper()}this._audio=new(this._options.AudioHelper||audiohelper_1.default)(this._updateSinkIds,this._updateInputStream,getUserMedia,{audioContext:Device.audioContext,enabledSounds:this._enabledSounds});this._audio.on("deviceChange",function(lostActiveDevices){var activeCall=_this._activeCall;var deviceIds=lostActiveDevices.map(function(device){return device.deviceId});_this._publisher.info("audio","device-change",{lost_active_device_ids:deviceIds},activeCall);if(activeCall){activeCall["_mediaHandler"]._onInputDevicesChanged()}})};Device.prototype._setupPublisher=function(){var _this=this;if(this._publisher){this._log.info("Found existing publisher; destroying...");this._destroyPublisher()}var publisherOptions={defaultPayload:this._createDefaultPayload,log:this._log,metadata:{app_name:this._options.appName,app_version:this._options.appVersion}};if(this._options.eventgw){publisherOptions.host=this._options.eventgw}if(this._home){publisherOptions.host=regions_1.createEventGatewayURI(this._home)}this._publisher=new(this._options.Publisher||Publisher)(PUBLISHER_PRODUCT_NAME,this.token,publisherOptions);if(this._options.publishEvents===false){this._publisher.disable()}else{this._publisher.on("error",function(error){_this._log.warn("Cannot connect to insights.",error)})}return this._publisher};Device.prototype._setupStream=function(){var _this=this;if(this._stream){this._log.info("Found existing stream; destroying...");this._destroyStream()}this._log.info("Setting up VSP");this._stream=new(this._options.PStream||PStream)(this.token,this._chunderURIs,{backoffMaxMs:this._options.backoffMaxMs,maxPreferredDurationMs:this._options.maxCallSignalingTimeoutMs});this._stream.addListener("close",this._onSignalingClose);this._stream.addListener("connected",this._onSignalingConnected);this._stream.addListener("error",this._onSignalingError);this._stream.addListener("invite",this._onSignalingInvite);this._stream.addListener("offline",this._onSignalingOffline);this._stream.addListener("ready",this._onSignalingReady);return this._streamConnectedPromise=new Promise(function(resolve){return _this._stream.once("connected",function(){resolve(_this._stream)})})};Device.prototype._showIncomingCall=function(call,play){var _this=this;var timeout;return Promise.race([play(),new Promise(function(resolve,reject){timeout=setTimeout(function(){var msg="Playing incoming ringtone took too long; it might not play. Continuing execution...";reject(new Error(msg))},RINGTONE_PLAY_TIMEOUT)})]).catch(function(reason){_this._log.info(reason.message)}).then(function(){clearTimeout(timeout);_this.emit(Device.EventName.Incoming,call)})};Device.prototype._startRegistrationTimer=function(){var _this=this;this._stopRegistrationTimer();this._regTimer=setTimeout(function(){_this._sendPresence(true)},REGISTRATION_INTERVAL)};Device.prototype._stopRegistrationTimer=function(){if(this._regTimer){clearTimeout(this._regTimer)}};Device.prototype._throwIfDestroyed=function(){if(this.state===Device.State.Destroyed){throw new errors_1.InvalidStateError("Device has been destroyed.")}};Device.prototype._updateRingtoneSinkIds=function(sinkIds){return Promise.resolve(this._soundcache.get(Device.SoundName.Incoming).setSinkIds(sinkIds))};Device.prototype._updateSpeakerSinkIds=function(sinkIds){Array.from(this._soundcache.entries()).filter(function(entry){return entry[0]!==Device.SoundName.Incoming}).forEach(function(entry){return entry[1].setSinkIds(sinkIds)});this._callSinkIds=sinkIds;var call=this._activeCall;return call?call._setSinkIds(sinkIds):Promise.resolve()};Device._defaultSounds={disconnect:{filename:"disconnect",maxDuration:3e3},dtmf0:{filename:"dtmf-0",maxDuration:1e3},dtmf1:{filename:"dtmf-1",maxDuration:1e3},dtmf2:{filename:"dtmf-2",maxDuration:1e3},dtmf3:{filename:"dtmf-3",maxDuration:1e3},dtmf4:{filename:"dtmf-4",maxDuration:1e3},dtmf5:{filename:"dtmf-5",maxDuration:1e3},dtmf6:{filename:"dtmf-6",maxDuration:1e3},dtmf7:{filename:"dtmf-7",maxDuration:1e3},dtmf8:{filename:"dtmf-8",maxDuration:1e3},dtmf9:{filename:"dtmf-9",maxDuration:1e3},dtmfh:{filename:"dtmf-hash",maxDuration:1e3},dtmfs:{filename:"dtmf-star",maxDuration:1e3},incoming:{filename:"incoming",shouldLoop:true},outgoing:{filename:"outgoing",maxDuration:3e3}};return Device}(events_1.EventEmitter);(function(Device){var EventName;(function(EventName){EventName["Error"]="error";EventName["Incoming"]="incoming";EventName["Destroyed"]="destroyed";EventName["Unregistered"]="unregistered";EventName["Registering"]="registering";EventName["Registered"]="registered";EventName["TokenWillExpire"]="tokenWillExpire"})(EventName=Device.EventName||(Device.EventName={}));var State;(function(State){State["Destroyed"]="destroyed";State["Unregistered"]="unregistered";State["Registering"]="registering";State["Registered"]="registered"})(State=Device.State||(Device.State={}));var SoundName;(function(SoundName){SoundName["Incoming"]="incoming";SoundName["Outgoing"]="outgoing";SoundName["Disconnect"]="disconnect";SoundName["Dtmf0"]="dtmf0";SoundName["Dtmf1"]="dtmf1";SoundName["Dtmf2"]="dtmf2";SoundName["Dtmf3"]="dtmf3";SoundName["Dtmf4"]="dtmf4";SoundName["Dtmf5"]="dtmf5";SoundName["Dtmf6"]="dtmf6";SoundName["Dtmf7"]="dtmf7";SoundName["Dtmf8"]="dtmf8";SoundName["Dtmf9"]="dtmf9";SoundName["DtmfS"]="dtmfs";SoundName["DtmfH"]="dtmfh"})(SoundName=Device.SoundName||(Device.SoundName={}))})(Device||(Device={}));exports.default=Device},{"./audiohelper":5,"./call":6,"./constants":7,"./dialtonePlayer":10,"./errors":12,"./eventpublisher":14,"./log":15,"./preflight/preflight":17,"./pstream":18,"./regions":19,"./rtc":23,"./rtc/getusermedia":21,"./sound":33,"./util":35,"./uuid":36,events:53,loglevel:55}],10:[function(require,module,exports){"use strict";Object.defineProperty(exports,"__esModule",{value:true});var errors_1=require("./errors");var bandFrequencies={dtmf0:[1360,960],dtmf1:[1230,720],dtmf2:[1360,720],dtmf3:[1480,720],dtmf4:[1230,790],dtmf5:[1360,790],dtmf6:[1480,790],dtmf7:[1230,870],dtmf8:[1360,870],dtmf9:[1480,870],dtmfh:[1480,960],dtmfs:[1230,960]};var DialtonePlayer=function(){function DialtonePlayer(_context){var _this=this;this._context=_context;this._gainNodes=[];this._gainNodes=[this._context.createGain(),this._context.createGain()];this._gainNodes.forEach(function(gainNode){gainNode.connect(_this._context.destination);gainNode.gain.value=.1;_this._gainNodes.push(gainNode)})}DialtonePlayer.prototype.cleanup=function(){this._gainNodes.forEach(function(gainNode){gainNode.disconnect()})};DialtonePlayer.prototype.play=function(sound){var _this=this;var frequencies=bandFrequencies[sound];if(!frequencies){throw new errors_1.InvalidArgumentError("Invalid DTMF sound name")}var oscillators=[this._context.createOscillator(),this._context.createOscillator()];oscillators.forEach(function(oscillator,i){oscillator.type="sine";oscillator.frequency.value=frequencies[i];oscillator.connect(_this._gainNodes[i]);oscillator.start();oscillator.stop(_this._context.currentTime+.1);oscillator.addEventListener("ended",function(){return oscillator.disconnect()})})};return DialtonePlayer}();exports.default=DialtonePlayer},{"./errors":12}],11:[function(require,module,exports){"use strict";var __extends=this&&this.__extends||function(){var extendStatics=function(d,b){extendStatics=Object.setPrototypeOf||{__proto__:[]}instanceof Array&&function(d,b){d.__proto__=b}||function(d,b){for(var p in b)if(b.hasOwnProperty(p))d[p]=b[p]};return extendStatics(d,b)};return function(d,b){extendStatics(d,b);function __(){this.constructor=d}d.prototype=b===null?Object.create(b):(__.prototype=b.prototype,new __)}}();Object.defineProperty(exports,"__esModule",{value:true});var twilioError_1=require("./twilioError");exports.TwilioError=twilioError_1.default;var AuthorizationErrors;(function(AuthorizationErrors){var AccessTokenInvalid=function(_super){__extends(AccessTokenInvalid,_super);function AccessTokenInvalid(messageOrError,error){var _this=_super.call(this,messageOrError,error)||this;_this.causes=[];_this.code=20101;_this.description="Invalid access token";_this.explanation="Twilio was unable to validate your Access Token";_this.name="AccessTokenInvalid";_this.solutions=[];Object.setPrototypeOf(_this,AuthorizationErrors.AccessTokenInvalid.prototype);var message=typeof messageOrError==="string"?messageOrError:_this.explanation;var originalError=typeof messageOrError==="object"?messageOrError:error;_this.message=_this.name+" ("+_this.code+"): "+message;_this.originalError=originalError;return _this}return AccessTokenInvalid}(twilioError_1.default);AuthorizationErrors.AccessTokenInvalid=AccessTokenInvalid;var AccessTokenExpired=function(_super){__extends(AccessTokenExpired,_super);function AccessTokenExpired(messageOrError,error){var _this=_super.call(this,messageOrError,error)||this;_this.causes=[];_this.code=20104;_this.description="Access token expired or expiration date invalid";_this.explanation="The Access Token provided to the Twilio API has expired, the expiration time specified in the token was invalid, or the expiration time specified was too far in the future";_this.name="AccessTokenExpired";_this.solutions=[];Object.setPrototypeOf(_this,AuthorizationErrors.AccessTokenExpired.prototype);var message=typeof messageOrError==="string"?messageOrError:_this.explanation;var originalError=typeof messageOrError==="object"?messageOrError:error;_this.message=_this.name+" ("+_this.code+"): "+message;_this.originalError=originalError;return _this}return AccessTokenExpired}(twilioError_1.default);AuthorizationErrors.AccessTokenExpired=AccessTokenExpired;var AuthenticationFailed=function(_super){__extends(AuthenticationFailed,_super);function AuthenticationFailed(messageOrError,error){var _this=_super.call(this,messageOrError,error)||this;_this.causes=[];_this.code=20151;_this.description="Authentication Failed";_this.explanation="The Authentication with the provided JWT failed";_this.name="AuthenticationFailed";_this.solutions=[];Object.setPrototypeOf(_this,AuthorizationErrors.AuthenticationFailed.prototype);var message=typeof messageOrError==="string"?messageOrError:_this.explanation;var originalError=typeof messageOrError==="object"?messageOrError:error;_this.message=_this.name+" ("+_this.code+"): "+message;_this.originalError=originalError;return _this}return AuthenticationFailed}(twilioError_1.default);AuthorizationErrors.AuthenticationFailed=AuthenticationFailed})(AuthorizationErrors=exports.AuthorizationErrors||(exports.AuthorizationErrors={}));var ClientErrors;(function(ClientErrors){var BadRequest=function(_super){__extends(BadRequest,_super);function BadRequest(messageOrError,error){var _this=_super.call(this,messageOrError,error)||this;_this.causes=[];_this.code=31400;_this.description="Bad Request (HTTP/SIP)";_this.explanation="The request could not be understood due to malformed syntax.";_this.name="BadRequest";_this.solutions=[];Object.setPrototypeOf(_this,ClientErrors.BadRequest.prototype);var message=typeof messageOrError==="string"?messageOrError:_this.explanation;var originalError=typeof messageOrError==="object"?messageOrError:error;_this.message=_this.name+" ("+_this.code+"): "+message;_this.originalError=originalError;return _this}return BadRequest}(twilioError_1.default);ClientErrors.BadRequest=BadRequest})(ClientErrors=exports.ClientErrors||(exports.ClientErrors={}));var GeneralErrors;(function(GeneralErrors){var UnknownError=function(_super){__extends(UnknownError,_super);function UnknownError(messageOrError,error){var _this=_super.call(this,messageOrError,error)||this;_this.causes=[];_this.code=31e3;_this.description="Unknown Error";_this.explanation="An unknown error has occurred. See error details for more information.";_this.name="UnknownError";_this.solutions=[];Object.setPrototypeOf(_this,GeneralErrors.UnknownError.prototype);var message=typeof messageOrError==="string"?messageOrError:_this.explanation;var originalError=typeof messageOrError==="object"?messageOrError:error;_this.message=_this.name+" ("+_this.code+"): "+message;_this.originalError=originalError;return _this}return UnknownError}(twilioError_1.default);GeneralErrors.UnknownError=UnknownError;var ConnectionError=function(_super){__extends(ConnectionError,_super);function ConnectionError(messageOrError,error){var _this=_super.call(this,messageOrError,error)||this;_this.causes=[];_this.code=31005;_this.description="Connection error";_this.explanation="A connection error occurred during the call";_this.name="ConnectionError";_this.solutions=[];Object.setPrototypeOf(_this,GeneralErrors.ConnectionError.prototype);var message=typeof messageOrError==="string"?messageOrError:_this.explanation;var originalError=typeof messageOrError==="object"?messageOrError:error;_this.message=_this.name+" ("+_this.code+"): "+message;_this.originalError=originalError;return _this}return ConnectionError}(twilioError_1.default);GeneralErrors.ConnectionError=ConnectionError;var CallCancelledError=function(_super){__extends(CallCancelledError,_super);function CallCancelledError(messageOrError,error){var _this=_super.call(this,messageOrError,error)||this;_this.causes=["The incoming call was cancelled because it was not answered in time or it was accepted/rejected by another application instance registered with the same identity."];_this.code=31008;_this.description="Call cancelled";_this.explanation="Unable to answer because the call has ended";_this.name="CallCancelledError";_this.solutions=[];Object.setPrototypeOf(_this,GeneralErrors.CallCancelledError.prototype);var message=typeof messageOrError==="string"?messageOrError:_this.explanation;var originalError=typeof messageOrError==="object"?messageOrError:error;_this.message=_this.name+" ("+_this.code+"): "+message;_this.originalError=originalError;return _this}return CallCancelledError}(twilioError_1.default);GeneralErrors.CallCancelledError=CallCancelledError;var TransportError=function(_super){__extends(TransportError,_super);function TransportError(messageOrError,error){var _this=_super.call(this,messageOrError,error)||this;_this.causes=[];_this.code=31009;_this.description="Transport error";_this.explanation="No transport available to send or receive messages";_this.name="TransportError";_this.solutions=[];Object.setPrototypeOf(_this,GeneralErrors.TransportError.prototype);var message=typeof messageOrError==="string"?messageOrError:_this.explanation;var originalError=typeof messageOrError==="object"?messageOrError:error;_this.message=_this.name+" ("+_this.code+"): "+message;_this.originalError=originalError;return _this}return TransportError}(twilioError_1.default);GeneralErrors.TransportError=TransportError})(GeneralErrors=exports.GeneralErrors||(exports.GeneralErrors={}));var MalformedRequestErrors;(function(MalformedRequestErrors){var MalformedRequestError=function(_super){__extends(MalformedRequestError,_super);function MalformedRequestError(messageOrError,error){var _this=_super.call(this,messageOrError,error)||this;_this.causes=["Invalid content or MessageType passed to sendMessage method."];_this.code=31100;_this.description="The request had malformed syntax.";_this.explanation="The request could not be understood due to malformed syntax.";_this.name="MalformedRequestError";_this.solutions=["Ensure content and MessageType passed to sendMessage method are valid."];Object.setPrototypeOf(_this,MalformedRequestErrors.MalformedRequestError.prototype);var message=typeof messageOrError==="string"?messageOrError:_this.explanation;var originalError=typeof messageOrError==="object"?messageOrError:error;_this.message=_this.name+" ("+_this.code+"): "+message;_this.originalError=originalError;return _this}return MalformedRequestError}(twilioError_1.default);MalformedRequestErrors.MalformedRequestError=MalformedRequestError})(MalformedRequestErrors=exports.MalformedRequestErrors||(exports.MalformedRequestErrors={}));(function(AuthorizationErrors){var RateExceededError=function(_super){__extends(RateExceededError,_super);function RateExceededError(messageOrError,error){var _this=_super.call(this,messageOrError,error)||this;_this.causes=["Rate limit exceeded."];_this.code=31206;_this.description="Rate exceeded authorized limit.";_this.explanation="The request performed exceeds the authorized limit.";_this.name="RateExceededError";_this.solutions=["Ensure message send rate does not exceed authorized limits."];Object.setPrototypeOf(_this,AuthorizationErrors.RateExceededError.prototype);var message=typeof messageOrError==="string"?messageOrError:_this.explanation;var originalError=typeof messageOrError==="object"?messageOrError:error;_this.message=_this.name+" ("+_this.code+"): "+message;_this.originalError=originalError;return _this}return RateExceededError}(twilioError_1.default);AuthorizationErrors.RateExceededError=RateExceededError;var PayloadSizeExceededError=function(_super){__extends(PayloadSizeExceededError,_super);function PayloadSizeExceededError(messageOrError,error){var _this=_super.call(this,messageOrError,error)||this;_this.causes=["The payload size of Call Message Event exceeds the authorized limit."];_this.code=31209;_this.description="Call Message Event Payload size exceeded authorized limit.";_this.explanation="The request performed to send a Call Message Event exceeds the payload size authorized limit";_this.name="PayloadSizeExceededError";_this.solutions=["Reduce payload size of Call Message Event to be within the authorized limit and try again."];Object.setPrototypeOf(_this,AuthorizationErrors.PayloadSizeExceededError.prototype);var message=typeof messageOrError==="string"?messageOrError:_this.explanation;var originalError=typeof messageOrError==="object"?messageOrError:error;_this.message=_this.name+" ("+_this.code+"): "+message;_this.originalError=originalError;return _this}return PayloadSizeExceededError}(twilioError_1.default);AuthorizationErrors.PayloadSizeExceededError=PayloadSizeExceededError})(AuthorizationErrors=exports.AuthorizationErrors||(exports.AuthorizationErrors={}));var UserMediaErrors;(function(UserMediaErrors){var PermissionDeniedError=function(_super){__extends(PermissionDeniedError,_super);function PermissionDeniedError(messageOrError,error){var _this=_super.call(this,messageOrError,error)||this;_this.causes=["The user denied the getUserMedia request.","The browser denied the getUserMedia request."];_this.code=31401;_this.description="UserMedia Permission Denied Error";_this.explanation="The browser or end-user denied permissions to user media. Therefore we were unable to acquire input audio.";_this.name="PermissionDeniedError";_this.solutions=["The user should accept the request next time prompted. If the browser saved the deny, the user should change that permission in their browser.","The user should to verify that the browser has permission to access the microphone at this address."];Object.setPrototypeOf(_this,UserMediaErrors.PermissionDeniedError.prototype);var message=typeof messageOrError==="string"?messageOrError:_this.explanation;var originalError=typeof messageOrError==="object"?messageOrError:error;_this.message=_this.name+" ("+_this.code+"): "+message;_this.originalError=originalError;return _this}return PermissionDeniedError}(twilioError_1.default);UserMediaErrors.PermissionDeniedError=PermissionDeniedError;var AcquisitionFailedError=function(_super){__extends(AcquisitionFailedError,_super);function AcquisitionFailedError(messageOrError,error){var _this=_super.call(this,messageOrError,error)||this;_this.causes=["NotFoundError - The deviceID specified was not found.","The getUserMedia constraints were overconstrained and no devices matched."];_this.code=31402;_this.description="UserMedia Acquisition Failed Error";_this.explanation="The browser and end-user allowed permissions, however getting the media failed. Usually this is due to bad constraints, but can sometimes fail due to browser, OS or hardware issues.";_this.name="AcquisitionFailedError";_this.solutions=["Ensure the deviceID being specified exists.","Try acquiring media with fewer constraints."];Object.setPrototypeOf(_this,UserMediaErrors.AcquisitionFailedError.prototype);var message=typeof messageOrError==="string"?messageOrError:_this.explanation;var originalError=typeof messageOrError==="object"?messageOrError:error;_this.message=_this.name+" ("+_this.code+"): "+message;_this.originalError=originalError;return _this}return AcquisitionFailedError}(twilioError_1.default);UserMediaErrors.AcquisitionFailedError=AcquisitionFailedError})(UserMediaErrors=exports.UserMediaErrors||(exports.UserMediaErrors={}));var SignalingErrors;(function(SignalingErrors){var ConnectionError=function(_super){__extends(ConnectionError,_super);function ConnectionError(messageOrError,error){var _this=_super.call(this,messageOrError,error)||this;_this.causes=[];_this.code=53e3;_this.description="Signaling connection error";_this.explanation="Raised whenever a signaling connection error occurs that is not covered by a more specific error code.";_this.name="ConnectionError";_this.solutions=[];Object.setPrototypeOf(_this,SignalingErrors.ConnectionError.prototype);var message=typeof messageOrError==="string"?messageOrError:_this.explanation;var originalError=typeof messageOrError==="object"?messageOrError:error;_this.message=_this.name+" ("+_this.code+"): "+message;_this.originalError=originalError;return _this}return ConnectionError}(twilioError_1.default);SignalingErrors.ConnectionError=ConnectionError;var ConnectionDisconnected=function(_super){__extends(ConnectionDisconnected,_super);function ConnectionDisconnected(messageOrError,error){var _this=_super.call(this,messageOrError,error)||this;_this.causes=["The device running your application lost its Internet connection."];_this.code=53001;_this.description="Signaling connection disconnected";_this.explanation="Raised whenever the signaling connection is unexpectedly disconnected.";_this.name="ConnectionDisconnected";_this.solutions=["Ensure the device running your application has access to a stable Internet connection."];Object.setPrototypeOf(_this,SignalingErrors.ConnectionDisconnected.prototype);var message=typeof messageOrError==="string"?messageOrError:_this.explanation;var originalError=typeof messageOrError==="object"?messageOrError:error;_this.message=_this.name+" ("+_this.code+"): "+message;_this.originalError=originalError;return _this}return ConnectionDisconnected}(twilioError_1.default);SignalingErrors.ConnectionDisconnected=ConnectionDisconnected})(SignalingErrors=exports.SignalingErrors||(exports.SignalingErrors={}));var MediaErrors;(function(MediaErrors){var ClientLocalDescFailed=function(_super){__extends(ClientLocalDescFailed,_super);function ClientLocalDescFailed(messageOrError,error){var _this=_super.call(this,messageOrError,error)||this;_this.causes=["The Client may not be using a supported WebRTC implementation.","The Client may not have the necessary resources to create or apply a new media description."];_this.code=53400;_this.description="Client is unable to create or apply a local media description";_this.explanation="Raised whenever a Client is unable to create or apply a local media description.";_this.name="ClientLocalDescFailed";_this.solutions=["If you are experiencing this error using the JavaScript SDK, ensure you are running it with a supported WebRTC implementation."];Object.setPrototypeOf(_this,MediaErrors.ClientLocalDescFailed.prototype);var message=typeof messageOrError==="string"?messageOrError:_this.explanation;var originalError=typeof messageOrError==="object"?messageOrError:error;_this.message=_this.name+" ("+_this.code+"): "+message;_this.originalError=originalError;return _this}return ClientLocalDescFailed}(twilioError_1.default);MediaErrors.ClientLocalDescFailed=ClientLocalDescFailed;var ClientRemoteDescFailed=function(_super){__extends(ClientRemoteDescFailed,_super);function ClientRemoteDescFailed(messageOrError,error){var _this=_super.call(this,messageOrError,error)||this;_this.causes=["The Client may not be using a supported WebRTC implementation.","The Client may be connecting peer-to-peer with another Participant that is not using a supported WebRTC implementation.","The Client may not have the necessary resources to apply a new media description."];_this.code=53402;_this.description="Client is unable to apply a remote media description";_this.explanation="Raised whenever the Client receives a remote media description but is unable to apply it.";_this.name="ClientRemoteDescFailed";_this.solutions=["If you are experiencing this error using the JavaScript SDK, ensure you are running it with a supported WebRTC implementation."];Object.setPrototypeOf(_this,MediaErrors.ClientRemoteDescFailed.prototype);var message=typeof messageOrError==="string"?messageOrError:_this.explanation;var originalError=typeof messageOrError==="object"?messageOrError:error;_this.message=_this.name+" ("+_this.code+"): "+message;_this.originalError=originalError;return _this}return ClientRemoteDescFailed}(twilioError_1.default);MediaErrors.ClientRemoteDescFailed=ClientRemoteDescFailed;var ConnectionError=function(_super){__extends(ConnectionError,_super);function ConnectionError(messageOrError,error){var _this=_super.call(this,messageOrError,error)||this;_this.causes=["The Client was unable to establish a media connection.","A media connection which was active failed liveliness checks."];_this.code=53405;_this.description="Media connection failed";_this.explanation="Raised by the Client or Server whenever a media connection fails.";_this.name="ConnectionError";_this.solutions=["If the problem persists, try connecting to another region.","Check your Client's network connectivity.","If you've provided custom ICE Servers then ensure that the URLs and credentials are valid."];Object.setPrototypeOf(_this,MediaErrors.ConnectionError.prototype);var message=typeof messageOrError==="string"?messageOrError:_this.explanation;var originalError=typeof messageOrError==="object"?messageOrError:error;_this.message=_this.name+" ("+_this.code+"): "+message;_this.originalError=originalError;return _this}return ConnectionError}(twilioError_1.default);MediaErrors.ConnectionError=ConnectionError})(MediaErrors=exports.MediaErrors||(exports.MediaErrors={}));exports.errorsByCode=new Map([[20101,AuthorizationErrors.AccessTokenInvalid],[20104,AuthorizationErrors.AccessTokenExpired],[20151,AuthorizationErrors.AuthenticationFailed],[31400,ClientErrors.BadRequest],[31e3,GeneralErrors.UnknownError],[31005,GeneralErrors.ConnectionError],[31008,GeneralErrors.CallCancelledError],[31009,GeneralErrors.TransportError],[31100,MalformedRequestErrors.MalformedRequestError],[31206,AuthorizationErrors.RateExceededError],[31209,AuthorizationErrors.PayloadSizeExceededError],[31401,UserMediaErrors.PermissionDeniedError],[31402,UserMediaErrors.AcquisitionFailedError],[53e3,SignalingErrors.ConnectionError],[53001,SignalingErrors.ConnectionDisconnected],[53400,MediaErrors.ClientLocalDescFailed],[53402,MediaErrors.ClientRemoteDescFailed],[53405,MediaErrors.ConnectionError]]);Object.freeze(exports.errorsByCode)},{"./twilioError":13}],12:[function(require,module,exports){"use strict";var __extends=this&&this.__extends||function(){var extendStatics=function(d,b){extendStatics=Object.setPrototypeOf||{__proto__:[]}instanceof Array&&function(d,b){d.__proto__=b}||function(d,b){for(var p in b)if(b.hasOwnProperty(p))d[p]=b[p]};return extendStatics(d,b)};return function(d,b){extendStatics(d,b);function __(){this.constructor=d}d.prototype=b===null?Object.create(b):(__.prototype=b.prototype,new __)}}();Object.defineProperty(exports,"__esModule",{value:true});var generated_1=require("./generated");exports.AuthorizationErrors=generated_1.AuthorizationErrors;exports.ClientErrors=generated_1.ClientErrors;exports.GeneralErrors=generated_1.GeneralErrors;exports.MediaErrors=generated_1.MediaErrors;exports.SignalingErrors=generated_1.SignalingErrors;exports.TwilioError=generated_1.TwilioError;exports.UserMediaErrors=generated_1.UserMediaErrors;var InvalidArgumentError=function(_super){__extends(InvalidArgumentError,_super);function InvalidArgumentError(message){var _this=_super.call(this,message)||this;_this.name="InvalidArgumentError";return _this}return InvalidArgumentError}(Error);exports.InvalidArgumentError=InvalidArgumentError;var InvalidStateError=function(_super){__extends(InvalidStateError,_super);function InvalidStateError(message){var _this=_super.call(this,message)||this;_this.name="InvalidStateError";return _this}return InvalidStateError}(Error);exports.InvalidStateError=InvalidStateError;var NotSupportedError=function(_super){__extends(NotSupportedError,_super);function NotSupportedError(message){var _this=_super.call(this,message)||this;_this.name="NotSupportedError";return _this}return NotSupportedError}(Error);exports.NotSupportedError=NotSupportedError;function getErrorByCode(code){var error=generated_1.errorsByCode.get(code);if(!error){throw new InvalidArgumentError("Error code "+code+" not found")}return error}exports.getErrorByCode=getErrorByCode;function hasErrorByCode(code){return generated_1.errorsByCode.has(code)}exports.hasErrorByCode=hasErrorByCode},{"./generated":11}],13:[function(require,module,exports){"use strict";var __extends=this&&this.__extends||function(){var extendStatics=function(d,b){extendStatics=Object.setPrototypeOf||{__proto__:[]}instanceof Array&&function(d,b){d.__proto__=b}||function(d,b){for(var p in b)if(b.hasOwnProperty(p))d[p]=b[p]};return extendStatics(d,b)};return function(d,b){extendStatics(d,b);function __(){this.constructor=d}d.prototype=b===null?Object.create(b):(__.prototype=b.prototype,new __)}}();Object.defineProperty(exports,"__esModule",{value:true});var TwilioError=function(_super){__extends(TwilioError,_super);function TwilioError(messageOrError,error){var _this=_super.call(this)||this;Object.setPrototypeOf(_this,TwilioError.prototype);var message=typeof messageOrError==="string"?messageOrError:_this.explanation;var originalError=typeof messageOrError==="object"?messageOrError:error;_this.message=_this.name+" ("+_this.code+"): "+message;_this.originalError=originalError;return _this}return TwilioError}(Error);exports.default=TwilioError},{}],14:[function(require,module,exports){"use strict";function _classCallCheck(instance,Constructor){if(!(instance instanceof Constructor)){throw new TypeError("Cannot call a class as a function")}}function _possibleConstructorReturn(self,call){if(!self){throw new ReferenceError("this hasn't been initialised - super() hasn't been called")}return call&&(typeof call==="object"||typeof call==="function")?call:self}function _inherits(subClass,superClass){if(typeof superClass!=="function"&&superClass!==null){throw new TypeError("Super expression must either be null or a function, not "+typeof superClass)}subClass.prototype=Object.create(superClass&&superClass.prototype,{constructor:{value:subClass,enumerable:false,writable:true,configurable:true}});if(superClass)Object.setPrototypeOf?Object.setPrototypeOf(subClass,superClass):subClass.__proto__=superClass}var EventEmitter=require("events").EventEmitter;var request=require("./request");var EventPublisher=function(_EventEmitter){_inherits(EventPublisher,_EventEmitter);function EventPublisher(productName,token,options){_classCallCheck(this,EventPublisher);var _this=_possibleConstructorReturn(this,(EventPublisher.__proto__||Object.getPrototypeOf(EventPublisher)).call(this));if(!(_this instanceof EventPublisher)){var _ret;return _ret=new EventPublisher(productName,token,options),_possibleConstructorReturn(_this,_ret)}options=Object.assign({defaultPayload:function defaultPayload(){return{}}},options);var defaultPayload=options.defaultPayload;if(typeof defaultPayload!=="function"){defaultPayload=function defaultPayload(){return Object.assign({},options.defaultPayload)}}var isEnabled=true;var metadata=Object.assign({app_name:undefined,app_version:undefined},options.metadata);Object.defineProperties(_this,{_defaultPayload:{value:defaultPayload},_isEnabled:{get:function get(){return isEnabled},set:function set(_isEnabled){isEnabled=_isEnabled}},_host:{value:options.host,writable:true},_log:{value:options.log},_request:{value:options.request||request,writable:true},_token:{value:token,writable:true},isEnabled:{enumerable:true,get:function get(){return isEnabled}},metadata:{enumerable:true,get:function get(){return metadata}},productName:{enumerable:true,value:productName},token:{enumerable:true,get:function get(){return this._token}}});return _this}return EventPublisher}(EventEmitter);EventPublisher.prototype._post=function _post(endpointName,level,group,name,payload,connection,force){var _this2=this;if(!this.isEnabled&&!force||!this._host){return Promise.resolve()}if(!connection||(!connection.parameters||!connection.parameters.CallSid)&&!connection.outboundConnectionId){return Promise.resolve()}var event={publisher:this.productName,group:group,name:name,timestamp:(new Date).toISOString(),level:level.toUpperCase(),payload_type:"application/json",private:false,payload:payload&&payload.forEach?payload.slice(0):Object.assign(this._defaultPayload(connection),payload)};if(this.metadata){event.publisher_metadata=this.metadata}var requestParams={url:"https://"+this._host+"/v4/"+endpointName,body:event,headers:{"Content-Type":"application/json","X-Twilio-Token":this.token}};return new Promise(function(resolve,reject){_this2._request.post(requestParams,function(err){if(err){_this2.emit("error",err);reject(err)}else{resolve()}})}).catch(function(e){_this2._log.warn("Unable to post "+group+" "+name+" event to Insights. Received error: "+e)})};EventPublisher.prototype.post=function post(level,group,name,payload,connection,force){return this._post("EndpointEvents",level,group,name,payload,connection,force)};EventPublisher.prototype.debug=function debug(group,name,payload,connection){return this.post("debug",group,name,payload,connection)};EventPublisher.prototype.info=function info(group,name,payload,connection){return this.post("info",group,name,payload,connection)};EventPublisher.prototype.warn=function warn(group,name,payload,connection){return this.post("warning",group,name,payload,connection)};EventPublisher.prototype.error=function error(group,name,payload,connection){return this.post("error",group,name,payload,connection)};EventPublisher.prototype.postMetrics=function postMetrics(group,name,metrics,customFields,connection){var _this3=this;return new Promise(function(resolve){var samples=metrics.map(formatMetric).map(function(sample){return Object.assign(sample,customFields)});resolve(_this3._post("EndpointMetrics","info",group,name,samples,connection))})};EventPublisher.prototype.setHost=function setHost(host){this._host=host};EventPublisher.prototype.setToken=function setToken(token){this._token=token};EventPublisher.prototype.enable=function enable(){this._isEnabled=true};EventPublisher.prototype.disable=function disable(){this._isEnabled=false};function formatMetric(sample){return{timestamp:new Date(sample.timestamp).toISOString(),total_packets_received:sample.totals.packetsReceived,total_packets_lost:sample.totals.packetsLost,total_packets_sent:sample.totals.packetsSent,total_bytes_received:sample.totals.bytesReceived,total_bytes_sent:sample.totals.bytesSent,packets_received:sample.packetsReceived,packets_lost:sample.packetsLost,packets_lost_fraction:sample.packetsLostFraction&&Math.round(sample.packetsLostFraction*100)/100,bytes_received:sample.bytesReceived,bytes_sent:sample.bytesSent,audio_codec:sample.codecName,audio_level_in:sample.audioInputLevel,audio_level_out:sample.audioOutputLevel,call_volume_input:sample.inputVolume,call_volume_output:sample.outputVolume,jitter:sample.jitter,rtt:sample.rtt,mos:sample.mos&&Math.round(sample.mos*100)/100}}module.exports=EventPublisher},{"./request":20,events:53}],15:[function(require,module,exports){"use strict";Object.defineProperty(exports,"__esModule",{value:true});var LogLevelModule=require("loglevel");var constants_1=require("./constants");var Log=function(){function Log(options){this._log=(options&&options.LogLevelModule?options.LogLevelModule:LogLevelModule).getLogger(constants_1.PACKAGE_NAME)}Log.getInstance=function(){if(!Log.instance){Log.instance=new Log}return Log.instance};Log.prototype.debug=function(){var _a;var args=[];for(var _i=0;_i0&&t[t.length-1])&&(op[0]===6||op[0]===2)){_=0;continue}if(op[0]===3&&(!t||op[1]>t[0]&&op[1]4.2){return PreflightTest.CallQuality.Excellent}else if(mos>=4.1&&mos<=4.2){return PreflightTest.CallQuality.Great}else if(mos>=3.7&&mos<=4){return PreflightTest.CallQuality.Good}else if(mos>=3.1&&mos<=3.6){return PreflightTest.CallQuality.Fair}else{return PreflightTest.CallQuality.Degraded}};PreflightTest.prototype._getReport=function(){var stats=this._getRTCStats();var testTiming={start:this._startTime};if(this._endTime){testTiming.end=this._endTime;testTiming.duration=this._endTime-this._startTime}var report={callSid:this._callSid,edge:this._edge,iceCandidateStats:this._rtcIceCandidateStatsReport.iceCandidateStats,networkTiming:this._networkTiming,samples:this._samples,selectedEdge:this._options.edge,stats:stats,testTiming:testTiming,totals:this._getRTCSampleTotals(),warnings:this._warnings};var selectedIceCandidatePairStats=this._rtcIceCandidateStatsReport.selectedIceCandidatePairStats;if(selectedIceCandidatePairStats){report.selectedIceCandidatePairStats=selectedIceCandidatePairStats;report.isTurnRequired=selectedIceCandidatePairStats.localCandidate.candidateType==="relay"||selectedIceCandidatePairStats.remoteCandidate.candidateType==="relay"}if(stats){report.callQuality=this._getCallQuality(stats.mos.average)}return report};PreflightTest.prototype._getRTCSampleTotals=function(){if(!this._latestSample){return}return __assign({},this._latestSample.totals)};PreflightTest.prototype._getRTCStats=function(){var firstMosSampleIdx=this._samples.findIndex(function(sample){return typeof sample.mos==="number"&&sample.mos>0});var samples=firstMosSampleIdx>=0?this._samples.slice(firstMosSampleIdx):[];if(!samples||!samples.length){return}return["jitter","mos","rtt"].reduce(function(statObj,stat){var _a;var values=samples.map(function(s){return s[stat]});return __assign(__assign({},statObj),(_a={},_a[stat]={average:Number((values.reduce(function(total,value){return total+value})/values.length).toPrecision(5)),max:Math.max.apply(Math,values),min:Math.min.apply(Math,values)},_a))},{})};PreflightTest.prototype._getStreamFromFile=function(){var audioContext=this._options.audioContext;if(!audioContext){throw new errors_1.NotSupportedError("Cannot fake input audio stream: AudioContext is not supported by this browser.")}var audioEl=new Audio(COWBELL_AUDIO_URL);audioEl.addEventListener("canplaythrough",function(){return audioEl.play()});if(typeof audioEl.setAttribute==="function"){audioEl.setAttribute("crossorigin","anonymous")}var src=audioContext.createMediaElementSource(audioEl);var dest=audioContext.createMediaStreamDestination();src.connect(dest);return dest.stream};PreflightTest.prototype._initDevice=function(token,options){var _this=this;try{this._device=new(options.deviceFactory||device_1.default)(token,{codecPreferences:options.codecPreferences,edge:options.edge,fileInputStream:options.fileInputStream,logLevel:options.logLevel,preflight:true});this._device.once(device_1.default.EventName.Registered,function(){_this._onDeviceRegistered()});this._device.once(device_1.default.EventName.Error,function(error){_this._onDeviceError(error)});this._device.register()}catch(error){setTimeout(function(){_this._onFailed(error)});return}this._signalingTimeoutTimer=setTimeout(function(){_this._onDeviceError(new errors_1.SignalingErrors.ConnectionError("WebSocket Connection Timeout"))},options.signalingTimeoutMs)};PreflightTest.prototype._onDeviceError=function(error){this._device.destroy();this._onFailed(error)};PreflightTest.prototype._onDeviceRegistered=function(){return __awaiter(this,void 0,void 0,function(){var _a,audio,publisher;var _this=this;return __generator(this,function(_b){switch(_b.label){case 0:clearTimeout(this._echoTimer);clearTimeout(this._signalingTimeoutTimer);_a=this;return[4,this._device.connect({rtcConfiguration:this._options.rtcConfiguration})];case 1:_a._call=_b.sent();this._networkTiming.signaling={start:Date.now()};this._setupCallHandlers(this._call);this._edge=this._device.edge||undefined;if(this._options.fakeMicInput){this._echoTimer=setTimeout(function(){return _this._device.disconnectAll()},ECHO_TEST_DURATION);audio=this._device.audio;if(audio){audio.disconnect(false);audio.outgoing(false)}}this._call.once("disconnect",function(){_this._device.once(device_1.default.EventName.Unregistered,function(){return _this._onUnregistered()});_this._device.destroy()});publisher=this._call["_publisher"];publisher.on("error",function(){if(!_this._hasInsightsErrored){_this._emitWarning("insights-connection-error","Received an error when attempting to connect to Insights gateway")}_this._hasInsightsErrored=true});return[2]}})})};PreflightTest.prototype._onFailed=function(error){clearTimeout(this._echoTimer);clearTimeout(this._signalingTimeoutTimer);this._releaseHandlers();this._endTime=Date.now();this._status=PreflightTest.Status.Failed;this.emit(PreflightTest.Events.Failed,error)};PreflightTest.prototype._onUnregistered=function(){var _this=this;setTimeout(function(){if(_this._status===PreflightTest.Status.Failed){return}clearTimeout(_this._echoTimer);clearTimeout(_this._signalingTimeoutTimer);_this._releaseHandlers();_this._endTime=Date.now();_this._status=PreflightTest.Status.Completed;_this._report=_this._getReport();_this.emit(PreflightTest.Events.Completed,_this._report)},10)};PreflightTest.prototype._releaseHandlers=function(){[this._device,this._call].forEach(function(emitter){if(emitter){emitter.eventNames().forEach(function(name){return emitter.removeAllListeners(name)})}})};PreflightTest.prototype._setupCallHandlers=function(call){var _this=this;if(this._options.fakeMicInput){call.once("volume",function(){call["_mediaHandler"].outputs.forEach(function(output){return output.audio.muted=true})})}call.on("warning",function(name,data){_this._emitWarning(name,"Received an RTCWarning. See .rtcWarning for the RTCWarning",data)});call.once("accept",function(){_this._callSid=call["_mediaHandler"].callSid;_this._status=PreflightTest.Status.Connected;_this.emit(PreflightTest.Events.Connected)});call.on("sample",function(sample){return __awaiter(_this,void 0,void 0,function(){var _a;return __generator(this,function(_b){switch(_b.label){case 0:if(!!this._latestSample)return[3,2];_a=this;return[4,(this._options.getRTCIceCandidateStatsReport||stats_1.getRTCIceCandidateStatsReport)(call["_mediaHandler"].version.pc)];case 1:_a._rtcIceCandidateStatsReport=_b.sent();_b.label=2;case 2:this._latestSample=sample;this._samples.push(sample);this.emit(PreflightTest.Events.Sample,sample);return[2]}})})});[{reportLabel:"peerConnection",type:"pcconnection"},{reportLabel:"ice",type:"iceconnection"},{reportLabel:"dtls",type:"dtlstransport"},{reportLabel:"signaling",type:"signaling"}].forEach(function(_a){var type=_a.type,reportLabel=_a.reportLabel;var handlerName="on"+type+"statechange";var originalHandler=call["_mediaHandler"][handlerName];call["_mediaHandler"][handlerName]=function(state){var timing=_this._networkTiming[reportLabel]=_this._networkTiming[reportLabel]||{start:0};if(state==="connecting"||state==="checking"){timing.start=Date.now()}else if((state==="connected"||state==="stable")&&!timing.duration){timing.end=Date.now();timing.duration=timing.end-timing.start}originalHandler(state)}})};Object.defineProperty(PreflightTest.prototype,"callSid",{get:function(){return this._callSid},enumerable:true,configurable:true});Object.defineProperty(PreflightTest.prototype,"endTime",{get:function(){return this._endTime},enumerable:true,configurable:true});Object.defineProperty(PreflightTest.prototype,"latestSample",{get:function(){return this._latestSample},enumerable:true,configurable:true});Object.defineProperty(PreflightTest.prototype,"report",{get:function(){return this._report},enumerable:true,configurable:true});Object.defineProperty(PreflightTest.prototype,"startTime",{get:function(){return this._startTime},enumerable:true,configurable:true});Object.defineProperty(PreflightTest.prototype,"status",{get:function(){return this._status},enumerable:true,configurable:true});return PreflightTest}(events_1.EventEmitter);exports.PreflightTest=PreflightTest;(function(PreflightTest){var CallQuality;(function(CallQuality){CallQuality["Excellent"]="excellent";CallQuality["Great"]="great";CallQuality["Good"]="good";CallQuality["Fair"]="fair";CallQuality["Degraded"]="degraded"})(CallQuality=PreflightTest.CallQuality||(PreflightTest.CallQuality={}));var Events;(function(Events){Events["Completed"]="completed";Events["Connected"]="connected";Events["Failed"]="failed";Events["Sample"]="sample";Events["Warning"]="warning"})(Events=PreflightTest.Events||(PreflightTest.Events={}));var Status;(function(Status){Status["Connecting"]="connecting";Status["Connected"]="connected";Status["Completed"]="completed";Status["Failed"]="failed"})(Status=PreflightTest.Status||(PreflightTest.Status={}))})(PreflightTest=exports.PreflightTest||(exports.PreflightTest={}));exports.PreflightTest=PreflightTest},{"../call":6,"../constants":7,"../device":9,"../errors":12,"../rtc/stats":29,events:53}],18:[function(require,module,exports){"use strict";function _toConsumableArray(arr){if(Array.isArray(arr)){for(var i=0,arr2=Array(arr.length);i2&&arguments[2]!==undefined?arguments[2]:"application/json";var messagetype=arguments[3];var voiceeventsid=arguments[4];var payload={callsid:callsid,content:content,contenttype:contenttype,messagetype:messagetype,voiceeventsid:voiceeventsid};this._publish("message",payload,true)};PStream.prototype.register=function(mediaCapabilities){var regPayload={media:mediaCapabilities};this._publish("register",regPayload,true)};PStream.prototype.invite=function(sdp,callsid,preflight,params){var payload={callsid:callsid,sdp:sdp,preflight:!!preflight,twilio:params?{params:params}:{}};this._publish("invite",payload,true)};PStream.prototype.reconnect=function(sdp,callsid,reconnect,params){var payload={callsid:callsid,sdp:sdp,reconnect:reconnect,preflight:false,twilio:params?{params:params}:{}};this._publish("invite",payload,true)};PStream.prototype.answer=function(sdp,callsid){this._publish("answer",{sdp:sdp,callsid:callsid},true)};PStream.prototype.dtmf=function(callsid,digits){this._publish("dtmf",{callsid:callsid,dtmf:digits},true)};PStream.prototype.hangup=function(callsid,message){var payload=message?{callsid:callsid,message:message}:{callsid:callsid};this._publish("hangup",payload,true)};PStream.prototype.reject=function(callsid){this._publish("reject",{callsid:callsid},true)};PStream.prototype.reinvite=function(sdp,callsid){this._publish("reinvite",{sdp:sdp,callsid:callsid},false)};PStream.prototype._destroy=function(){this.transport.removeListener("close",this._handleTransportClose);this.transport.removeListener("error",this._handleTransportError);this.transport.removeListener("message",this._handleTransportMessage);this.transport.removeListener("open",this._handleTransportOpen);this.transport.close();this.emit("offline",this)};PStream.prototype.destroy=function(){this._log.info("PStream.destroy() called...");this._destroy();return this};PStream.prototype.updatePreferredURI=function(uri){this._preferredUri=uri;this.transport.updatePreferredURI(uri)};PStream.prototype.updateURIs=function(uris){this._uris=uris;this.transport.updateURIs(this._uris)};PStream.prototype.publish=function(type,payload){return this._publish(type,payload,true)};PStream.prototype._publish=function(type,payload,shouldRetry){var msg=JSON.stringify({type:type,version:PSTREAM_VERSION,payload:payload});var isSent=!!this.transport.send(msg);if(!isSent){this.emit("error",{error:{code:31009,message:"No transport available to send or receive messages",twilioError:new GeneralErrors.TransportError}});if(shouldRetry){this._messageQueue.push([type,payload,true])}}};function getBrowserInfo(){var nav=typeof navigator!=="undefined"?navigator:{};var info={p:"browser",v:C.RELEASE_VERSION,browser:{userAgent:nav.userAgent||"unknown",platform:nav.platform||"unknown"},plugin:"rtc"};return info}module.exports=PStream},{"./constants":7,"./errors":12,"./log":15,"./wstransport":37,events:53}],19:[function(require,module,exports){"use strict";var _a,_b,_c,_d;Object.defineProperty(exports,"__esModule",{value:true});var errors_1=require("./errors");var DeprecatedRegion;(function(DeprecatedRegion){DeprecatedRegion["Au"]="au";DeprecatedRegion["Br"]="br";DeprecatedRegion["Ie"]="ie";DeprecatedRegion["Jp"]="jp";DeprecatedRegion["Sg"]="sg";DeprecatedRegion["UsOr"]="us-or";DeprecatedRegion["UsVa"]="us-va"})(DeprecatedRegion=exports.DeprecatedRegion||(exports.DeprecatedRegion={}));var Edge;(function(Edge){Edge["Sydney"]="sydney";Edge["SaoPaulo"]="sao-paulo";Edge["Dublin"]="dublin";Edge["Frankfurt"]="frankfurt";Edge["Tokyo"]="tokyo";Edge["Singapore"]="singapore";Edge["Ashburn"]="ashburn";Edge["Umatilla"]="umatilla";Edge["Roaming"]="roaming";Edge["AshburnIx"]="ashburn-ix";Edge["SanJoseIx"]="san-jose-ix";Edge["LondonIx"]="london-ix";Edge["FrankfurtIx"]="frankfurt-ix";Edge["SingaporeIx"]="singapore-ix";Edge["SydneyIx"]="sydney-ix";Edge["TokyoIx"]="tokyo-ix"})(Edge=exports.Edge||(exports.Edge={}));var Region;(function(Region){Region["Au1"]="au1";Region["Au1Ix"]="au1-ix";Region["Br1"]="br1";Region["De1"]="de1";Region["De1Ix"]="de1-ix";Region["Gll"]="gll";Region["Ie1"]="ie1";Region["Ie1Ix"]="ie1-ix";Region["Ie1Tnx"]="ie1-tnx";Region["Jp1"]="jp1";Region["Jp1Ix"]="jp1-ix";Region["Sg1"]="sg1";Region["Sg1Ix"]="sg1-ix";Region["Sg1Tnx"]="sg1-tnx";Region["Us1"]="us1";Region["Us1Ix"]="us1-ix";Region["Us1Tnx"]="us1-tnx";Region["Us2"]="us2";Region["Us2Ix"]="us2-ix";Region["Us2Tnx"]="us2-tnx"})(Region=exports.Region||(exports.Region={}));exports.deprecatedRegions=(_a={},_a[DeprecatedRegion.Au]=Region.Au1,_a[DeprecatedRegion.Br]=Region.Br1,_a[DeprecatedRegion.Ie]=Region.Ie1,_a[DeprecatedRegion.Jp]=Region.Jp1,_a[DeprecatedRegion.Sg]=Region.Sg1,_a[DeprecatedRegion.UsOr]=Region.Us1,_a[DeprecatedRegion.UsVa]=Region.Us1,_a);exports.regionShortcodes={ASIAPAC_SINGAPORE:Region.Sg1,ASIAPAC_SYDNEY:Region.Au1,ASIAPAC_TOKYO:Region.Jp1,EU_FRANKFURT:Region.De1,EU_IRELAND:Region.Ie1,SOUTH_AMERICA_SAO_PAULO:Region.Br1,US_EAST_VIRGINIA:Region.Us1,US_WEST_OREGON:Region.Us2};var regionURIs=(_b={},_b[Region.Au1]="chunderw-vpc-gll-au1.twilio.com",_b[Region.Au1Ix]="chunderw-vpc-gll-au1-ix.twilio.com",_b[Region.Br1]="chunderw-vpc-gll-br1.twilio.com",_b[Region.De1]="chunderw-vpc-gll-de1.twilio.com",_b[Region.De1Ix]="chunderw-vpc-gll-de1-ix.twilio.com",_b[Region.Gll]="chunderw-vpc-gll.twilio.com",_b[Region.Ie1]="chunderw-vpc-gll-ie1.twilio.com",_b[Region.Ie1Ix]="chunderw-vpc-gll-ie1-ix.twilio.com",_b[Region.Ie1Tnx]="chunderw-vpc-gll-ie1-tnx.twilio.com",_b[Region.Jp1]="chunderw-vpc-gll-jp1.twilio.com",_b[Region.Jp1Ix]="chunderw-vpc-gll-jp1-ix.twilio.com",_b[Region.Sg1]="chunderw-vpc-gll-sg1.twilio.com",_b[Region.Sg1Ix]="chunderw-vpc-gll-sg1-ix.twilio.com",_b[Region.Sg1Tnx]="chunderw-vpc-gll-sg1-tnx.twilio.com",_b[Region.Us1]="chunderw-vpc-gll-us1.twilio.com",_b[Region.Us1Ix]="chunderw-vpc-gll-us1-ix.twilio.com",_b[Region.Us1Tnx]="chunderw-vpc-gll-us1-tnx.twilio.com",_b[Region.Us2]="chunderw-vpc-gll-us2.twilio.com",_b[Region.Us2Ix]="chunderw-vpc-gll-us2-ix.twilio.com",_b[Region.Us2Tnx]="chunderw-vpc-gll-us2-tnx.twilio.com",_b);exports.edgeToRegion=(_c={},_c[Edge.Sydney]=Region.Au1,_c[Edge.SaoPaulo]=Region.Br1,_c[Edge.Dublin]=Region.Ie1,_c[Edge.Frankfurt]=Region.De1,_c[Edge.Tokyo]=Region.Jp1,_c[Edge.Singapore]=Region.Sg1,_c[Edge.Ashburn]=Region.Us1,_c[Edge.Umatilla]=Region.Us2,_c[Edge.Roaming]=Region.Gll,_c[Edge.AshburnIx]=Region.Us1Ix,_c[Edge.SanJoseIx]=Region.Us2Ix,_c[Edge.LondonIx]=Region.Ie1Ix,_c[Edge.FrankfurtIx]=Region.De1Ix,_c[Edge.SingaporeIx]=Region.Sg1Ix,_c[Edge.SydneyIx]=Region.Au1Ix,_c[Edge.TokyoIx]=Region.Jp1Ix,_c);exports.regionToEdge=(_d={},_d[Region.Au1]=Edge.Sydney,_d[Region.Br1]=Edge.SaoPaulo,_d[Region.Ie1]=Edge.Dublin,_d[Region.De1]=Edge.Frankfurt,_d[Region.Jp1]=Edge.Tokyo,_d[Region.Sg1]=Edge.Singapore,_d[Region.Us1]=Edge.Ashburn,_d[Region.Us2]=Edge.Umatilla,_d[Region.Gll]=Edge.Roaming,_d[Region.Us1Ix]=Edge.AshburnIx,_d[Region.Us2Ix]=Edge.SanJoseIx,_d[Region.Ie1Ix]=Edge.LondonIx,_d[Region.De1Ix]=Edge.FrankfurtIx,_d[Region.Sg1Ix]=Edge.SingaporeIx,_d[Region.Au1Ix]=Edge.SydneyIx,_d[Region.Jp1Ix]=Edge.TokyoIx,_d[Region.Us1Tnx]=Edge.AshburnIx,_d[Region.Us2Tnx]=Edge.AshburnIx,_d[Region.Ie1Tnx]=Edge.LondonIx,_d[Region.Sg1Tnx]=Edge.SingaporeIx,_d);exports.defaultRegion="gll";exports.defaultEdge=Edge.Roaming;exports.defaultChunderRegionURI="chunderw-vpc-gll.twilio.com";var defaultEventGatewayURI="eventgw.twilio.com";function createChunderRegionURI(region){return region===exports.defaultRegion?exports.defaultChunderRegionURI:"chunderw-vpc-gll-"+region+".twilio.com"}function createChunderEdgeURI(edge){return"voice-js."+edge+".twilio.com"}function createEventGatewayURI(region){return region?"eventgw."+region+".twilio.com":defaultEventGatewayURI}exports.createEventGatewayURI=createEventGatewayURI;function createSignalingEndpointURL(uri){return"wss://"+uri+"/signal"}exports.createSignalingEndpointURL=createSignalingEndpointURL;function getChunderURIs(edge,region,onDeprecated){if(!!region&&typeof region!=="string"){throw new errors_1.InvalidArgumentError("If `region` is provided, it must be of type `string`.")}if(!!edge&&typeof edge!=="string"&&!Array.isArray(edge)){throw new errors_1.InvalidArgumentError("If `edge` is provided, it must be of type `string` or an array of strings.")}var deprecatedMessages=[];var uris;if(region&&edge){throw new errors_1.InvalidArgumentError("You cannot specify `region` when `edge` is specified in"+"`Twilio.Device.Options`.")}else if(region){var chunderRegion=region;deprecatedMessages.push("Regions are deprecated in favor of edges. Please see this page for "+"documentation: https://www.twilio.com/docs/voice/client/edges.");var isDeprecatedRegion=Object.values(DeprecatedRegion).includes(chunderRegion);if(isDeprecatedRegion){chunderRegion=exports.deprecatedRegions[chunderRegion]}var isKnownRegion=Object.values(Region).includes(chunderRegion);if(isKnownRegion){var preferredEdge=exports.regionToEdge[chunderRegion];deprecatedMessages.push('Region "'+chunderRegion+'" is deprecated, please use `edge` '+('"'+preferredEdge+'".'))}uris=[createChunderRegionURI(chunderRegion)]}else if(edge){var edgeValues_1=Object.values(Edge);var edgeParams=Array.isArray(edge)?edge:[edge];uris=edgeParams.map(function(param){return edgeValues_1.includes(param)?createChunderRegionURI(exports.edgeToRegion[param]):createChunderEdgeURI(param)})}else{uris=[exports.defaultChunderRegionURI]}if(onDeprecated&&deprecatedMessages.length){setTimeout(function(){return onDeprecated(deprecatedMessages.join("\n"))})}return uris}exports.getChunderURIs=getChunderURIs;function getRegionShortcode(region){return exports.regionShortcodes[region]||null}exports.getRegionShortcode=getRegionShortcode},{"./errors":12}],20:[function(require,module,exports){"use strict";var XHR=require("xmlhttprequest").XMLHttpRequest;function request(method,params,callback){var options={};options.XMLHttpRequest=options.XMLHttpRequest||XHR;var xhr=new options.XMLHttpRequest;xhr.open(method,params.url,true);xhr.onreadystatechange=function onreadystatechange(){if(xhr.readyState!==4){return}if(200<=xhr.status&&xhr.status<300){callback(null,xhr.responseText);return}callback(new Error(xhr.responseText))};for(var headerName in params.headers){xhr.setRequestHeader(headerName,params.headers[headerName])}xhr.send(JSON.stringify(params.body))}var Request=request;Request.get=function get(params,callback){return new this("GET",params,callback)};Request.post=function post(params,callback){return new this("POST",params,callback)};module.exports=Request},{xmlhttprequest:2}],21:[function(require,module,exports){"use strict";var _typeof=typeof Symbol==="function"&&typeof Symbol.iterator==="symbol"?function(obj){return typeof obj}:function(obj){return obj&&typeof Symbol==="function"&&obj.constructor===Symbol&&obj!==Symbol.prototype?"symbol":typeof obj};var NotSupportedError=require("../errors").NotSupportedError;var util=require("../util");function getUserMedia(constraints,options){options=options||{};options.util=options.util||util;options.navigator=options.navigator||(typeof navigator!=="undefined"?navigator:null);return new Promise(function(resolve,reject){if(!options.navigator){throw new NotSupportedError("getUserMedia is not supported")}switch("function"){case _typeof(options.navigator.mediaDevices&&options.navigator.mediaDevices.getUserMedia):return resolve(options.navigator.mediaDevices.getUserMedia(constraints));case _typeof(options.navigator.webkitGetUserMedia):return options.navigator.webkitGetUserMedia(constraints,resolve,reject);case _typeof(options.navigator.mozGetUserMedia):return options.navigator.mozGetUserMedia(constraints,resolve,reject);case _typeof(options.navigator.getUserMedia):return options.navigator.getUserMedia(constraints,resolve,reject);default:throw new NotSupportedError("getUserMedia is not supported")}}).catch(function(e){throw options.util.isFirefox()&&e.name==="NotReadableError"?new NotSupportedError("Firefox does not currently support opening multiple audio input tracks"+"simultaneously, even across different tabs.\n"+"Related Bugzilla thread: https://bugzilla.mozilla.org/show_bug.cgi?id=1299324"):e})}module.exports=getUserMedia},{"../errors":12,"../util":35}],22:[function(require,module,exports){"use strict";Object.defineProperty(exports,"__esModule",{value:true});var IceCandidate=function(){function IceCandidate(iceCandidate,isRemote){if(isRemote===void 0){isRemote=false}this.deleted=false;var cost;var parts=iceCandidate.candidate.split("network-cost ");if(parts[1]){cost=parseInt(parts[1],10)}this.candidateType=iceCandidate.type;this.ip=iceCandidate.ip||iceCandidate.address;this.isRemote=isRemote;this.networkCost=cost;this.port=iceCandidate.port;this.priority=iceCandidate.priority;this.protocol=iceCandidate.protocol;this.relatedAddress=iceCandidate.relatedAddress;this.relatedPort=iceCandidate.relatedPort;this.tcpType=iceCandidate.tcpType;this.transportId=iceCandidate.sdpMid}IceCandidate.prototype.toPayload=function(){return{candidate_type:this.candidateType,deleted:this.deleted,ip:this.ip,is_remote:this.isRemote,"network-cost":this.networkCost,port:this.port,priority:this.priority,protocol:this.protocol,related_address:this.relatedAddress,related_port:this.relatedPort,tcp_type:this.tcpType,transport_id:this.transportId}};return IceCandidate}();exports.IceCandidate=IceCandidate},{}],23:[function(require,module,exports){"use strict";var PeerConnection=require("./peerconnection");var _require=require("./rtcpc"),test=_require.test;function enabled(){return test()}function getMediaEngine(){return typeof RTCIceGatherer!=="undefined"?"ORTC":"WebRTC"}module.exports={enabled:enabled,getMediaEngine:getMediaEngine,PeerConnection:PeerConnection}},{"./peerconnection":26,"./rtcpc":27}],24:[function(require,module,exports){var OLD_MAX_VOLUME=32767;var NativeRTCStatsReport=typeof window!=="undefined"?window.RTCStatsReport:undefined;function MockRTCStatsReport(statsMap){if(!(this instanceof MockRTCStatsReport)){return new MockRTCStatsReport(statsMap)}var self=this;Object.defineProperties(this,{size:{enumerable:true,get:function(){return self._map.size}},_map:{value:statsMap}});this[Symbol.iterator]=statsMap[Symbol.iterator]}if(NativeRTCStatsReport){MockRTCStatsReport.prototype=Object.create(NativeRTCStatsReport.prototype);MockRTCStatsReport.prototype.constructor=MockRTCStatsReport}["entries","forEach","get","has","keys","values"].forEach(function(key){MockRTCStatsReport.prototype[key]=function(){var _a;var args=[];for(var _i=0;_i=0}exports.isNonNegativeNumber=isNonNegativeNumber;exports.default={calculate:calculate,isNonNegativeNumber:isNonNegativeNumber}},{}],26:[function(require,module,exports){"use strict";var _require=require("../errors"),InvalidArgumentError=_require.InvalidArgumentError,MediaErrors=_require.MediaErrors,NotSupportedError=_require.NotSupportedError,SignalingErrors=_require.SignalingErrors;var Log=require("../log").default;var util=require("../util");var RTCPC=require("./rtcpc");var _require2=require("./sdp"),setIceAggressiveNomination=_require2.setIceAggressiveNomination;var ICE_GATHERING_TIMEOUT=15e3;var ICE_GATHERING_FAIL_NONE="none";var ICE_GATHERING_FAIL_TIMEOUT="timeout";var INITIAL_ICE_CONNECTION_STATE="new";var VOLUME_INTERVAL_MS=50;function PeerConnection(audioHelper,pstream,getUserMedia,options){if(!audioHelper||!pstream||!getUserMedia){throw new InvalidArgumentError("Audiohelper, pstream and getUserMedia are required arguments")}if(!(this instanceof PeerConnection)){return new PeerConnection(audioHelper,pstream,getUserMedia,options)}function noop(){}this.onopen=noop;this.onerror=noop;this.onclose=noop;this.ondisconnected=noop;this.onfailed=noop;this.onconnected=noop;this.onreconnected=noop;this.onsignalingstatechange=noop;this.ondtlstransportstatechange=noop;this.onicegatheringfailure=noop;this.onicegatheringstatechange=noop;this.oniceconnectionstatechange=noop;this.onpcconnectionstatechange=noop;this.onicecandidate=noop;this.onselectedcandidatepairchange=noop;this.onvolume=noop;this.version=null;this.pstream=pstream;this.stream=null;this.sinkIds=new Set(["default"]);this.outputs=new Map;this.status="connecting";this.callSid=null;this.isMuted=false;this.getUserMedia=getUserMedia;var AudioContext=typeof window!=="undefined"&&(window.AudioContext||window.webkitAudioContext);this._isSinkSupported=!!AudioContext&&typeof HTMLAudioElement!=="undefined"&&HTMLAudioElement.prototype.setSinkId;this._audioContext=AudioContext&&audioHelper._audioContext;this._hasIceCandidates=false;this._hasIceGatheringFailures=false;this._iceGatheringTimeoutId=null;this._masterAudio=null;this._masterAudioDeviceId=null;this._mediaStreamSource=null;this._dtmfSender=null;this._dtmfSenderUnsupported=false;this._callEvents=[];this._nextTimeToPublish=Date.now();this._onAnswerOrRinging=noop;this._onHangup=noop;this._remoteStream=null;this._shouldManageStream=true;this._iceState=INITIAL_ICE_CONNECTION_STATE;this._isUnifiedPlan=options.isUnifiedPlan;this.options=options=options||{};this.navigator=options.navigator||(typeof navigator!=="undefined"?navigator:null);this.util=options.util||util;this.codecPreferences=options.codecPreferences;this._log=Log.getInstance();return this}PeerConnection.prototype.uri=function(){return this._uri};PeerConnection.prototype.openWithConstraints=function(constraints){return this.getUserMedia({audio:constraints}).then(this._setInputTracksFromStream.bind(this,false))};PeerConnection.prototype.setInputTracksFromStream=function(stream){var self=this;return this._setInputTracksFromStream(true,stream).then(function(){self._shouldManageStream=false})};PeerConnection.prototype._createAnalyser=function(audioContext,options){options=Object.assign({fftSize:32,smoothingTimeConstant:.3},options);var analyser=audioContext.createAnalyser();for(var field in options){analyser[field]=options[field]}return analyser};PeerConnection.prototype._setVolumeHandler=function(handler){this.onvolume=handler};PeerConnection.prototype._startPollingVolume=function(){if(!this._audioContext||!this.stream||!this._remoteStream){return}var audioContext=this._audioContext;var inputAnalyser=this._inputAnalyser=this._createAnalyser(audioContext);var inputBufferLength=inputAnalyser.frequencyBinCount;var inputDataArray=new Uint8Array(inputBufferLength);this._inputAnalyser2=this._createAnalyser(audioContext,{minDecibels:-127,maxDecibels:0,smoothingTimeConstant:0});var outputAnalyser=this._outputAnalyser=this._createAnalyser(audioContext);var outputBufferLength=outputAnalyser.frequencyBinCount;var outputDataArray=new Uint8Array(outputBufferLength);this._outputAnalyser2=this._createAnalyser(audioContext,{minDecibels:-127,maxDecibels:0,smoothingTimeConstant:0});this._updateInputStreamSource(this.stream);this._updateOutputStreamSource(this._remoteStream);var self=this;setTimeout(function emitVolume(){if(!self._audioContext){return}else if(self.status==="closed"){self._inputAnalyser.disconnect();self._outputAnalyser.disconnect();self._inputAnalyser2.disconnect();self._outputAnalyser2.disconnect();return}self._inputAnalyser.getByteFrequencyData(inputDataArray);var inputVolume=self.util.average(inputDataArray);self._inputAnalyser2.getByteFrequencyData(inputDataArray);var inputVolume2=self.util.average(inputDataArray);self._outputAnalyser.getByteFrequencyData(outputDataArray);var outputVolume=self.util.average(outputDataArray);self._outputAnalyser2.getByteFrequencyData(outputDataArray);var outputVolume2=self.util.average(outputDataArray);self.onvolume(inputVolume/255,outputVolume/255,inputVolume2,outputVolume2);setTimeout(emitVolume,VOLUME_INTERVAL_MS)},VOLUME_INTERVAL_MS)};PeerConnection.prototype._stopStream=function _stopStream(stream){if(!this._shouldManageStream){return}if(typeof MediaStreamTrack.prototype.stop==="function"){var audioTracks=typeof stream.getAudioTracks==="function"?stream.getAudioTracks():stream.audioTracks;audioTracks.forEach(function(track){track.stop()})}else{stream.stop()}};PeerConnection.prototype._updateInputStreamSource=function(stream){if(this._inputStreamSource){this._inputStreamSource.disconnect()}this._inputStreamSource=this._audioContext.createMediaStreamSource(stream);this._inputStreamSource.connect(this._inputAnalyser);this._inputStreamSource.connect(this._inputAnalyser2)};PeerConnection.prototype._updateOutputStreamSource=function(stream){if(this._outputStreamSource){this._outputStreamSource.disconnect()}this._outputStreamSource=this._audioContext.createMediaStreamSource(stream);this._outputStreamSource.connect(this._outputAnalyser);this._outputStreamSource.connect(this._outputAnalyser2)};PeerConnection.prototype._setInputTracksFromStream=function(shouldClone,newStream){return this._isUnifiedPlan?this._setInputTracksForUnifiedPlan(shouldClone,newStream):this._setInputTracksForPlanB(shouldClone,newStream)};PeerConnection.prototype._setInputTracksForPlanB=function(shouldClone,newStream){var _this=this;if(!newStream){return Promise.reject(new InvalidArgumentError("Can not set input stream to null while in a call"))}if(!newStream.getAudioTracks().length){return Promise.reject(new InvalidArgumentError("Supplied input stream has no audio tracks"))}var localStream=this.stream;if(!localStream){this.stream=shouldClone?cloneStream(newStream):newStream}else{this._stopStream(localStream);removeStream(this.version.pc,localStream);localStream.getAudioTracks().forEach(localStream.removeTrack,localStream);newStream.getAudioTracks().forEach(localStream.addTrack,localStream);addStream(this.version.pc,newStream);this._updateInputStreamSource(this.stream)}this.mute(this.isMuted);if(!this.version){return Promise.resolve(this.stream)}return new Promise(function(resolve,reject){_this.version.createOffer(_this.options.maxAverageBitrate,_this.codecPreferences,{audio:true},function(){_this.version.processAnswer(_this.codecPreferences,_this._answerSdp,function(){resolve(_this.stream)},reject)},reject)})};PeerConnection.prototype._setInputTracksForUnifiedPlan=function(shouldClone,newStream){var _this2=this;if(!newStream){return Promise.reject(new InvalidArgumentError("Can not set input stream to null while in a call"))}if(!newStream.getAudioTracks().length){return Promise.reject(new InvalidArgumentError("Supplied input stream has no audio tracks"))}var localStream=this.stream;var getStreamPromise=function getStreamPromise(){_this2.mute(_this2.isMuted);return Promise.resolve(_this2.stream)};if(!localStream){this.stream=shouldClone?cloneStream(newStream):newStream}else{if(this._shouldManageStream){this._stopStream(localStream)}if(!this._sender){this._sender=this.version.pc.getSenders()[0]}return this._sender.replaceTrack(newStream.getAudioTracks()[0]).then(function(){_this2._updateInputStreamSource(newStream);return getStreamPromise()})}return getStreamPromise()};PeerConnection.prototype._onInputDevicesChanged=function(){if(!this.stream){return}var activeInputWasLost=this.stream.getAudioTracks().every(function(track){return track.readyState==="ended"});if(activeInputWasLost&&this._shouldManageStream){this.openWithConstraints(true)}};PeerConnection.prototype._onIceGatheringFailure=function(type){this._hasIceGatheringFailures=true;this.onicegatheringfailure(type)};PeerConnection.prototype._onMediaConnectionStateChange=function(newState){var previousState=this._iceState;if(previousState===newState||newState!=="connected"&&newState!=="disconnected"&&newState!=="failed"){return}this._iceState=newState;var message=void 0;switch(newState){case"connected":if(previousState==="disconnected"||previousState==="failed"){message="ICE liveliness check succeeded. Connection with Twilio restored";this._log.info(message);this.onreconnected(message)}else{message="Media connection established.";this._log.info(message);this.onconnected(message)}this._stopIceGatheringTimeout();this._hasIceGatheringFailures=false;break;case"disconnected":message="ICE liveliness check failed. May be having trouble connecting to Twilio";this._log.info(message);this.ondisconnected(message);break;case"failed":message="Connection with Twilio was interrupted.";this._log.info(message);this.onfailed(message);break}};PeerConnection.prototype._setSinkIds=function(sinkIds){if(!this._isSinkSupported){return Promise.reject(new NotSupportedError("Audio output selection is not supported by this browser"))}this.sinkIds=new Set(sinkIds.forEach?sinkIds:[sinkIds]);return this.version?this._updateAudioOutputs():Promise.resolve()};PeerConnection.prototype._startIceGatheringTimeout=function startIceGatheringTimeout(){var _this3=this;this._stopIceGatheringTimeout();this._iceGatheringTimeoutId=setTimeout(function(){_this3._onIceGatheringFailure(ICE_GATHERING_FAIL_TIMEOUT)},ICE_GATHERING_TIMEOUT)};PeerConnection.prototype._stopIceGatheringTimeout=function stopIceGatheringTimeout(){clearInterval(this._iceGatheringTimeoutId)};PeerConnection.prototype._updateAudioOutputs=function updateAudioOutputs(){var addedOutputIds=Array.from(this.sinkIds).filter(function(id){return!this.outputs.has(id)},this);var removedOutputIds=Array.from(this.outputs.keys()).filter(function(id){return!this.sinkIds.has(id)},this);var self=this;var createOutputPromises=addedOutputIds.map(this._createAudioOutput,this);return Promise.all(createOutputPromises).then(function(){return Promise.all(removedOutputIds.map(self._removeAudioOutput,self))})};PeerConnection.prototype._createAudio=function createAudio(arr){return new Audio(arr)};PeerConnection.prototype._createAudioOutput=function createAudioOutput(id){var dest=this._audioContext.createMediaStreamDestination();this._mediaStreamSource.connect(dest);var audio=this._createAudio();setAudioSource(audio,dest.stream);var self=this;return audio.setSinkId(id).then(function(){return audio.play()}).then(function(){self.outputs.set(id,{audio:audio,dest:dest})})};PeerConnection.prototype._removeAudioOutputs=function removeAudioOutputs(){if(this._masterAudio&&typeof this._masterAudioDeviceId!=="undefined"){this._disableOutput(this,this._masterAudioDeviceId);this.outputs.delete(this._masterAudioDeviceId);this._masterAudioDeviceId=null;if(!this._masterAudio.paused){this._masterAudio.pause()}if(typeof this._masterAudio.srcObject!=="undefined"){this._masterAudio.srcObject=null}else{this._masterAudio.src=""}this._masterAudio=null}return Array.from(this.outputs.keys()).map(this._removeAudioOutput,this)};PeerConnection.prototype._disableOutput=function disableOutput(pc,id){var output=pc.outputs.get(id);if(!output){return}if(output.audio){output.audio.pause();output.audio.src=""}if(output.dest){output.dest.disconnect()}};PeerConnection.prototype._reassignMasterOutput=function reassignMasterOutput(pc,masterId){var masterOutput=pc.outputs.get(masterId);pc.outputs.delete(masterId);var self=this;var idToReplace=Array.from(pc.outputs.keys())[0]||"default";return masterOutput.audio.setSinkId(idToReplace).then(function(){self._disableOutput(pc,idToReplace);pc.outputs.set(idToReplace,masterOutput);pc._masterAudioDeviceId=idToReplace}).catch(function rollback(){pc.outputs.set(masterId,masterOutput);self._log.info("Could not reassign master output. Attempted to roll back.")})};PeerConnection.prototype._removeAudioOutput=function removeAudioOutput(id){if(this._masterAudioDeviceId===id){return this._reassignMasterOutput(this,id)}this._disableOutput(this,id);this.outputs.delete(id);return Promise.resolve()};PeerConnection.prototype._onAddTrack=function onAddTrack(pc,stream){var audio=pc._masterAudio=this._createAudio();setAudioSource(audio,stream);audio.play();var deviceId=Array.from(pc.outputs.keys())[0]||"default";pc._masterAudioDeviceId=deviceId;pc.outputs.set(deviceId,{audio:audio});pc._mediaStreamSource=pc._audioContext.createMediaStreamSource(stream);pc.pcStream=stream;pc._updateAudioOutputs()};PeerConnection.prototype._fallbackOnAddTrack=function fallbackOnAddTrack(pc,stream){var audio=document&&document.createElement("audio");audio.autoplay=true;if(!setAudioSource(audio,stream)){pc._log.info("Error attaching stream to element.")}pc.outputs.set("default",{audio:audio})};PeerConnection.prototype._setEncodingParameters=function(enableDscp){if(!enableDscp||!this._sender||typeof this._sender.getParameters!=="function"||typeof this._sender.setParameters!=="function"){return}var params=this._sender.getParameters();if(!params.priority&&!(params.encodings&¶ms.encodings.length)){return}params.priority="high";if(params.encodings&¶ms.encodings.length){params.encodings.forEach(function(encoding){encoding.priority="high";encoding.networkPriority="high"})}this._sender.setParameters(params)};PeerConnection.prototype._setupPeerConnection=function(rtcConstraints,rtcConfiguration){var _this4=this;var self=this;var version=new(this.options.rtcpcFactory||RTCPC);version.create(rtcConstraints,rtcConfiguration);addStream(version.pc,this.stream);var eventName="ontrack"in version.pc?"ontrack":"onaddstream";version.pc[eventName]=function(event){var stream=self._remoteStream=event.stream||event.streams[0];if(typeof version.pc.getSenders==="function"){_this4._sender=version.pc.getSenders()[0]}if(self._isSinkSupported){self._onAddTrack(self,stream)}else{self._fallbackOnAddTrack(self,stream)}self._startPollingVolume()};return version};PeerConnection.prototype._maybeSetIceAggressiveNomination=function(sdp){return this.options.forceAggressiveIceNomination?setIceAggressiveNomination(sdp):sdp};PeerConnection.prototype._setupChannel=function(){var _this5=this;var pc=this.version.pc;this.version.pc.onopen=function(){_this5.status="open";_this5.onopen()};this.version.pc.onstatechange=function(){if(_this5.version.pc&&_this5.version.pc.readyState==="stable"){_this5.status="open";_this5.onopen()}};this.version.pc.onsignalingstatechange=function(){var state=pc.signalingState;_this5._log.info('signalingState is "'+state+'"');if(_this5.version.pc&&_this5.version.pc.signalingState==="stable"){_this5.status="open";_this5.onopen()}_this5.onsignalingstatechange(pc.signalingState)};pc.onconnectionstatechange=function(){_this5._log.info('pc.connectionState is "'+pc.connectionState+'"');_this5.onpcconnectionstatechange(pc.connectionState);_this5._onMediaConnectionStateChange(pc.connectionState)};pc.onicecandidate=function(event){var candidate=event.candidate;if(candidate){_this5._hasIceCandidates=true;_this5.onicecandidate(candidate);_this5._setupRTCIceTransportListener()}_this5._log.info("ICE Candidate: "+JSON.stringify(candidate))};pc.onicegatheringstatechange=function(){var state=pc.iceGatheringState;if(state==="gathering"){_this5._startIceGatheringTimeout()}else if(state==="complete"){_this5._stopIceGatheringTimeout();if(!_this5._hasIceCandidates){_this5._onIceGatheringFailure(ICE_GATHERING_FAIL_NONE)}if(_this5._hasIceCandidates&&_this5._hasIceGatheringFailures){_this5._startIceGatheringTimeout()}}_this5._log.info('pc.iceGatheringState is "'+pc.iceGatheringState+'"');_this5.onicegatheringstatechange(state)};pc.oniceconnectionstatechange=function(){_this5._log.info('pc.iceConnectionState is "'+pc.iceConnectionState+'"');_this5.oniceconnectionstatechange(pc.iceConnectionState);_this5._onMediaConnectionStateChange(pc.iceConnectionState)}};PeerConnection.prototype._initializeMediaStream=function(rtcConstraints,rtcConfiguration){if(this.status==="open"){return false}if(this.pstream.status==="disconnected"){this.onerror({info:{code:31e3,message:"Cannot establish connection. Client is disconnected",twilioError:new SignalingErrors.ConnectionDisconnected}});this.close();return false}this.version=this._setupPeerConnection(rtcConstraints,rtcConfiguration);this._setupChannel();return true};PeerConnection.prototype._removeReconnectionListeners=function(){if(this.pstream){this.pstream.removeListener("answer",this._onAnswerOrRinging);this.pstream.removeListener("hangup",this._onHangup)}};PeerConnection.prototype._setupRTCDtlsTransportListener=function(){var _this6=this;var dtlsTransport=this.getRTCDtlsTransport();if(!dtlsTransport||dtlsTransport.onstatechange){return}var handler=function handler(){_this6._log.info('dtlsTransportState is "'+dtlsTransport.state+'"');_this6.ondtlstransportstatechange(dtlsTransport.state)};handler();dtlsTransport.onstatechange=handler};PeerConnection.prototype._setupRTCIceTransportListener=function(){var _this7=this;var iceTransport=this._getRTCIceTransport();if(!iceTransport||iceTransport.onselectedcandidatepairchange){return}iceTransport.onselectedcandidatepairchange=function(){return _this7.onselectedcandidatepairchange(iceTransport.getSelectedCandidatePair())}};PeerConnection.prototype.iceRestart=function(){var _this8=this;this._log.info("Attempting to restart ICE...");this._hasIceCandidates=false;this.version.createOffer(this.options.maxAverageBitrate,this.codecPreferences,{iceRestart:true}).then(function(){_this8._removeReconnectionListeners();_this8._onAnswerOrRinging=function(payload){_this8._removeReconnectionListeners();if(!payload.sdp||_this8.version.pc.signalingState!=="have-local-offer"){var message="Invalid state or param during ICE Restart:"+("hasSdp:"+!!payload.sdp+", signalingState:"+_this8.version.pc.signalingState);_this8._log.info(message);return}var sdp=_this8._maybeSetIceAggressiveNomination(payload.sdp);_this8._answerSdp=sdp;if(_this8.status!=="closed"){_this8.version.processAnswer(_this8.codecPreferences,sdp,null,function(err){var message=err&&err.message?err.message:err;_this8._log.info("Failed to process answer during ICE Restart. Error: "+message)})}};_this8._onHangup=function(){_this8._log.info("Received hangup during ICE Restart");_this8._removeReconnectionListeners()};_this8.pstream.on("answer",_this8._onAnswerOrRinging);_this8.pstream.on("hangup",_this8._onHangup);_this8.pstream.reinvite(_this8.version.getSDP(),_this8.callSid)}).catch(function(err){var message=err&&err.message?err.message:err;_this8._log.info("Failed to createOffer during ICE Restart. Error: "+message);_this8.onfailed(message)})};PeerConnection.prototype.makeOutgoingCall=function(token,params,callsid,rtcConstraints,rtcConfiguration,onMediaStarted){var _this9=this;if(!this._initializeMediaStream(rtcConstraints,rtcConfiguration)){return}var self=this;this.callSid=callsid;function onAnswerSuccess(){if(self.options){self._setEncodingParameters(self.options.dscp)}onMediaStarted(self.version.pc)}function onAnswerError(err){var errMsg=err.message||err;self.onerror({info:{code:31e3,message:"Error processing answer: "+errMsg,twilioError:new MediaErrors.ClientRemoteDescFailed}})}this._onAnswerOrRinging=function(payload){if(!payload.sdp){return}var sdp=_this9._maybeSetIceAggressiveNomination(payload.sdp);self._answerSdp=sdp;if(self.status!=="closed"){self.version.processAnswer(_this9.codecPreferences,sdp,onAnswerSuccess,onAnswerError)}self.pstream.removeListener("answer",self._onAnswerOrRinging);self.pstream.removeListener("ringing",self._onAnswerOrRinging)};this.pstream.on("answer",this._onAnswerOrRinging);this.pstream.on("ringing",this._onAnswerOrRinging);function onOfferSuccess(){if(self.status!=="closed"){self.pstream.invite(self.version.getSDP(),self.callSid,self.options.preflight,params);self._setupRTCDtlsTransportListener()}}function onOfferError(err){var errMsg=err.message||err;self.onerror({info:{code:31e3,message:"Error creating the offer: "+errMsg,twilioError:new MediaErrors.ClientLocalDescFailed}})}this.version.createOffer(this.options.maxAverageBitrate,this.codecPreferences,{audio:true},onOfferSuccess,onOfferError)};PeerConnection.prototype.answerIncomingCall=function(callSid,sdp,rtcConstraints,rtcConfiguration,onMediaStarted){if(!this._initializeMediaStream(rtcConstraints,rtcConfiguration)){return}sdp=this._maybeSetIceAggressiveNomination(sdp);this._answerSdp=sdp.replace(/^a=setup:actpass$/gm,"a=setup:passive");this.callSid=callSid;var self=this;function onAnswerSuccess(){if(self.status!=="closed"){self.pstream.answer(self.version.getSDP(),callSid);if(self.options){self._setEncodingParameters(self.options.dscp)}onMediaStarted(self.version.pc);self._setupRTCDtlsTransportListener()}}function onAnswerError(err){var errMsg=err.message||err;self.onerror({info:{code:31e3,message:"Error creating the answer: "+errMsg,twilioError:new MediaErrors.ClientRemoteDescFailed}})}this.version.processSDP(this.options.maxAverageBitrate,this.codecPreferences,sdp,{audio:true},onAnswerSuccess,onAnswerError)};PeerConnection.prototype.close=function(){if(this.version&&this.version.pc){if(this.version.pc.signalingState!=="closed"){this.version.pc.close()}this.version.pc=null}if(this.stream){this.mute(false);this._stopStream(this.stream)}this.stream=null;this._removeReconnectionListeners();this._stopIceGatheringTimeout();Promise.all(this._removeAudioOutputs()).catch(function(){});if(this._mediaStreamSource){this._mediaStreamSource.disconnect()}if(this._inputAnalyser){this._inputAnalyser.disconnect()}if(this._outputAnalyser){this._outputAnalyser.disconnect()}if(this._inputAnalyser2){this._inputAnalyser2.disconnect()}if(this._outputAnalyser2){this._outputAnalyser2.disconnect()}this.status="closed";this.onclose()};PeerConnection.prototype.reject=function(callSid){this.callSid=callSid};PeerConnection.prototype.ignore=function(callSid){this.callSid=callSid};PeerConnection.prototype.mute=function(shouldMute){this.isMuted=shouldMute;if(!this.stream){return}if(this._sender&&this._sender.track){this._sender.track.enabled=!shouldMute}else{var audioTracks=typeof this.stream.getAudioTracks==="function"?this.stream.getAudioTracks():this.stream.audioTracks;audioTracks.forEach(function(track){track.enabled=!shouldMute})}};PeerConnection.prototype.getOrCreateDTMFSender=function getOrCreateDTMFSender(){if(this._dtmfSender||this._dtmfSenderUnsupported){return this._dtmfSender||null}var self=this;var pc=this.version.pc;if(!pc){this._log.info("No RTCPeerConnection available to call createDTMFSender on");return null}if(typeof pc.getSenders==="function"&&(typeof RTCDTMFSender==="function"||typeof RTCDtmfSender==="function")){var chosenSender=pc.getSenders().find(function(sender){return sender.dtmf});if(chosenSender){this._log.info("Using RTCRtpSender#dtmf");this._dtmfSender=chosenSender.dtmf;return this._dtmfSender}}if(typeof pc.createDTMFSender==="function"&&typeof pc.getLocalStreams==="function"){var track=pc.getLocalStreams().map(function(stream){var tracks=self._getAudioTracks(stream);return tracks&&tracks[0]})[0];if(!track){this._log.info("No local audio MediaStreamTrack available on the RTCPeerConnection to pass to createDTMFSender");return null}this._log.info("Creating RTCDTMFSender");this._dtmfSender=pc.createDTMFSender(track);return this._dtmfSender}this._log.info("RTCPeerConnection does not support RTCDTMFSender");this._dtmfSenderUnsupported=true;return null};PeerConnection.prototype.getRTCDtlsTransport=function getRTCDtlsTransport(){var sender=this.version&&this.version.pc&&typeof this.version.pc.getSenders==="function"&&this.version.pc.getSenders()[0];return sender&&sender.transport||null};PeerConnection.prototype._canStopMediaStreamTrack=function(){return typeof MediaStreamTrack.prototype.stop==="function"};PeerConnection.prototype._getAudioTracks=function(stream){return typeof stream.getAudioTracks==="function"?stream.getAudioTracks():stream.audioTracks};PeerConnection.prototype._getRTCIceTransport=function _getRTCIceTransport(){var dtlsTransport=this.getRTCDtlsTransport();return dtlsTransport&&dtlsTransport.iceTransport||null};PeerConnection.protocol=function(){return RTCPC.test()?new RTCPC:null}();function addStream(pc,stream){if(typeof pc.addTrack==="function"){stream.getAudioTracks().forEach(function(track){pc.addTrack(track,stream)})}else{pc.addStream(stream)}}function cloneStream(oldStream){var newStream=typeof MediaStream!=="undefined"?new MediaStream:new webkitMediaStream;oldStream.getAudioTracks().forEach(newStream.addTrack,newStream);return newStream}function removeStream(pc,stream){if(typeof pc.removeTrack==="function"){pc.getSenders().forEach(function(sender){pc.removeTrack(sender)})}else{pc.removeStream(stream)}}function setAudioSource(audio,stream){if(typeof audio.srcObject!=="undefined"){audio.srcObject=stream}else if(typeof audio.mozSrcObject!=="undefined"){audio.mozSrcObject=stream}else if(typeof audio.src!=="undefined"){var _window=audio.options.window||window;audio.src=(_window.URL||_window.webkitURL).createObjectURL(stream)}else{return false}return true}PeerConnection.enabled=RTCPC.test();module.exports=PeerConnection},{"../errors":12,"../log":15,"../util":35,"./rtcpc":27,"./sdp":28}],27:[function(require,module,exports){(function(global){(function(){"use strict";var _typeof=typeof Symbol==="function"&&typeof Symbol.iterator==="symbol"?function(obj){return typeof obj}:function(obj){return obj&&typeof Symbol==="function"&&obj.constructor===Symbol&&obj!==Symbol.prototype?"symbol":typeof obj};var RTCPeerConnectionShim=require("rtcpeerconnection-shim");var Log=require("../log").default;var _require=require("./sdp"),setCodecPreferences=_require.setCodecPreferences,setMaxAverageBitrate=_require.setMaxAverageBitrate;var util=require("../util");function RTCPC(){if(typeof window==="undefined"){this.log.info("No RTCPeerConnection implementation available. The window object was not found.");return}if(util.isLegacyEdge()){this.RTCPeerConnection=new RTCPeerConnectionShim(typeof window!=="undefined"?window:global)}else if(typeof window.RTCPeerConnection==="function"){this.RTCPeerConnection=window.RTCPeerConnection}else if(typeof window.webkitRTCPeerConnection==="function"){this.RTCPeerConnection=webkitRTCPeerConnection}else if(typeof window.mozRTCPeerConnection==="function"){this.RTCPeerConnection=mozRTCPeerConnection;window.RTCSessionDescription=mozRTCSessionDescription;window.RTCIceCandidate=mozRTCIceCandidate}else{this.log.info("No RTCPeerConnection implementation available")}}RTCPC.prototype.create=function(rtcConstraints,rtcConfiguration){this.log=Log.getInstance();this.pc=new this.RTCPeerConnection(rtcConfiguration,rtcConstraints)};RTCPC.prototype.createModernConstraints=function(c){if(typeof c==="undefined"){return null}var nc=Object.assign({},c);if(typeof webkitRTCPeerConnection!=="undefined"&&!util.isLegacyEdge()){nc.mandatory={};if(typeof c.audio!=="undefined"){nc.mandatory.OfferToReceiveAudio=c.audio}if(typeof c.video!=="undefined"){nc.mandatory.OfferToReceiveVideo=c.video}}else{if(typeof c.audio!=="undefined"){nc.offerToReceiveAudio=c.audio}if(typeof c.video!=="undefined"){nc.offerToReceiveVideo=c.video}}delete nc.audio;delete nc.video;return nc};RTCPC.prototype.createOffer=function(maxAverageBitrate,codecPreferences,constraints,onSuccess,onError){var _this=this;constraints=this.createModernConstraints(constraints);return promisifyCreate(this.pc.createOffer,this.pc)(constraints).then(function(offer){if(!_this.pc){return Promise.resolve()}var sdp=setMaxAverageBitrate(offer.sdp,maxAverageBitrate);return promisifySet(_this.pc.setLocalDescription,_this.pc)(new RTCSessionDescription({type:"offer",sdp:setCodecPreferences(sdp,codecPreferences)}))}).then(onSuccess,onError)};RTCPC.prototype.createAnswer=function(maxAverageBitrate,codecPreferences,constraints,onSuccess,onError){var _this2=this;constraints=this.createModernConstraints(constraints);return promisifyCreate(this.pc.createAnswer,this.pc)(constraints).then(function(answer){if(!_this2.pc){return Promise.resolve()}var sdp=setMaxAverageBitrate(answer.sdp,maxAverageBitrate);return promisifySet(_this2.pc.setLocalDescription,_this2.pc)(new RTCSessionDescription({type:"answer",sdp:setCodecPreferences(sdp,codecPreferences)}))}).then(onSuccess,onError)};RTCPC.prototype.processSDP=function(maxAverageBitrate,codecPreferences,sdp,constraints,onSuccess,onError){var _this3=this;sdp=setCodecPreferences(sdp,codecPreferences);var desc=new RTCSessionDescription({sdp:sdp,type:"offer"});return promisifySet(this.pc.setRemoteDescription,this.pc)(desc).then(function(){_this3.createAnswer(maxAverageBitrate,codecPreferences,constraints,onSuccess,onError)})};RTCPC.prototype.getSDP=function(){return this.pc.localDescription.sdp};RTCPC.prototype.processAnswer=function(codecPreferences,sdp,onSuccess,onError){if(!this.pc){return Promise.resolve()}sdp=setCodecPreferences(sdp,codecPreferences);return promisifySet(this.pc.setRemoteDescription,this.pc)(new RTCSessionDescription({sdp:sdp,type:"answer"})).then(onSuccess,onError)};RTCPC.test=function(){if((typeof navigator==="undefined"?"undefined":_typeof(navigator))==="object"){var getUserMedia=navigator.mediaDevices&&navigator.mediaDevices.getUserMedia||navigator.webkitGetUserMedia||navigator.mozGetUserMedia||navigator.getUserMedia;if(util.isLegacyEdge(navigator)){return false}if(getUserMedia&&typeof window.RTCPeerConnection==="function"){return true}else if(getUserMedia&&typeof window.webkitRTCPeerConnection==="function"){return true}else if(getUserMedia&&typeof window.mozRTCPeerConnection==="function"){try{var test=new window.mozRTCPeerConnection;if(typeof test.getLocalStreams!=="function")return false}catch(e){return false}return true}else if(typeof RTCIceGatherer!=="undefined"){return true}}return false};function promisify(fn,ctx,areCallbacksFirst){return function(){var args=Array.prototype.slice.call(arguments);return new Promise(function(resolve){resolve(fn.apply(ctx,args))}).catch(function(){return new Promise(function(resolve,reject){fn.apply(ctx,areCallbacksFirst?[resolve,reject].concat(args):args.concat([resolve,reject]))})})}}function promisifyCreate(fn,ctx){return promisify(fn,ctx,true)}function promisifySet(fn,ctx){return promisify(fn,ctx,false)}module.exports=RTCPC}).call(this)}).call(this,typeof global!=="undefined"?global:typeof self!=="undefined"?self:typeof window!=="undefined"?window:{})},{"../log":15,"../util":35,"./sdp":28,"rtcpeerconnection-shim":61}],28:[function(require,module,exports){"use strict";var _slicedToArray=function(){function sliceIterator(arr,i){var _arr=[];var _n=true;var _d=false;var _e=undefined;try{for(var _i=arr[Symbol.iterator](),_s;!(_n=(_s=_i.next()).done);_n=true){_arr.push(_s.value);if(i&&_arr.length===i)break}}catch(err){_d=true;_e=err}finally{try{if(!_n&&_i["return"])_i["return"]()}finally{if(_d)throw _e}}return _arr}return function(arr,i){if(Array.isArray(arr)){return arr}else if(Symbol.iterator in Object(arr)){return sliceIterator(arr,i)}else{throw new TypeError("Invalid attempt to destructure non-iterable instance")}}}();var util=require("../util");var ptToFixedBitrateAudioCodecName={0:"PCMU",8:"PCMA"};var defaultOpusId=111;var BITRATE_MAX=51e4;var BITRATE_MIN=6e3;function getPreferredCodecInfo(sdp){var _ref=/a=rtpmap:(\d+) (\S+)/m.exec(sdp)||[null,"",""],_ref2=_slicedToArray(_ref,3),codecId=_ref2[1],codecName=_ref2[2];var regex=new RegExp("a=fmtp:"+codecId+" (\\S+)","m");var _ref3=regex.exec(sdp)||[null,""],_ref4=_slicedToArray(_ref3,2),codecParams=_ref4[1];return{codecName:codecName,codecParams:codecParams}}function setIceAggressiveNomination(sdp){if(!util.isChrome(window,window.navigator)){return sdp}return sdp.split("\n").filter(function(line){return line.indexOf("a=ice-lite")===-1}).join("\n")}function setMaxAverageBitrate(sdp,maxAverageBitrate){if(typeof maxAverageBitrate!=="number"||maxAverageBitrateBITRATE_MAX){return sdp}var matches=/a=rtpmap:(\d+) opus/m.exec(sdp);var opusId=matches&&matches.length?matches[1]:defaultOpusId;var regex=new RegExp("a=fmtp:"+opusId);var lines=sdp.split("\n").map(function(line){return regex.test(line)?line+(";maxaveragebitrate="+maxAverageBitrate):line});return lines.join("\n")}function setCodecPreferences(sdp,preferredCodecs){var mediaSections=getMediaSections(sdp);var session=sdp.split("\r\nm=")[0];return[session].concat(mediaSections.map(function(section){if(!/^m=(audio|video)/.test(section)){return section}var kind=section.match(/^m=(audio|video)/)[1];var codecMap=createCodecMapForMediaSection(section);var payloadTypes=getReorderedPayloadTypes(codecMap,preferredCodecs);var newSection=setPayloadTypesInMediaSection(payloadTypes,section);var pcmaPayloadTypes=codecMap.get("pcma")||[];var pcmuPayloadTypes=codecMap.get("pcmu")||[];var fixedBitratePayloadTypes=kind==="audio"?new Set(pcmaPayloadTypes.concat(pcmuPayloadTypes)):new Set;return fixedBitratePayloadTypes.has(payloadTypes[0])?newSection.replace(/\r\nb=(AS|TIAS):([0-9]+)/g,""):newSection})).join("\r\n")}function getMediaSections(sdp,kind,direction){return sdp.replace(/\r\n\r\n$/,"\r\n").split("\r\nm=").slice(1).map(function(mediaSection){return"m="+mediaSection}).filter(function(mediaSection){var kindPattern=new RegExp("m="+(kind||".*"),"gm");var directionPattern=new RegExp("a="+(direction||".*"),"gm");return kindPattern.test(mediaSection)&&directionPattern.test(mediaSection)})}function createCodecMapForMediaSection(section){return Array.from(createPtToCodecName(section)).reduce(function(codecMap,pair){var pt=pair[0];var codecName=pair[1];var pts=codecMap.get(codecName)||[];return codecMap.set(codecName,pts.concat(pt))},new Map)}function getReorderedPayloadTypes(codecMap,preferredCodecs){preferredCodecs=preferredCodecs.map(function(codecName){return codecName.toLowerCase()});var preferredPayloadTypes=util.flatMap(preferredCodecs,function(codecName){return codecMap.get(codecName)||[]});var remainingCodecs=util.difference(Array.from(codecMap.keys()),preferredCodecs);var remainingPayloadTypes=util.flatMap(remainingCodecs,function(codecName){return codecMap.get(codecName)});return preferredPayloadTypes.concat(remainingPayloadTypes)}function setPayloadTypesInMediaSection(payloadTypes,section){var lines=section.split("\r\n");var mLine=lines[0];var otherLines=lines.slice(1);mLine=mLine.replace(/([0-9]+\s?)+$/,payloadTypes.join(" "));return[mLine].concat(otherLines).join("\r\n")}function createPtToCodecName(mediaSection){return getPayloadTypesInMediaSection(mediaSection).reduce(function(ptToCodecName,pt){var rtpmapPattern=new RegExp("a=rtpmap:"+pt+" ([^/]+)");var matches=mediaSection.match(rtpmapPattern);var codecName=matches?matches[1].toLowerCase():ptToFixedBitrateAudioCodecName[pt]?ptToFixedBitrateAudioCodecName[pt].toLowerCase():"";return ptToCodecName.set(pt,codecName)},new Map)}function getPayloadTypesInMediaSection(section){var mLine=section.split("\r\n")[0];var matches=mLine.match(/([0-9]+)/g);if(!matches){return[]}return matches.slice(1).map(function(match){return parseInt(match,10)})}module.exports={getPreferredCodecInfo:getPreferredCodecInfo,setCodecPreferences:setCodecPreferences,setIceAggressiveNomination:setIceAggressiveNomination,setMaxAverageBitrate:setMaxAverageBitrate}},{"../util":35}],29:[function(require,module,exports){var __spreadArrays=this&&this.__spreadArrays||function(){for(var s=0,i=0,il=arguments.length;i0}function sampleDevices(mediaDevices){nativeMediaDevices.enumerateDevices().then(function(newDevices){var knownDevices=mediaDevices._knownDevices;var oldDevices=knownDevices.slice();[].splice.apply(knownDevices,[0,knownDevices.length].concat(newDevices.sort(sortDevicesById)));if(!mediaDevices._deviceChangeIsNative&&devicesHaveChanged(knownDevices,oldDevices)){mediaDevices.dispatchEvent(new Event("devicechange"))}if(!mediaDevices._deviceInfoChangeIsNative&&deviceInfosHaveChanged(knownDevices,oldDevices)){mediaDevices.dispatchEvent(new Event("deviceinfochange"))}})}function propertyHasChanged(propertyName,as,bs){return as.some(function(a,i){return a[propertyName]!==bs[i][propertyName]})}function reemitNativeEvent(mediaDevices,eventName){var methodName="on"+eventName;function dispatchEvent(event){mediaDevices.dispatchEvent(event)}if(methodName in nativeMediaDevices){if("addEventListener"in nativeMediaDevices){nativeMediaDevices.addEventListener(eventName,dispatchEvent)}else{nativeMediaDevices[methodName]=dispatchEvent}return true}return false}function sortDevicesById(a,b){return a.deviceId0){this._maxDurationTimeout=setTimeout(this._stop.bind(this),this._maxDuration)}forceShouldLoop=typeof forceShouldLoop==="boolean"?forceShouldLoop:this._shouldLoop;var self=this;var playPromise=this._playPromise=Promise.all(this._sinkIds.map(function createAudioElement(sinkId){if(!self._Audio){return Promise.resolve()}var audioElement=self._activeEls.get(sinkId);if(audioElement){return self._playAudioElement(sinkId,forceIsMuted,forceShouldLoop)}audioElement=new self._Audio(self.url);if(typeof audioElement.setAttribute==="function"){audioElement.setAttribute("crossorigin","anonymous")}return new Promise(function(resolve){audioElement.addEventListener("canplaythrough",resolve)}).then(function(){return(self._isSinkSupported?audioElement.setSinkId(sinkId):Promise.resolve()).then(function setSinkIdSuccess(){self._activeEls.set(sinkId,audioElement);if(!self._playPromise){return Promise.resolve()}return self._playAudioElement(sinkId,forceIsMuted,forceShouldLoop)})})}));return playPromise};Sound.prototype._stop=function _stop(){var _this2=this;this._activeEls.forEach(function(audioEl,sinkId){if(_this2._sinkIds.includes(sinkId)){audioEl.pause();audioEl.currentTime=0}else{destroyAudioElement(audioEl);_this2._activeEls.delete(sinkId)}});clearTimeout(this._maxDurationTimeout);this._playPromise=null;this._maxDurationTimeout=null};Sound.prototype.setSinkIds=function setSinkIds(ids){if(!this._isSinkSupported){return}ids=ids.forEach?ids:[ids];[].splice.apply(this._sinkIds,[0,this._sinkIds.length].concat(ids))};Sound.prototype.stop=function stop(){var _this3=this;this._operations.enqueue(function(){_this3._stop();return Promise.resolve()})};Sound.prototype.play=function play(){var _this4=this;return this._operations.enqueue(function(){return _this4._play()})};module.exports=Sound},{"./asyncQueue":4,"./errors":12,"@twilio/audioplayer":41}],34:[function(require,module,exports){"use strict";var __extends=this&&this.__extends||function(){var extendStatics=function(d,b){extendStatics=Object.setPrototypeOf||{__proto__:[]}instanceof Array&&function(d,b){d.__proto__=b}||function(d,b){for(var p in b)if(b.hasOwnProperty(p))d[p]=b[p]};return extendStatics(d,b)};return function(d,b){extendStatics(d,b);function __(){this.constructor=d}d.prototype=b===null?Object.create(b):(__.prototype=b.prototype,new __)}}();var __assign=this&&this.__assign||function(){__assign=Object.assign||function(t){for(var s,i=1,n=arguments.length;imax?1:0},0)}function countLow(min,values){return values.reduce(function(lowCount,value){return lowCount+=valuethis._maxSampleCount){samples.splice(0,samples.length-this._maxSampleCount)}};StatsMonitor.prototype._clearWarning=function(statName,thresholdName,data){var warningId=statName+":"+thresholdName;var activeWarning=this._activeWarnings.get(warningId);if(!activeWarning||Date.now()-activeWarning.timeRaised0?currentPacketsLost/currentInboundPackets*100:0;var totalInboundPackets=stats.packetsReceived+stats.packetsLost;var totalPacketsLostFraction=totalInboundPackets>0?stats.packetsLost/totalInboundPackets*100:100;var rttValue=typeof stats.rtt==="number"||!previousSample?stats.rtt:previousSample.rtt;var audioInputLevelValues=this._inputVolumes.splice(0);this._supplementalSampleBuffers.audioInputLevel.push(audioInputLevelValues);var audioOutputLevelValues=this._outputVolumes.splice(0);this._supplementalSampleBuffers.audioOutputLevel.push(audioOutputLevelValues);return{audioInputLevel:Math.round(util_1.average(audioInputLevelValues)),audioOutputLevel:Math.round(util_1.average(audioOutputLevelValues)),bytesReceived:currentBytesReceived,bytesSent:currentBytesSent,codecName:stats.codecName,jitter:stats.jitter,mos:this._mos.calculate(rttValue,stats.jitter,previousSample&¤tPacketsLostFraction),packetsLost:currentPacketsLost,packetsLostFraction:currentPacketsLostFraction,packetsReceived:currentPacketsReceived,packetsSent:currentPacketsSent,rtt:rttValue,timestamp:stats.timestamp,totals:{bytesReceived:stats.bytesReceived,bytesSent:stats.bytesSent,packetsLost:stats.packetsLost,packetsLostFraction:totalPacketsLostFraction,packetsReceived:stats.packetsReceived,packetsSent:stats.packetsSent}}};StatsMonitor.prototype._fetchSample=function(){var _this=this;this._getSample().then(function(sample){_this._addSample(sample);_this._raiseWarnings();_this.emit("sample",sample)}).catch(function(error){_this.disable();_this.emit("error",error)})};StatsMonitor.prototype._getSample=function(){var _this=this;return this._getRTCStats(this._peerConnection).then(function(stats){var previousSample=null;if(_this._sampleBuffer.length){previousSample=_this._sampleBuffer[_this._sampleBuffer.length-1]}return _this._createSample(stats,previousSample)})};StatsMonitor.prototype._raiseWarning=function(statName,thresholdName,data){var warningId=statName+":"+thresholdName;if(this._activeWarnings.has(warningId)){return}this._activeWarnings.set(warningId,{timeRaised:Date.now()});var thresholds=this._thresholds[statName];var thresholdValue;if(Array.isArray(thresholds)){var foundThreshold=thresholds.find(function(threshold){return thresholdName in threshold});if(foundThreshold){thresholdValue=foundThreshold[thresholdName]}}else{thresholdValue=this._thresholds[statName][thresholdName]}this.emit("warning",__assign(__assign({},data),{name:statName,threshold:{name:thresholdName,value:thresholdValue}}))};StatsMonitor.prototype._raiseWarnings=function(){var _this=this;if(!this._warningsEnabled){return}Object.keys(this._thresholds).forEach(function(name){return _this._raiseWarningsForStat(name)})};StatsMonitor.prototype._raiseWarningsForStat=function(statName){var _this=this;var limits=Array.isArray(this._thresholds[statName])?this._thresholds[statName]:[this._thresholds[statName]];limits.forEach(function(limit){var samples=_this._sampleBuffer;var clearCount=limit.clearCount||SAMPLE_COUNT_CLEAR;var raiseCount=limit.raiseCount||SAMPLE_COUNT_RAISE;var sampleCount=limit.sampleCount||_this._maxSampleCount;var relevantSamples=samples.slice(-sampleCount);var values=relevantSamples.map(function(sample){return sample[statName]});var containsNull=values.some(function(value){return typeof value==="undefined"||value===null});if(containsNull){return}var count;if(typeof limit.max==="number"){count=countHigh(limit.max,values);if(count>=raiseCount){_this._raiseWarning(statName,"max",{values:values,samples:relevantSamples})}else if(count<=clearCount){_this._clearWarning(statName,"max",{values:values,samples:relevantSamples})}}if(typeof limit.min==="number"){count=countLow(limit.min,values);if(count>=raiseCount){_this._raiseWarning(statName,"min",{values:values,samples:relevantSamples})}else if(count<=clearCount){_this._clearWarning(statName,"min",{values:values,samples:relevantSamples})}}if(typeof limit.maxDuration==="number"&&samples.length>1){relevantSamples=samples.slice(-2);var prevValue=relevantSamples[0][statName];var curValue=relevantSamples[1][statName];var prevStreak=_this._currentStreaks.get(statName)||0;var streak=prevValue===curValue?prevStreak+1:0;_this._currentStreaks.set(statName,streak);if(streak>=limit.maxDuration){_this._raiseWarning(statName,"maxDuration",{value:streak})}else if(streak===0){_this._clearWarning(statName,"maxDuration",{value:prevStreak})}}if(typeof limit.minStandardDeviation==="number"){var sampleSets=_this._supplementalSampleBuffers[statName];if(!sampleSets||sampleSets.lengthlimit.sampleCount){sampleSets.splice(0,sampleSets.length-limit.sampleCount)}var flatSamples=flattenSamples(sampleSets.slice(-sampleCount));var stdDev=calculateStandardDeviation(flatSamples);if(typeof stdDev!=="number"){return}if(stdDevy}],["minAverage",function(x,y){return x=sampleCount){var avg=util_1.average(values);if(comparator(avg,limit[thresholdName])){_this._raiseWarning(statName,thresholdName,{values:values,samples:relevantSamples})}else if(!comparator(avg,limit.clearValue||limit[thresholdName])){_this._clearWarning(statName,thresholdName,{values:values,samples:relevantSamples})}}})})};return StatsMonitor}(events_1.EventEmitter);exports.default=StatsMonitor},{"./errors":12,"./rtc/mos":25,"./rtc/stats":29,"./util":35,events:53}],35:[function(require,module,exports){(function(global){(function(){function TwilioException(message){if(!(this instanceof TwilioException)){return new TwilioException(message)}this.message=message}TwilioException.prototype.toString=function(){return"Twilio.Exception: "+this.message};function average(values){return values&&values.length?values.reduce(function(t,v){return t+v})/values.length:0}function difference(lefts,rights,getKey){getKey=getKey||function(a){return a};var rightKeys=new Set(rights.map(getKey));return lefts.filter(function(left){return!rightKeys.has(getKey(left))})}function isElectron(navigator){return!!navigator.userAgent.match("Electron")}function isChrome(window,navigator){var isCriOS=!!navigator.userAgent.match("CriOS");var isHeadlessChrome=!!navigator.userAgent.match("HeadlessChrome");var isGoogle=typeof window.chrome!=="undefined"&&navigator.vendor==="Google Inc."&&navigator.userAgent.indexOf("OPR")===-1&&navigator.userAgent.indexOf("Edge")===-1;return isCriOS||isElectron(navigator)||isGoogle||isHeadlessChrome}function isFirefox(navigator){navigator=navigator||(typeof window==="undefined"?global.navigator:window.navigator);return!!navigator&&typeof navigator.userAgent==="string"&&/firefox|fxios/i.test(navigator.userAgent)}function isLegacyEdge(navigator){navigator=navigator||(typeof window==="undefined"?global.navigator:window.navigator);return!!navigator&&typeof navigator.userAgent==="string"&&/edge\/\d+/i.test(navigator.userAgent)}function isSafari(navigator){return!!navigator.vendor&&navigator.vendor.indexOf("Apple")!==-1&&navigator.userAgent&&navigator.userAgent.indexOf("CriOS")===-1&&navigator.userAgent.indexOf("FxiOS")===-1}function isUnifiedPlanDefault(window,navigator,PeerConnection,RtpTransceiver){if(typeof window==="undefined"||typeof navigator==="undefined"||typeof PeerConnection==="undefined"||typeof RtpTransceiver==="undefined"||typeof PeerConnection.prototype==="undefined"||typeof RtpTransceiver.prototype==="undefined"){return false}if(isChrome(window,navigator)&&PeerConnection.prototype.addTransceiver){var pc=new PeerConnection;var isUnifiedPlan=true;try{pc.addTransceiver("audio")}catch(e){isUnifiedPlan=false}pc.close();return isUnifiedPlan}else if(isFirefox(navigator)){return true}else if(isSafari(navigator)){return"currentDirection"in RtpTransceiver.prototype}return false}function queryToJson(params){if(!params){return""}return params.split("&").reduce(function(output,pair){var parts=pair.split("=");var key=parts[0];var value=decodeURIComponent((parts[1]||"").replace(/\+/g,"%20"));if(key){output[key]=value}return output},{})}function flatMap(list,mapFn){var listArray=list instanceof Map||list instanceof Set?Array.from(list.values()):list;mapFn=mapFn||function(item){return item};return listArray.reduce(function(flattened,item){var mapped=mapFn(item);return flattened.concat(mapped)},[])}exports.Exception=TwilioException;exports.average=average;exports.difference=difference;exports.isElectron=isElectron;exports.isChrome=isChrome;exports.isFirefox=isFirefox;exports.isLegacyEdge=isLegacyEdge;exports.isSafari=isSafari;exports.isUnifiedPlanDefault=isUnifiedPlanDefault;exports.queryToJson=queryToJson;exports.flatMap=flatMap}).call(this)}).call(this,typeof global!=="undefined"?global:typeof self!=="undefined"?self:typeof window!=="undefined"?window:{})},{}],36:[function(require,module,exports){"use strict";Object.defineProperty(exports,"__esModule",{value:true});var md5=require("md5");var errors_1=require("../twilio/errors");function generateUuid(){if(typeof window!=="object"){throw new errors_1.NotSupportedError("This platform is not supported.")}var crypto=window.crypto;if(typeof crypto!=="object"){throw new errors_1.NotSupportedError("The `crypto` module is not available on this platform.")}if(typeof(crypto.randomUUID||crypto.getRandomValues)==="undefined"){throw new errors_1.NotSupportedError("Neither `crypto.randomUUID` or `crypto.getRandomValues` are available "+"on this platform.")}var uInt32Arr=window.Uint32Array;if(typeof uInt32Arr==="undefined"){throw new errors_1.NotSupportedError("The `Uint32Array` module is not available on this platform.")}var generateRandomValues=typeof crypto.randomUUID==="function"?function(){return crypto.randomUUID()}:function(){return crypto.getRandomValues(new Uint32Array(32)).toString()};return md5(generateRandomValues())}function generateVoiceEventSid(){return"KX"+generateUuid()}exports.generateVoiceEventSid=generateVoiceEventSid},{"../twilio/errors":12,md5:56}],37:[function(require,module,exports){"use strict";var __extends=this&&this.__extends||function(){var extendStatics=function(d,b){extendStatics=Object.setPrototypeOf||{__proto__:[]}instanceof Array&&function(d,b){d.__proto__=b}||function(d,b){for(var p in b)if(b.hasOwnProperty(p))d[p]=b[p]};return extendStatics(d,b)};return function(d,b){extendStatics(d,b);function __(){this.constructor=d}d.prototype=b===null?Object.create(b):(__.prototype=b.prototype,new __)}}();var __assign=this&&this.__assign||function(){__assign=Object.assign||function(t){for(var s,i=1,n=arguments.length;i=_this._uris.length){_this._uriIndex=0}};_this._onSocketClose=function(event){_this._log.info("Received websocket close event code: "+event.code+". Reason: "+event.reason);if(event.code===1006||event.code===1015){_this.emit("error",{code:31005,message:event.reason||"Websocket connection to Twilio's signaling servers were "+"unexpectedly ended. If this is happening consistently, there may "+"be an issue resolving the hostname provided. If a region or an "+"edge is being specified in Device setup, ensure it is valid.",twilioError:new errors_1.SignalingErrors.ConnectionError});var wasConnected=_this.state===WSTransportState.Open||_this._previousState===WSTransportState.Open;if(_this._shouldFallback||!wasConnected){_this._moveUriIndex()}_this._shouldFallback=true}_this._closeSocket()};_this._onSocketError=function(err){_this._log.info("WebSocket received error: "+err.message);_this.emit("error",{code:31e3,message:err.message||"WSTransport socket error",twilioError:new errors_1.SignalingErrors.ConnectionDisconnected})};_this._onSocketMessage=function(message){_this._setHeartbeatTimeout();if(_this._socket&&message.data==="\n"){_this._socket.send("\n");return}_this.emit("message",message)};_this._onSocketOpen=function(){_this._log.info("WebSocket opened successfully.");_this._timeOpened=Date.now();_this._shouldFallback=false;_this._setState(WSTransportState.Open);clearTimeout(_this._connectTimeout);_this._resetBackoffs();_this._setHeartbeatTimeout();_this.emit("open")};_this._options=__assign(__assign({},WSTransport.defaultConstructorOptions),options);_this._uris=uris;_this._backoff=_this._setupBackoffs();return _this}WSTransport.prototype.close=function(){this._log.info("WSTransport.close() called...");this._close()};WSTransport.prototype.open=function(){this._log.info("WSTransport.open() called...");if(this._socket&&(this._socket.readyState===WebSocket.CONNECTING||this._socket.readyState===WebSocket.OPEN)){this._log.info("WebSocket already open.");return}if(this._preferredUri){this._connect(this._preferredUri)}else{this._connect(this._uris[this._uriIndex])}};WSTransport.prototype.send=function(message){if(!this._socket||this._socket.readyState!==WebSocket.OPEN){return false}try{this._socket.send(message)}catch(e){this._log.info("Error while sending message:",e.message);this._closeSocket();return false}return true};WSTransport.prototype.updatePreferredURI=function(uri){this._preferredUri=uri};WSTransport.prototype.updateURIs=function(uris){if(typeof uris==="string"){uris=[uris]}this._uris=uris;this._uriIndex=0};WSTransport.prototype._close=function(){this._setState(WSTransportState.Closed);this._closeSocket()};WSTransport.prototype._closeSocket=function(){clearTimeout(this._connectTimeout);clearTimeout(this._heartbeatTimeout);this._log.info("Closing and cleaning up WebSocket...");if(!this._socket){this._log.info("No WebSocket to clean up.");return}this._socket.removeEventListener("close",this._onSocketClose);this._socket.removeEventListener("error",this._onSocketError);this._socket.removeEventListener("message",this._onSocketMessage);this._socket.removeEventListener("open",this._onSocketOpen);if(this._socket.readyState===WebSocket.CONNECTING||this._socket.readyState===WebSocket.OPEN){this._socket.close()}if(this._timeOpened&&Date.now()-this._timeOpened>CONNECT_SUCCESS_TIMEOUT){this._resetBackoffs()}if(this.state!==WSTransportState.Closed){this._performBackoff()}delete this._socket;this.emit("close")};WSTransport.prototype._connect=function(uri,retryCount){var _this=this;this._log.info(typeof retryCount==="number"?"Attempting to reconnect (retry #"+retryCount+")...":"Attempting to connect...");this._closeSocket();this._setState(WSTransportState.Connecting);this._connectedUri=uri;try{this._socket=new this._options.WebSocket(this._connectedUri)}catch(e){this._log.info("Could not connect to endpoint:",e.message);this._close();this.emit("error",{code:31e3,message:e.message||"Could not connect to "+this._connectedUri,twilioError:new errors_1.SignalingErrors.ConnectionDisconnected});return}this._socket.addEventListener("close",this._onSocketClose);this._socket.addEventListener("error",this._onSocketError);this._socket.addEventListener("message",this._onSocketMessage);this._socket.addEventListener("open",this._onSocketOpen);delete this._timeOpened;this._connectTimeout=setTimeout(function(){_this._log.info("WebSocket connection attempt timed out.");_this._moveUriIndex();_this._closeSocket()},this._options.connectTimeoutMs)};WSTransport.prototype._performBackoff=function(){if(this._preferredUri){this._log.info("Preferred URI set; backing off.");this._backoff.preferred.backoff()}else{this._log.info("Preferred URI not set; backing off.");this._backoff.primary.backoff()}};WSTransport.prototype._resetBackoffs=function(){this._backoff.preferred.reset();this._backoff.primary.reset();this._backoffStartTime.preferred=null;this._backoffStartTime.primary=null};WSTransport.prototype._setHeartbeatTimeout=function(){var _this=this;clearTimeout(this._heartbeatTimeout);this._heartbeatTimeout=setTimeout(function(){_this._log.info("No messages received in "+HEARTBEAT_TIMEOUT/1e3+" seconds. Reconnecting...");_this._shouldFallback=true;_this._closeSocket()},HEARTBEAT_TIMEOUT)};WSTransport.prototype._setState=function(state){this._previousState=this.state;this.state=state};WSTransport.prototype._setupBackoffs=function(){var _this=this;var preferredBackoffConfig={factor:2,maxDelay:this._options.maxPreferredDelayMs,randomisationFactor:.4};this._log.info("Initializing preferred transport backoff using config: ",preferredBackoffConfig);var preferredBackoff=Backoff.exponential(preferredBackoffConfig);preferredBackoff.on("backoff",function(attempt,delay){if(_this.state===WSTransportState.Closed){_this._log.info("Preferred backoff initiated but transport state is closed; not attempting a connection.");return}_this._log.info("Will attempt to reconnect Websocket to preferred URI in "+delay+"ms");if(attempt===0){_this._backoffStartTime.preferred=Date.now();_this._log.info("Preferred backoff start; "+_this._backoffStartTime.preferred)}});preferredBackoff.on("ready",function(attempt,_delay){if(_this.state===WSTransportState.Closed){_this._log.info("Preferred backoff ready but transport state is closed; not attempting a connection.");return}if(_this._backoffStartTime.preferred===null){_this._log.info("Preferred backoff start time invalid; not attempting a connection.");return}if(Date.now()-_this._backoffStartTime.preferred>_this._options.maxPreferredDurationMs){_this._log.info("Max preferred backoff attempt time exceeded; falling back to primary backoff.");_this._preferredUri=null;_this._backoff.primary.backoff();return}if(typeof _this._preferredUri!=="string"){_this._log.info("Preferred URI cleared; falling back to primary backoff.");_this._preferredUri=null;_this._backoff.primary.backoff();return}_this._connect(_this._preferredUri,attempt+1)});var primaryBackoffConfig={factor:2,initialDelay:this._uris&&this._uris.length>1?Math.floor(Math.random()*(5e3-1e3+1))+1e3:100,maxDelay:this._options.maxPrimaryDelayMs,randomisationFactor:.4};this._log.info("Initializing primary transport backoff using config: ",primaryBackoffConfig);var primaryBackoff=Backoff.exponential(primaryBackoffConfig);primaryBackoff.on("backoff",function(attempt,delay){if(_this.state===WSTransportState.Closed){_this._log.info("Primary backoff initiated but transport state is closed; not attempting a connection.");return}_this._log.info("Will attempt to reconnect WebSocket in "+delay+"ms");if(attempt===0){_this._backoffStartTime.primary=Date.now();_this._log.info("Primary backoff start; "+_this._backoffStartTime.primary)}});primaryBackoff.on("ready",function(attempt,_delay){if(_this.state===WSTransportState.Closed){_this._log.info("Primary backoff ready but transport state is closed; not attempting a connection.");return}if(_this._backoffStartTime.primary===null){_this._log.info("Primary backoff start time invalid; not attempting a connection.");return}if(Date.now()-_this._backoffStartTime.primary>_this._options.maxPrimaryDurationMs){_this._log.info("Max primary backoff attempt time exceeded; not attempting a connection.");return}_this._connect(_this._uris[_this._uriIndex],attempt+1)});return{preferred:preferredBackoff,primary:primaryBackoff}};Object.defineProperty(WSTransport.prototype,"uri",{get:function(){return this._connectedUri},enumerable:true,configurable:true});WSTransport.defaultConstructorOptions={WebSocket:WebSocket,connectTimeoutMs:CONNECT_TIMEOUT,maxPreferredDelayMs:MAX_PREFERRED_DELAY,maxPreferredDurationMs:MAX_PREFERRED_DURATION,maxPrimaryDelayMs:MAX_PRIMARY_DELAY,maxPrimaryDurationMs:MAX_PRIMARY_DURATION};return WSTransport}(events_1.EventEmitter);exports.default=WSTransport},{"./errors":12,"./log":15,backoff:45,events:53,ws:1}],38:[function(require,module,exports){"use strict";var _regenerator=require("babel-runtime/regenerator");var _regenerator2=_interopRequireDefault(_regenerator);var _createClass=function(){function defineProperties(target,props){for(var i=0;i1&&arguments[1]!==undefined?arguments[1]:{};var options=arguments.length>2&&arguments[2]!==undefined?arguments[2]:{};_classCallCheck(this,AudioPlayer);var _this=_possibleConstructorReturn(this,(AudioPlayer.__proto__||Object.getPrototypeOf(AudioPlayer)).call(this));_this._audioNode=null;_this._pendingPlayDeferreds=[];_this._loop=false;_this._src="";_this._sinkId="default";if(typeof srcOrOptions!=="string"){options=srcOrOptions}_this._audioContext=audioContext;_this._audioElement=new(options.AudioFactory||Audio);_this._bufferPromise=_this._createPlayDeferred().promise;_this._destination=_this._audioContext.destination;_this._gainNode=_this._audioContext.createGain();_this._gainNode.connect(_this._destination);_this._XMLHttpRequest=options.XMLHttpRequestFactory||XMLHttpRequest;_this.addEventListener("canplaythrough",function(){_this._resolvePlayDeferreds()});if(typeof srcOrOptions==="string"){_this.src=srcOrOptions}return _this}_createClass(AudioPlayer,[{key:"load",value:function load(){this._load(this._src)}},{key:"pause",value:function pause(){if(this.paused){return}this._audioElement.pause();this._audioNode.stop();this._audioNode.disconnect(this._gainNode);this._audioNode=null;this._rejectPlayDeferreds(new Error("The play() request was interrupted by a call to pause()."))}},{key:"play",value:function play(){return __awaiter(this,void 0,void 0,_regenerator2.default.mark(function _callee(){var _this2=this;var buffer;return _regenerator2.default.wrap(function _callee$(_context){while(1){switch(_context.prev=_context.next){case 0:if(this.paused){_context.next=6;break}_context.next=3;return this._bufferPromise;case 3:if(this.paused){_context.next=5;break}return _context.abrupt("return");case 5:throw new Error("The play() request was interrupted by a call to pause().");case 6:this._audioNode=this._audioContext.createBufferSource();this._audioNode.loop=this.loop;this._audioNode.addEventListener("ended",function(){if(_this2._audioNode&&_this2._audioNode.loop){return}_this2.dispatchEvent("ended")});_context.next=11;return this._bufferPromise;case 11:buffer=_context.sent;if(!this.paused){_context.next=14;break}throw new Error("The play() request was interrupted by a call to pause().");case 14:this._audioNode.buffer=buffer;this._audioNode.connect(this._gainNode);this._audioNode.start();if(!this._audioElement.srcObject){_context.next=19;break}return _context.abrupt("return",this._audioElement.play());case 19:case"end":return _context.stop()}}},_callee,this)}))}},{key:"setSinkId",value:function setSinkId(sinkId){return __awaiter(this,void 0,void 0,_regenerator2.default.mark(function _callee2(){return _regenerator2.default.wrap(function _callee2$(_context2){while(1){switch(_context2.prev=_context2.next){case 0:if(!(typeof this._audioElement.setSinkId!=="function")){_context2.next=2;break}throw new Error("This browser does not support setSinkId.");case 2:if(!(sinkId===this.sinkId)){_context2.next=4;break}return _context2.abrupt("return");case 4:if(!(sinkId==="default")){_context2.next=11;break}if(!this.paused){this._gainNode.disconnect(this._destination)}this._audioElement.srcObject=null;this._destination=this._audioContext.destination;this._gainNode.connect(this._destination);this._sinkId=sinkId;return _context2.abrupt("return");case 11:_context2.next=13;return this._audioElement.setSinkId(sinkId);case 13:if(!this._audioElement.srcObject){_context2.next=15;break}return _context2.abrupt("return");case 15:this._gainNode.disconnect(this._audioContext.destination);this._destination=this._audioContext.createMediaStreamDestination();this._audioElement.srcObject=this._destination.stream;this._sinkId=sinkId;this._gainNode.connect(this._destination);case 20:case"end":return _context2.stop()}}},_callee2,this)}))}},{key:"_createPlayDeferred",value:function _createPlayDeferred(){var deferred=new Deferred_1.default;this._pendingPlayDeferreds.push(deferred);return deferred}},{key:"_load",value:function _load(src){var _this3=this;if(this._src&&this._src!==src){this.pause()}this._src=src;this._bufferPromise=new Promise(function(resolve,reject){return __awaiter(_this3,void 0,void 0,_regenerator2.default.mark(function _callee3(){var buffer;return _regenerator2.default.wrap(function _callee3$(_context3){while(1){switch(_context3.prev=_context3.next){case 0:if(src){_context3.next=2;break}return _context3.abrupt("return",this._createPlayDeferred().promise);case 2:_context3.next=4;return bufferSound(this._audioContext,this._XMLHttpRequest,src);case 4:buffer=_context3.sent;this.dispatchEvent("canplaythrough");resolve(buffer);case 7:case"end":return _context3.stop()}}},_callee3,this)}))})}},{key:"_rejectPlayDeferreds",value:function _rejectPlayDeferreds(reason){var deferreds=this._pendingPlayDeferreds;deferreds.splice(0,deferreds.length).forEach(function(_ref){var reject=_ref.reject;return reject(reason)})}},{key:"_resolvePlayDeferreds",value:function _resolvePlayDeferreds(result){var deferreds=this._pendingPlayDeferreds;deferreds.splice(0,deferreds.length).forEach(function(_ref2){var resolve=_ref2.resolve;return resolve(result)})}},{key:"destination",get:function get(){return this._destination}},{key:"loop",get:function get(){return this._loop},set:function set(shouldLoop){if(!shouldLoop&&this.loop&&!this.paused){var _pauseAfterPlaythrough=function _pauseAfterPlaythrough(){self._audioNode.removeEventListener("ended",_pauseAfterPlaythrough);self.pause()};var self=this;this._audioNode.addEventListener("ended",_pauseAfterPlaythrough)}this._loop=shouldLoop}},{key:"muted",get:function get(){return this._gainNode.gain.value===0},set:function set(shouldBeMuted){this._gainNode.gain.value=shouldBeMuted?0:1}},{key:"paused",get:function get(){return this._audioNode===null}},{key:"src",get:function get(){return this._src},set:function set(src){this._load(src)}},{key:"srcObject",get:function get(){return this._audioElement.srcObject},set:function set(srcObject){this._audioElement.srcObject=srcObject}},{key:"sinkId",get:function get(){return this._sinkId}}]);return AudioPlayer}(EventTarget_1.default);exports.default=AudioPlayer;function bufferSound(context,RequestFactory,src){return __awaiter(this,void 0,void 0,_regenerator2.default.mark(function _callee4(){var request,event;return _regenerator2.default.wrap(function _callee4$(_context4){while(1){switch(_context4.prev=_context4.next){case 0:request=new RequestFactory;request.open("GET",src,true);request.responseType="arraybuffer";_context4.next=5;return new Promise(function(resolve){request.addEventListener("load",resolve);request.send()});case 5:event=_context4.sent;_context4.prev=6;return _context4.abrupt("return",context.decodeAudioData(event.target.response));case 10:_context4.prev=10;_context4.t0=_context4["catch"](6);return _context4.abrupt("return",new Promise(function(resolve){context.decodeAudioData(event.target.response,resolve)}));case 13:case"end":return _context4.stop()}}},_callee4,this,[[6,10]])}))}},{"./Deferred":39,"./EventTarget":40,"babel-runtime/regenerator":44}],39:[function(require,module,exports){"use strict";var _createClass=function(){function defineProperties(target,props){for(var i=0;i1?_len-1:0),_key=1;_key<_len;_key++){args[_key-1]=arguments[_key]}return(_eventEmitter=this._eventEmitter).emit.apply(_eventEmitter,[name].concat(args))}},{key:"removeEventListener",value:function removeEventListener(name,handler){return this._eventEmitter.removeListener(name,handler)}}]);return EventTarget}();exports.default=EventTarget},{events:53}],41:[function(require,module,exports){"use strict";var AudioPlayer=require("./AudioPlayer");module.exports=AudioPlayer.default},{"./AudioPlayer":38}],42:[function(require,module,exports){var g=function(){return this}()||Function("return this")();var hadRuntime=g.regeneratorRuntime&&Object.getOwnPropertyNames(g).indexOf("regeneratorRuntime")>=0;var oldRuntime=hadRuntime&&g.regeneratorRuntime;g.regeneratorRuntime=undefined;module.exports=require("./runtime");if(hadRuntime){g.regeneratorRuntime=oldRuntime}else{try{delete g.regeneratorRuntime}catch(e){g.regeneratorRuntime=undefined}}},{"./runtime":43}],43:[function(require,module,exports){!function(global){"use strict";var Op=Object.prototype;var hasOwn=Op.hasOwnProperty;var undefined;var $Symbol=typeof Symbol==="function"?Symbol:{};var iteratorSymbol=$Symbol.iterator||"@@iterator";var asyncIteratorSymbol=$Symbol.asyncIterator||"@@asyncIterator";var toStringTagSymbol=$Symbol.toStringTag||"@@toStringTag";var inModule=typeof module==="object";var runtime=global.regeneratorRuntime;if(runtime){if(inModule){module.exports=runtime}return}runtime=global.regeneratorRuntime=inModule?module.exports:{};function wrap(innerFn,outerFn,self,tryLocsList){var protoGenerator=outerFn&&outerFn.prototype instanceof Generator?outerFn:Generator;var generator=Object.create(protoGenerator.prototype);var context=new Context(tryLocsList||[]);generator._invoke=makeInvokeMethod(innerFn,self,context);return generator}runtime.wrap=wrap;function tryCatch(fn,obj,arg){try{return{type:"normal",arg:fn.call(obj,arg)}}catch(err){return{type:"throw",arg:err}}}var GenStateSuspendedStart="suspendedStart";var GenStateSuspendedYield="suspendedYield";var GenStateExecuting="executing";var GenStateCompleted="completed";var ContinueSentinel={};function Generator(){}function GeneratorFunction(){}function GeneratorFunctionPrototype(){}var IteratorPrototype={};IteratorPrototype[iteratorSymbol]=function(){return this};var getProto=Object.getPrototypeOf;var NativeIteratorPrototype=getProto&&getProto(getProto(values([])));if(NativeIteratorPrototype&&NativeIteratorPrototype!==Op&&hasOwn.call(NativeIteratorPrototype,iteratorSymbol)){IteratorPrototype=NativeIteratorPrototype}var Gp=GeneratorFunctionPrototype.prototype=Generator.prototype=Object.create(IteratorPrototype);GeneratorFunction.prototype=Gp.constructor=GeneratorFunctionPrototype;GeneratorFunctionPrototype.constructor=GeneratorFunction;GeneratorFunctionPrototype[toStringTagSymbol]=GeneratorFunction.displayName="GeneratorFunction";function defineIteratorMethods(prototype){["next","throw","return"].forEach(function(method){prototype[method]=function(arg){return this._invoke(method,arg)}})}runtime.isGeneratorFunction=function(genFun){var ctor=typeof genFun==="function"&&genFun.constructor;return ctor?ctor===GeneratorFunction||(ctor.displayName||ctor.name)==="GeneratorFunction":false};runtime.mark=function(genFun){if(Object.setPrototypeOf){Object.setPrototypeOf(genFun,GeneratorFunctionPrototype)}else{genFun.__proto__=GeneratorFunctionPrototype;if(!(toStringTagSymbol in genFun)){genFun[toStringTagSymbol]="GeneratorFunction"}}genFun.prototype=Object.create(Gp);return genFun};runtime.awrap=function(arg){return{__await:arg}};function AsyncIterator(generator){function invoke(method,arg,resolve,reject){var record=tryCatch(generator[method],generator,arg);if(record.type==="throw"){reject(record.arg)}else{var result=record.arg;var value=result.value;if(value&&typeof value==="object"&&hasOwn.call(value,"__await")){return Promise.resolve(value.__await).then(function(value){invoke("next",value,resolve,reject)},function(err){invoke("throw",err,resolve,reject)})}return Promise.resolve(value).then(function(unwrapped){result.value=unwrapped;resolve(result)},reject)}}var previousPromise;function enqueue(method,arg){function callInvokeWithMethodAndArg(){return new Promise(function(resolve,reject){invoke(method,arg,resolve,reject)})}return previousPromise=previousPromise?previousPromise.then(callInvokeWithMethodAndArg,callInvokeWithMethodAndArg):callInvokeWithMethodAndArg()}this._invoke=enqueue}defineIteratorMethods(AsyncIterator.prototype);AsyncIterator.prototype[asyncIteratorSymbol]=function(){return this};runtime.AsyncIterator=AsyncIterator;runtime.async=function(innerFn,outerFn,self,tryLocsList){var iter=new AsyncIterator(wrap(innerFn,outerFn,self,tryLocsList));return runtime.isGeneratorFunction(outerFn)?iter:iter.next().then(function(result){return result.done?result.value:iter.next()})};function makeInvokeMethod(innerFn,self,context){var state=GenStateSuspendedStart;return function invoke(method,arg){if(state===GenStateExecuting){throw new Error("Generator is already running")}if(state===GenStateCompleted){if(method==="throw"){throw arg}return doneResult()}context.method=method;context.arg=arg;while(true){var delegate=context.delegate;if(delegate){var delegateResult=maybeInvokeDelegate(delegate,context);if(delegateResult){if(delegateResult===ContinueSentinel)continue;return delegateResult}}if(context.method==="next"){context.sent=context._sent=context.arg}else if(context.method==="throw"){if(state===GenStateSuspendedStart){state=GenStateCompleted;throw context.arg}context.dispatchException(context.arg)}else if(context.method==="return"){context.abrupt("return",context.arg)}state=GenStateExecuting;var record=tryCatch(innerFn,self,context);if(record.type==="normal"){state=context.done?GenStateCompleted:GenStateSuspendedYield;if(record.arg===ContinueSentinel){continue}return{value:record.arg,done:context.done}}else if(record.type==="throw"){state=GenStateCompleted;context.method="throw";context.arg=record.arg}}}}function maybeInvokeDelegate(delegate,context){var method=delegate.iterator[context.method];if(method===undefined){context.delegate=null;if(context.method==="throw"){if(delegate.iterator.return){context.method="return";context.arg=undefined;maybeInvokeDelegate(delegate,context);if(context.method==="throw"){return ContinueSentinel}}context.method="throw";context.arg=new TypeError("The iterator does not provide a 'throw' method")}return ContinueSentinel}var record=tryCatch(method,delegate.iterator,context.arg);if(record.type==="throw"){context.method="throw";context.arg=record.arg;context.delegate=null;return ContinueSentinel}var info=record.arg;if(!info){context.method="throw";context.arg=new TypeError("iterator result is not an object");context.delegate=null;return ContinueSentinel}if(info.done){context[delegate.resultName]=info.value;context.next=delegate.nextLoc;if(context.method!=="return"){context.method="next";context.arg=undefined}}else{return info}context.delegate=null;return ContinueSentinel}defineIteratorMethods(Gp);Gp[toStringTagSymbol]="Generator";Gp[iteratorSymbol]=function(){return this};Gp.toString=function(){return"[object Generator]"};function pushTryEntry(locs){var entry={tryLoc:locs[0]};if(1 in locs){entry.catchLoc=locs[1]}if(2 in locs){entry.finallyLoc=locs[2];entry.afterLoc=locs[3]}this.tryEntries.push(entry)}function resetTryEntry(entry){var record=entry.completion||{};record.type="normal";delete record.arg;entry.completion=record}function Context(tryLocsList){this.tryEntries=[{tryLoc:"root"}];tryLocsList.forEach(pushTryEntry,this);this.reset(true)}runtime.keys=function(object){var keys=[];for(var key in object){keys.push(key)}keys.reverse();return function next(){while(keys.length){var key=keys.pop();if(key in object){next.value=key;next.done=false;return next}}next.done=true;return next}};function values(iterable){if(iterable){var iteratorMethod=iterable[iteratorSymbol];if(iteratorMethod){return iteratorMethod.call(iterable)}if(typeof iterable.next==="function"){return iterable}if(!isNaN(iterable.length)){var i=-1,next=function next(){while(++i=0;--i){var entry=this.tryEntries[i];var record=entry.completion;if(entry.tryLoc==="root"){return handle("end")}if(entry.tryLoc<=this.prev){var hasCatch=hasOwn.call(entry,"catchLoc");var hasFinally=hasOwn.call(entry,"finallyLoc");if(hasCatch&&hasFinally){if(this.prev=0;--i){var entry=this.tryEntries[i];if(entry.tryLoc<=this.prev&&hasOwn.call(entry,"finallyLoc")&&this.prev=0;--i){var entry=this.tryEntries[i];if(entry.finallyLoc===finallyLoc){this.complete(entry.completion,entry.afterLoc);resetTryEntry(entry);return ContinueSentinel}}},catch:function(tryLoc){for(var i=this.tryEntries.length-1;i>=0;--i){var entry=this.tryEntries[i];if(entry.tryLoc===tryLoc){var record=entry.completion;if(record.type==="throw"){var thrown=record.arg;resetTryEntry(entry)}return thrown}}throw new Error("illegal catch attempt")},delegateYield:function(iterable,resultName,nextLoc){this.delegate={iterator:values(iterable),resultName:resultName,nextLoc:nextLoc};if(this.method==="next"){this.arg=undefined}return ContinueSentinel}}}(function(){return this}()||Function("return this")())},{}],44:[function(require,module,exports){module.exports=require("regenerator-runtime")},{"regenerator-runtime":42}],45:[function(require,module,exports){var Backoff=require("./lib/backoff");var ExponentialBackoffStrategy=require("./lib/strategy/exponential");var FibonacciBackoffStrategy=require("./lib/strategy/fibonacci");var FunctionCall=require("./lib/function_call.js");module.exports.Backoff=Backoff;module.exports.FunctionCall=FunctionCall;module.exports.FibonacciStrategy=FibonacciBackoffStrategy;module.exports.ExponentialStrategy=ExponentialBackoffStrategy;module.exports.fibonacci=function(options){return new Backoff(new FibonacciBackoffStrategy(options))};module.exports.exponential=function(options){return new Backoff(new ExponentialBackoffStrategy(options))};module.exports.call=function(fn,vargs,callback){var args=Array.prototype.slice.call(arguments);fn=args[0];vargs=args.slice(1,args.length-1);callback=args[args.length-1];return new FunctionCall(fn,vargs,callback)}},{"./lib/backoff":46,"./lib/function_call.js":47,"./lib/strategy/exponential":48,"./lib/strategy/fibonacci":49}],46:[function(require,module,exports){var events=require("events");var precond=require("precond");var util=require("util");function Backoff(backoffStrategy){events.EventEmitter.call(this);this.backoffStrategy_=backoffStrategy;this.maxNumberOfRetry_=-1;this.backoffNumber_=0;this.backoffDelay_=0;this.timeoutID_=-1;this.handlers={backoff:this.onBackoff_.bind(this)}}util.inherits(Backoff,events.EventEmitter);Backoff.prototype.failAfter=function(maxNumberOfRetry){precond.checkArgument(maxNumberOfRetry>0,"Expected a maximum number of retry greater than 0 but got %s.",maxNumberOfRetry);this.maxNumberOfRetry_=maxNumberOfRetry};Backoff.prototype.backoff=function(err){precond.checkState(this.timeoutID_===-1,"Backoff in progress.");if(this.backoffNumber_===this.maxNumberOfRetry_){this.emit("fail",err);this.reset()}else{this.backoffDelay_=this.backoffStrategy_.next();this.timeoutID_=setTimeout(this.handlers.backoff,this.backoffDelay_);this.emit("backoff",this.backoffNumber_,this.backoffDelay_,err)}};Backoff.prototype.onBackoff_=function(){this.timeoutID_=-1;this.emit("ready",this.backoffNumber_,this.backoffDelay_);this.backoffNumber_++};Backoff.prototype.reset=function(){this.backoffNumber_=0;this.backoffStrategy_.reset();clearTimeout(this.timeoutID_);this.timeoutID_=-1};module.exports=Backoff},{events:53,precond:57,util:65}],47:[function(require,module,exports){var events=require("events");var precond=require("precond");var util=require("util");var Backoff=require("./backoff");var FibonacciBackoffStrategy=require("./strategy/fibonacci");function FunctionCall(fn,args,callback){events.EventEmitter.call(this);precond.checkIsFunction(fn,"Expected fn to be a function.");precond.checkIsArray(args,"Expected args to be an array.");precond.checkIsFunction(callback,"Expected callback to be a function.");this.function_=fn;this.arguments_=args;this.callback_=callback;this.lastResult_=[];this.numRetries_=0;this.backoff_=null;this.strategy_=null;this.failAfter_=-1;this.retryPredicate_=FunctionCall.DEFAULT_RETRY_PREDICATE_;this.state_=FunctionCall.State_.PENDING}util.inherits(FunctionCall,events.EventEmitter);FunctionCall.State_={PENDING:0,RUNNING:1,COMPLETED:2,ABORTED:3};FunctionCall.DEFAULT_RETRY_PREDICATE_=function(err){return true};FunctionCall.prototype.isPending=function(){return this.state_==FunctionCall.State_.PENDING};FunctionCall.prototype.isRunning=function(){return this.state_==FunctionCall.State_.RUNNING};FunctionCall.prototype.isCompleted=function(){return this.state_==FunctionCall.State_.COMPLETED};FunctionCall.prototype.isAborted=function(){return this.state_==FunctionCall.State_.ABORTED};FunctionCall.prototype.setStrategy=function(strategy){precond.checkState(this.isPending(),"FunctionCall in progress.");this.strategy_=strategy;return this};FunctionCall.prototype.retryIf=function(retryPredicate){precond.checkState(this.isPending(),"FunctionCall in progress.");this.retryPredicate_=retryPredicate;return this};FunctionCall.prototype.getLastResult=function(){return this.lastResult_.concat()};FunctionCall.prototype.getNumRetries=function(){return this.numRetries_};FunctionCall.prototype.failAfter=function(maxNumberOfRetry){precond.checkState(this.isPending(),"FunctionCall in progress.");this.failAfter_=maxNumberOfRetry;return this};FunctionCall.prototype.abort=function(){if(this.isCompleted()||this.isAborted()){return}if(this.isRunning()){this.backoff_.reset()}this.state_=FunctionCall.State_.ABORTED;this.lastResult_=[new Error("Backoff aborted.")];this.emit("abort");this.doCallback_()};FunctionCall.prototype.start=function(backoffFactory){precond.checkState(!this.isAborted(),"FunctionCall is aborted.");precond.checkState(this.isPending(),"FunctionCall already started.");var strategy=this.strategy_||new FibonacciBackoffStrategy;this.backoff_=backoffFactory?backoffFactory(strategy):new Backoff(strategy);this.backoff_.on("ready",this.doCall_.bind(this,true));this.backoff_.on("fail",this.doCallback_.bind(this));this.backoff_.on("backoff",this.handleBackoff_.bind(this));if(this.failAfter_>0){this.backoff_.failAfter(this.failAfter_)}this.state_=FunctionCall.State_.RUNNING;this.doCall_(false)};FunctionCall.prototype.doCall_=function(isRetry){if(isRetry){this.numRetries_++}var eventArgs=["call"].concat(this.arguments_);events.EventEmitter.prototype.emit.apply(this,eventArgs);var callback=this.handleFunctionCallback_.bind(this);this.function_.apply(null,this.arguments_.concat(callback))};FunctionCall.prototype.doCallback_=function(){this.callback_.apply(null,this.lastResult_)};FunctionCall.prototype.handleFunctionCallback_=function(){if(this.isAborted()){return}var args=Array.prototype.slice.call(arguments);this.lastResult_=args;events.EventEmitter.prototype.emit.apply(this,["callback"].concat(args));var err=args[0];if(err&&this.retryPredicate_(err)){this.backoff_.backoff(err)}else{this.state_=FunctionCall.State_.COMPLETED;this.doCallback_()}};FunctionCall.prototype.handleBackoff_=function(number,delay,err){this.emit("backoff",number,delay,err)};module.exports=FunctionCall},{"./backoff":46,"./strategy/fibonacci":49,events:53,precond:57,util:65}],48:[function(require,module,exports){var util=require("util");var precond=require("precond");var BackoffStrategy=require("./strategy");function ExponentialBackoffStrategy(options){BackoffStrategy.call(this,options);this.backoffDelay_=0;this.nextBackoffDelay_=this.getInitialDelay();this.factor_=ExponentialBackoffStrategy.DEFAULT_FACTOR;if(options&&options.factor!==undefined){precond.checkArgument(options.factor>1,"Exponential factor should be greater than 1 but got %s.",options.factor);this.factor_=options.factor}}util.inherits(ExponentialBackoffStrategy,BackoffStrategy);ExponentialBackoffStrategy.DEFAULT_FACTOR=2;ExponentialBackoffStrategy.prototype.next_=function(){this.backoffDelay_=Math.min(this.nextBackoffDelay_,this.getMaxDelay());this.nextBackoffDelay_=this.backoffDelay_*this.factor_;return this.backoffDelay_};ExponentialBackoffStrategy.prototype.reset_=function(){this.backoffDelay_=0;this.nextBackoffDelay_=this.getInitialDelay()};module.exports=ExponentialBackoffStrategy},{"./strategy":50,precond:57,util:65}],49:[function(require,module,exports){var util=require("util");var BackoffStrategy=require("./strategy");function FibonacciBackoffStrategy(options){BackoffStrategy.call(this,options);this.backoffDelay_=0;this.nextBackoffDelay_=this.getInitialDelay()}util.inherits(FibonacciBackoffStrategy,BackoffStrategy);FibonacciBackoffStrategy.prototype.next_=function(){var backoffDelay=Math.min(this.nextBackoffDelay_,this.getMaxDelay());this.nextBackoffDelay_+=this.backoffDelay_;this.backoffDelay_=backoffDelay;return backoffDelay};FibonacciBackoffStrategy.prototype.reset_=function(){this.nextBackoffDelay_=this.getInitialDelay();this.backoffDelay_=0};module.exports=FibonacciBackoffStrategy},{"./strategy":50,util:65}],50:[function(require,module,exports){var events=require("events");var util=require("util");function isDef(value){return value!==undefined&&value!==null}function BackoffStrategy(options){options=options||{};if(isDef(options.initialDelay)&&options.initialDelay<1){throw new Error("The initial timeout must be greater than 0.")}else if(isDef(options.maxDelay)&&options.maxDelay<1){throw new Error("The maximal timeout must be greater than 0.")}this.initialDelay_=options.initialDelay||100;this.maxDelay_=options.maxDelay||1e4;if(this.maxDelay_<=this.initialDelay_){throw new Error("The maximal backoff delay must be "+"greater than the initial backoff delay.")}if(isDef(options.randomisationFactor)&&(options.randomisationFactor<0||options.randomisationFactor>1)){throw new Error("The randomisation factor must be between 0 and 1.")}this.randomisationFactor_=options.randomisationFactor||0}BackoffStrategy.prototype.getMaxDelay=function(){return this.maxDelay_};BackoffStrategy.prototype.getInitialDelay=function(){return this.initialDelay_};BackoffStrategy.prototype.next=function(){var backoffDelay=this.next_();var randomisationMultiple=1+Math.random()*this.randomisationFactor_;var randomizedDelay=Math.round(backoffDelay*randomisationMultiple);return randomizedDelay};BackoffStrategy.prototype.next_=function(){throw new Error("BackoffStrategy.next_() unimplemented.")};BackoffStrategy.prototype.reset=function(){this.reset_()};BackoffStrategy.prototype.reset_=function(){throw new Error("BackoffStrategy.reset_() unimplemented.")};module.exports=BackoffStrategy},{events:53,util:65}],51:[function(require,module,exports){var charenc={utf8:{stringToBytes:function(str){return charenc.bin.stringToBytes(unescape(encodeURIComponent(str)))},bytesToString:function(bytes){return decodeURIComponent(escape(charenc.bin.bytesToString(bytes)))}},bin:{stringToBytes:function(str){for(var bytes=[],i=0;i>>32-b},rotr:function(n,b){return n<<32-b|n>>>b},endian:function(n){if(n.constructor==Number){return crypt.rotl(n,8)&16711935|crypt.rotl(n,24)&4278255360}for(var i=0;i0;n--)bytes.push(Math.floor(Math.random()*256));return bytes},bytesToWords:function(bytes){for(var words=[],i=0,b=0;i>>5]|=bytes[i]<<24-b%32;return words},wordsToBytes:function(words){for(var bytes=[],b=0;b>>5]>>>24-b%32&255);return bytes},bytesToHex:function(bytes){for(var hex=[],i=0;i>>4).toString(16));hex.push((bytes[i]&15).toString(16))}return hex.join("")},hexToBytes:function(hex){for(var bytes=[],c=0;c>>6*(3-j)&63));else base64.push("=")}return base64.join("")},base64ToBytes:function(base64){base64=base64.replace(/[^A-Z0-9+\/]/gi,"");for(var bytes=[],i=0,imod4=0;i>>6-imod4*2)}return bytes}};module.exports=crypt})()},{}],53:[function(require,module,exports){var objectCreate=Object.create||objectCreatePolyfill;var objectKeys=Object.keys||objectKeysPolyfill;var bind=Function.prototype.bind||functionBindPolyfill;function EventEmitter(){if(!this._events||!Object.prototype.hasOwnProperty.call(this,"_events")){this._events=objectCreate(null);this._eventsCount=0}this._maxListeners=this._maxListeners||undefined}module.exports=EventEmitter;EventEmitter.EventEmitter=EventEmitter;EventEmitter.prototype._events=undefined;EventEmitter.prototype._maxListeners=undefined;var defaultMaxListeners=10;var hasDefineProperty;try{var o={};if(Object.defineProperty)Object.defineProperty(o,"x",{value:0});hasDefineProperty=o.x===0}catch(err){hasDefineProperty=false}if(hasDefineProperty){Object.defineProperty(EventEmitter,"defaultMaxListeners",{enumerable:true,get:function(){return defaultMaxListeners},set:function(arg){if(typeof arg!=="number"||arg<0||arg!==arg)throw new TypeError('"defaultMaxListeners" must be a positive number');defaultMaxListeners=arg}})}else{EventEmitter.defaultMaxListeners=defaultMaxListeners}EventEmitter.prototype.setMaxListeners=function setMaxListeners(n){if(typeof n!=="number"||n<0||isNaN(n))throw new TypeError('"n" argument must be a positive number');this._maxListeners=n;return this};function $getMaxListeners(that){if(that._maxListeners===undefined)return EventEmitter.defaultMaxListeners;return that._maxListeners}EventEmitter.prototype.getMaxListeners=function getMaxListeners(){return $getMaxListeners(this)};function emitNone(handler,isFn,self){if(isFn)handler.call(self);else{var len=handler.length;var listeners=arrayClone(handler,len);for(var i=0;i1)er=arguments[1];if(er instanceof Error){throw er}else{var err=new Error('Unhandled "error" event. ('+er+")");err.context=er;throw err}return false}handler=events[type];if(!handler)return false;var isFn=typeof handler==="function";len=arguments.length;switch(len){case 1:emitNone(handler,isFn,this);break;case 2:emitOne(handler,isFn,this,arguments[1]);break;case 3:emitTwo(handler,isFn,this,arguments[1],arguments[2]);break;case 4:emitThree(handler,isFn,this,arguments[1],arguments[2],arguments[3]);break;default:args=new Array(len-1);for(i=1;i0&&existing.length>m){existing.warned=true;var w=new Error("Possible EventEmitter memory leak detected. "+existing.length+' "'+String(type)+'" listeners '+"added. Use emitter.setMaxListeners() to "+"increase limit.");w.name="MaxListenersExceededWarning";w.emitter=target;w.type=type;w.count=existing.length;if(typeof console==="object"&&console.warn){console.warn("%s: %s",w.name,w.message)}}}}return target}EventEmitter.prototype.addListener=function addListener(type,listener){return _addListener(this,type,listener,false)};EventEmitter.prototype.on=EventEmitter.prototype.addListener;EventEmitter.prototype.prependListener=function prependListener(type,listener){return _addListener(this,type,listener,true)};function onceWrapper(){if(!this.fired){this.target.removeListener(this.type,this.wrapFn);this.fired=true;switch(arguments.length){case 0:return this.listener.call(this.target);case 1:return this.listener.call(this.target,arguments[0]);case 2:return this.listener.call(this.target,arguments[0],arguments[1]);case 3:return this.listener.call(this.target,arguments[0],arguments[1],arguments[2]);default:var args=new Array(arguments.length);for(var i=0;i=0;i--){if(list[i]===listener||list[i].listener===listener){originalListener=list[i].listener;position=i;break}}if(position<0)return this;if(position===0)list.shift();else spliceOne(list,position);if(list.length===1)events[type]=list[0];if(events.removeListener)this.emit("removeListener",type,originalListener||listener)}return this};EventEmitter.prototype.removeAllListeners=function removeAllListeners(type){var listeners,events,i;events=this._events;if(!events)return this;if(!events.removeListener){if(arguments.length===0){this._events=objectCreate(null);this._eventsCount=0}else if(events[type]){if(--this._eventsCount===0)this._events=objectCreate(null);else delete events[type]}return this}if(arguments.length===0){var keys=objectKeys(events);var key;for(i=0;i=0;i--){this.removeListener(type,listeners[i])}}return this};function _listeners(target,type,unwrap){var events=target._events;if(!events)return[];var evlistener=events[type];if(!evlistener)return[];if(typeof evlistener==="function")return unwrap?[evlistener.listener||evlistener]:[evlistener];return unwrap?unwrapListeners(evlistener):arrayClone(evlistener,evlistener.length)}EventEmitter.prototype.listeners=function listeners(type){return _listeners(this,type,true)};EventEmitter.prototype.rawListeners=function rawListeners(type){return _listeners(this,type,false)};EventEmitter.listenerCount=function(emitter,type){if(typeof emitter.listenerCount==="function"){return emitter.listenerCount(type)}else{return listenerCount.call(emitter,type)}};EventEmitter.prototype.listenerCount=listenerCount;function listenerCount(type){var events=this._events;if(events){var evlistener=events[type];if(typeof evlistener==="function"){return 1}else if(evlistener){return evlistener.length}}return 0}EventEmitter.prototype.eventNames=function eventNames(){return this._eventsCount>0?Reflect.ownKeys(this._events):[]};function spliceOne(list,index){for(var i=index,k=i+1,n=list.length;k=0&&level<=self.levels.SILENT){currentLevel=level;if(persist!==false){persistLevelIfPossible(level)}replaceLoggingMethods.call(self,level,name);if(typeof console===undefinedType&&level>>24)&16711935|(m[i]<<24|m[i]>>>8)&4278255360}m[l>>>5]|=128<>>9<<4)+14]=l;var FF=md5._ff,GG=md5._gg,HH=md5._hh,II=md5._ii;for(var i=0;i>>0;b=b+bb>>>0;c=c+cc>>>0;d=d+dd>>>0}return crypt.endian([a,b,c,d])};md5._ff=function(a,b,c,d,x,s,t){var n=a+(b&c|~b&d)+(x>>>0)+t;return(n<>>32-s)+b};md5._gg=function(a,b,c,d,x,s,t){var n=a+(b&d|c&~d)+(x>>>0)+t;return(n<>>32-s)+b};md5._hh=function(a,b,c,d,x,s,t){var n=a+(b^c^d)+(x>>>0)+t;return(n<>>32-s)+b};md5._ii=function(a,b,c,d,x,s,t){var n=a+(c^(b|~d))+(x>>>0)+t;return(n<>>32-s)+b};md5._blocksize=16;md5._digestsize=16;module.exports=function(message,options){if(message===undefined||message===null)throw new Error("Illegal argument "+message);var digestbytes=crypt.wordsToBytes(md5(message,options));return options&&options.asBytes?digestbytes:options&&options.asString?bin.bytesToString(digestbytes):crypt.bytesToHex(digestbytes)}})()},{charenc:51,crypt:52,"is-buffer":54}],57:[function(require,module,exports){module.exports=require("./lib/checks")},{"./lib/checks":58}],58:[function(require,module,exports){var util=require("util");var errors=module.exports=require("./errors");function failCheck(ExceptionConstructor,callee,messageFormat,formatArgs){messageFormat=messageFormat||"";var message=util.format.apply(this,[messageFormat].concat(formatArgs));var error=new ExceptionConstructor(message);Error.captureStackTrace(error,callee);throw error}function failArgumentCheck(callee,message,formatArgs){failCheck(errors.IllegalArgumentError,callee,message,formatArgs)}function failStateCheck(callee,message,formatArgs){failCheck(errors.IllegalStateError,callee,message,formatArgs)}module.exports.checkArgument=function(value,message){if(!value){failArgumentCheck(arguments.callee,message,Array.prototype.slice.call(arguments,2))}};module.exports.checkState=function(value,message){if(!value){failStateCheck(arguments.callee,message,Array.prototype.slice.call(arguments,2))}};module.exports.checkIsDef=function(value,message){if(value!==undefined){return value}failArgumentCheck(arguments.callee,message||"Expected value to be defined but was undefined.",Array.prototype.slice.call(arguments,2))};module.exports.checkIsDefAndNotNull=function(value,message){if(value!=null){return value}failArgumentCheck(arguments.callee,message||'Expected value to be defined and not null but got "'+typeOf(value)+'".',Array.prototype.slice.call(arguments,2))};function typeOf(value){var s=typeof value;if(s=="object"){if(!value){return"null"}else if(value instanceof Array){return"array"}}return s}function typeCheck(expect){return function(value,message){var type=typeOf(value);if(type==expect){return value}failArgumentCheck(arguments.callee,message||'Expected "'+expect+'" but got "'+type+'".',Array.prototype.slice.call(arguments,2))}}module.exports.checkIsString=typeCheck("string");module.exports.checkIsArray=typeCheck("array");module.exports.checkIsNumber=typeCheck("number");module.exports.checkIsBoolean=typeCheck("boolean");module.exports.checkIsFunction=typeCheck("function");module.exports.checkIsObject=typeCheck("object")},{"./errors":59,util:65}],59:[function(require,module,exports){var util=require("util");function IllegalArgumentError(message){Error.call(this,message);this.message=message}util.inherits(IllegalArgumentError,Error);IllegalArgumentError.prototype.name="IllegalArgumentError";function IllegalStateError(message){Error.call(this,message);this.message=message}util.inherits(IllegalStateError,Error);IllegalStateError.prototype.name="IllegalStateError";module.exports.IllegalStateError=IllegalStateError;module.exports.IllegalArgumentError=IllegalArgumentError},{util:65}],60:[function(require,module,exports){var process=module.exports={};var cachedSetTimeout;var cachedClearTimeout;function defaultSetTimout(){throw new Error("setTimeout has not been defined")}function defaultClearTimeout(){throw new Error("clearTimeout has not been defined")}(function(){try{if(typeof setTimeout==="function"){cachedSetTimeout=setTimeout}else{cachedSetTimeout=defaultSetTimout}}catch(e){cachedSetTimeout=defaultSetTimout}try{if(typeof clearTimeout==="function"){cachedClearTimeout=clearTimeout}else{cachedClearTimeout=defaultClearTimeout}}catch(e){cachedClearTimeout=defaultClearTimeout}})();function runTimeout(fun){if(cachedSetTimeout===setTimeout){return setTimeout(fun,0)}if((cachedSetTimeout===defaultSetTimout||!cachedSetTimeout)&&setTimeout){cachedSetTimeout=setTimeout;return setTimeout(fun,0)}try{return cachedSetTimeout(fun,0)}catch(e){try{return cachedSetTimeout.call(null,fun,0)}catch(e){return cachedSetTimeout.call(this,fun,0)}}}function runClearTimeout(marker){if(cachedClearTimeout===clearTimeout){return clearTimeout(marker)}if((cachedClearTimeout===defaultClearTimeout||!cachedClearTimeout)&&clearTimeout){cachedClearTimeout=clearTimeout;return clearTimeout(marker)}try{return cachedClearTimeout(marker)}catch(e){try{return cachedClearTimeout.call(null,marker)}catch(e){return cachedClearTimeout.call(this,marker)}}}var queue=[];var draining=false;var currentQueue;var queueIndex=-1;function cleanUpNextTick(){if(!draining||!currentQueue){return}draining=false;if(currentQueue.length){queue=currentQueue.concat(queue)}else{queueIndex=-1}if(queue.length){drainQueue()}}function drainQueue(){if(draining){return}var timeout=runTimeout(cleanUpNextTick);draining=true;var len=queue.length;while(len){currentQueue=queue;queue=[];while(++queueIndex1){for(var i=1;i=14393&&url.indexOf("?transport=udp")===-1});delete server.url;server.urls=isString?urls[0]:urls;return!!urls.length}})}function getCommonCapabilities(localCapabilities,remoteCapabilities){var commonCapabilities={codecs:[],headerExtensions:[],fecMechanisms:[]};var findCodecByPayloadType=function(pt,codecs){pt=parseInt(pt,10);for(var i=0;i0;i--){this._iceGatherers.push(new window.RTCIceGatherer({iceServers:config.iceServers,gatherPolicy:config.iceTransportPolicy}))}}else{config.iceCandidatePoolSize=0}this._config=config;this.transceivers=[];this._sdpSessionId=SDPUtils.generateSessionId();this._sdpSessionVersion=0;this._dtlsRole=undefined;this._isClosed=false};RTCPeerConnection.prototype.onicecandidate=null;RTCPeerConnection.prototype.onaddstream=null;RTCPeerConnection.prototype.ontrack=null;RTCPeerConnection.prototype.onremovestream=null;RTCPeerConnection.prototype.onsignalingstatechange=null;RTCPeerConnection.prototype.oniceconnectionstatechange=null;RTCPeerConnection.prototype.onicegatheringstatechange=null;RTCPeerConnection.prototype.onnegotiationneeded=null;RTCPeerConnection.prototype.ondatachannel=null;RTCPeerConnection.prototype._dispatchEvent=function(name,event){if(this._isClosed){return}this.dispatchEvent(event);if(typeof this["on"+name]==="function"){this["on"+name](event)}};RTCPeerConnection.prototype._emitGatheringStateChange=function(){var event=new Event("icegatheringstatechange");this._dispatchEvent("icegatheringstatechange",event)};RTCPeerConnection.prototype.getConfiguration=function(){return this._config};RTCPeerConnection.prototype.getLocalStreams=function(){return this.localStreams};RTCPeerConnection.prototype.getRemoteStreams=function(){return this.remoteStreams};RTCPeerConnection.prototype._createTransceiver=function(kind){var hasBundleTransport=this.transceivers.length>0;var transceiver={track:null,iceGatherer:null,iceTransport:null,dtlsTransport:null,localCapabilities:null,remoteCapabilities:null,rtpSender:null,rtpReceiver:null,kind:kind,mid:null,sendEncodingParameters:null,recvEncodingParameters:null,stream:null,associatedRemoteMediaStreams:[],wantReceive:true};if(this.usingBundle&&hasBundleTransport){transceiver.iceTransport=this.transceivers[0].iceTransport;transceiver.dtlsTransport=this.transceivers[0].dtlsTransport}else{var transports=this._createIceAndDtlsTransports();transceiver.iceTransport=transports.iceTransport;transceiver.dtlsTransport=transports.dtlsTransport}this.transceivers.push(transceiver);return transceiver};RTCPeerConnection.prototype.addTrack=function(track,stream){if(this._isClosed){throw makeError("InvalidStateError","Attempted to call addTrack on a closed peerconnection.")}var alreadyExists=this.transceivers.find(function(s){return s.track===track});if(alreadyExists){throw makeError("InvalidAccessError","Track already exists.")}var transceiver;for(var i=0;i=15025){stream.getTracks().forEach(function(track){pc.addTrack(track,stream)})}else{var clonedStream=stream.clone();stream.getTracks().forEach(function(track,idx){var clonedTrack=clonedStream.getTracks()[idx];track.addEventListener("enabled",function(event){clonedTrack.enabled=event.enabled})});clonedStream.getTracks().forEach(function(track){pc.addTrack(track,clonedStream)})}};RTCPeerConnection.prototype.removeTrack=function(sender){if(this._isClosed){throw makeError("InvalidStateError","Attempted to call removeTrack on a closed peerconnection.")}if(!(sender instanceof window.RTCRtpSender)){throw new TypeError("Argument 1 of RTCPeerConnection.removeTrack "+"does not implement interface RTCRtpSender.")}var transceiver=this.transceivers.find(function(t){return t.rtpSender===sender});if(!transceiver){throw makeError("InvalidAccessError","Sender was not created by this connection.")}var stream=transceiver.stream;transceiver.rtpSender.stop();transceiver.rtpSender=null;transceiver.track=null;transceiver.stream=null;var localStreams=this.transceivers.map(function(t){return t.stream});if(localStreams.indexOf(stream)===-1&&this.localStreams.indexOf(stream)>-1){this.localStreams.splice(this.localStreams.indexOf(stream),1)}this._maybeFireNegotiationNeeded()};RTCPeerConnection.prototype.removeStream=function(stream){var pc=this;stream.getTracks().forEach(function(track){var sender=pc.getSenders().find(function(s){return s.track===track});if(sender){pc.removeTrack(sender)}})};RTCPeerConnection.prototype.getSenders=function(){return this.transceivers.filter(function(transceiver){return!!transceiver.rtpSender}).map(function(transceiver){return transceiver.rtpSender})};RTCPeerConnection.prototype.getReceivers=function(){return this.transceivers.filter(function(transceiver){return!!transceiver.rtpReceiver}).map(function(transceiver){return transceiver.rtpReceiver})};RTCPeerConnection.prototype._createIceGatherer=function(sdpMLineIndex,usingBundle){var pc=this;if(usingBundle&&sdpMLineIndex>0){return this.transceivers[0].iceGatherer}else if(this._iceGatherers.length){return this._iceGatherers.shift()}var iceGatherer=new window.RTCIceGatherer({iceServers:this._config.iceServers,gatherPolicy:this._config.iceTransportPolicy});Object.defineProperty(iceGatherer,"state",{value:"new",writable:true});this.transceivers[sdpMLineIndex].bufferedCandidateEvents=[];this.transceivers[sdpMLineIndex].bufferCandidates=function(event){var end=!event.candidate||Object.keys(event.candidate).length===0;iceGatherer.state=end?"completed":"gathering";if(pc.transceivers[sdpMLineIndex].bufferedCandidateEvents!==null){pc.transceivers[sdpMLineIndex].bufferedCandidateEvents.push(event)}};iceGatherer.addEventListener("localcandidate",this.transceivers[sdpMLineIndex].bufferCandidates);return iceGatherer};RTCPeerConnection.prototype._gather=function(mid,sdpMLineIndex){var pc=this;var iceGatherer=this.transceivers[sdpMLineIndex].iceGatherer;if(iceGatherer.onlocalcandidate){return}var bufferedCandidateEvents=this.transceivers[sdpMLineIndex].bufferedCandidateEvents;this.transceivers[sdpMLineIndex].bufferedCandidateEvents=null;iceGatherer.removeEventListener("localcandidate",this.transceivers[sdpMLineIndex].bufferCandidates);iceGatherer.onlocalcandidate=function(evt){if(pc.usingBundle&&sdpMLineIndex>0){return}var event=new Event("icecandidate");event.candidate={sdpMid:mid,sdpMLineIndex:sdpMLineIndex};var cand=evt.candidate;var end=!cand||Object.keys(cand).length===0;if(end){if(iceGatherer.state==="new"||iceGatherer.state==="gathering"){iceGatherer.state="completed"}}else{if(iceGatherer.state==="new"){iceGatherer.state="gathering"}cand.component=1;var serializedCandidate=SDPUtils.writeCandidate(cand);event.candidate=Object.assign(event.candidate,SDPUtils.parseCandidate(serializedCandidate));event.candidate.candidate=serializedCandidate}var sections=SDPUtils.getMediaSections(pc.localDescription.sdp);if(!end){sections[event.candidate.sdpMLineIndex]+="a="+event.candidate.candidate+"\r\n"}else{sections[event.candidate.sdpMLineIndex]+="a=end-of-candidates\r\n"}pc.localDescription.sdp=SDPUtils.getDescription(pc.localDescription.sdp)+sections.join("");var complete=pc.transceivers.every(function(transceiver){return transceiver.iceGatherer&&transceiver.iceGatherer.state==="completed"});if(pc.iceGatheringState!=="gathering"){pc.iceGatheringState="gathering";pc._emitGatheringStateChange()}if(!end){pc._dispatchEvent("icecandidate",event)}if(complete){pc._dispatchEvent("icecandidate",new Event("icecandidate"));pc.iceGatheringState="complete";pc._emitGatheringStateChange()}};window.setTimeout(function(){bufferedCandidateEvents.forEach(function(e){iceGatherer.onlocalcandidate(e)})},0)};RTCPeerConnection.prototype._createIceAndDtlsTransports=function(){var pc=this;var iceTransport=new window.RTCIceTransport(null);iceTransport.onicestatechange=function(){pc._updateConnectionState()};var dtlsTransport=new window.RTCDtlsTransport(iceTransport);dtlsTransport.ondtlsstatechange=function(){pc._updateConnectionState()};dtlsTransport.onerror=function(){Object.defineProperty(dtlsTransport,"state",{value:"failed",writable:true});pc._updateConnectionState()};return{iceTransport:iceTransport,dtlsTransport:dtlsTransport}};RTCPeerConnection.prototype._disposeIceAndDtlsTransports=function(sdpMLineIndex){var iceGatherer=this.transceivers[sdpMLineIndex].iceGatherer;if(iceGatherer){delete iceGatherer.onlocalcandidate;delete this.transceivers[sdpMLineIndex].iceGatherer}var iceTransport=this.transceivers[sdpMLineIndex].iceTransport;if(iceTransport){delete iceTransport.onicestatechange;delete this.transceivers[sdpMLineIndex].iceTransport}var dtlsTransport=this.transceivers[sdpMLineIndex].dtlsTransport;if(dtlsTransport){delete dtlsTransport.ondtlsstatechange;delete dtlsTransport.onerror;delete this.transceivers[sdpMLineIndex].dtlsTransport}};RTCPeerConnection.prototype._transceive=function(transceiver,send,recv){var params=getCommonCapabilities(transceiver.localCapabilities,transceiver.remoteCapabilities);if(send&&transceiver.rtpSender){params.encodings=transceiver.sendEncodingParameters;params.rtcp={cname:SDPUtils.localCName,compound:transceiver.rtcpParameters.compound};if(transceiver.recvEncodingParameters.length){params.rtcp.ssrc=transceiver.recvEncodingParameters[0].ssrc}transceiver.rtpSender.send(params)}if(recv&&transceiver.rtpReceiver&¶ms.codecs.length>0){if(transceiver.kind==="video"&&transceiver.recvEncodingParameters&&edgeVersion<15019){transceiver.recvEncodingParameters.forEach(function(p){delete p.rtx})}if(transceiver.recvEncodingParameters.length){params.encodings=transceiver.recvEncodingParameters}else{params.encodings=[{}]}params.rtcp={compound:transceiver.rtcpParameters.compound};if(transceiver.rtcpParameters.cname){params.rtcp.cname=transceiver.rtcpParameters.cname}if(transceiver.sendEncodingParameters.length){params.rtcp.ssrc=transceiver.sendEncodingParameters[0].ssrc}transceiver.rtpReceiver.receive(params)}};RTCPeerConnection.prototype.setLocalDescription=function(description){var pc=this;if(["offer","answer"].indexOf(description.type)===-1){return Promise.reject(makeError("TypeError",'Unsupported type "'+description.type+'"'))}if(!isActionAllowedInSignalingState("setLocalDescription",description.type,pc.signalingState)||pc._isClosed){return Promise.reject(makeError("InvalidStateError","Can not set local "+description.type+" in state "+pc.signalingState))}var sections;var sessionpart;if(description.type==="offer"){sections=SDPUtils.splitSections(description.sdp);sessionpart=sections.shift();sections.forEach(function(mediaSection,sdpMLineIndex){var caps=SDPUtils.parseRtpParameters(mediaSection);pc.transceivers[sdpMLineIndex].localCapabilities=caps});pc.transceivers.forEach(function(transceiver,sdpMLineIndex){pc._gather(transceiver.mid,sdpMLineIndex)})}else if(description.type==="answer"){sections=SDPUtils.splitSections(pc.remoteDescription.sdp);sessionpart=sections.shift();var isIceLite=SDPUtils.matchPrefix(sessionpart,"a=ice-lite").length>0;sections.forEach(function(mediaSection,sdpMLineIndex){var transceiver=pc.transceivers[sdpMLineIndex];var iceGatherer=transceiver.iceGatherer;var iceTransport=transceiver.iceTransport;var dtlsTransport=transceiver.dtlsTransport;var localCapabilities=transceiver.localCapabilities;var remoteCapabilities=transceiver.remoteCapabilities;var rejected=SDPUtils.isRejected(mediaSection)&&SDPUtils.matchPrefix(mediaSection,"a=bundle-only").length===0;if(!rejected&&!transceiver.isDatachannel){var remoteIceParameters=SDPUtils.getIceParameters(mediaSection,sessionpart);var remoteDtlsParameters=SDPUtils.getDtlsParameters(mediaSection,sessionpart);if(isIceLite){remoteDtlsParameters.role="server"}if(!pc.usingBundle||sdpMLineIndex===0){pc._gather(transceiver.mid,sdpMLineIndex);if(iceTransport.state==="new"){iceTransport.start(iceGatherer,remoteIceParameters,isIceLite?"controlling":"controlled")}if(dtlsTransport.state==="new"){dtlsTransport.start(remoteDtlsParameters)}}var params=getCommonCapabilities(localCapabilities,remoteCapabilities);pc._transceive(transceiver,params.codecs.length>0,false)}})}pc.localDescription={type:description.type,sdp:description.sdp};if(description.type==="offer"){pc._updateSignalingState("have-local-offer")}else{pc._updateSignalingState("stable")}return Promise.resolve()};RTCPeerConnection.prototype.setRemoteDescription=function(description){var pc=this;if(["offer","answer"].indexOf(description.type)===-1){return Promise.reject(makeError("TypeError",'Unsupported type "'+description.type+'"'))}if(!isActionAllowedInSignalingState("setRemoteDescription",description.type,pc.signalingState)||pc._isClosed){return Promise.reject(makeError("InvalidStateError","Can not set remote "+description.type+" in state "+pc.signalingState))}var streams={};pc.remoteStreams.forEach(function(stream){streams[stream.id]=stream});var receiverList=[];var sections=SDPUtils.splitSections(description.sdp);var sessionpart=sections.shift();var isIceLite=SDPUtils.matchPrefix(sessionpart,"a=ice-lite").length>0;var usingBundle=SDPUtils.matchPrefix(sessionpart,"a=group:BUNDLE ").length>0;pc.usingBundle=usingBundle;var iceOptions=SDPUtils.matchPrefix(sessionpart,"a=ice-options:")[0];if(iceOptions){pc.canTrickleIceCandidates=iceOptions.substr(14).split(" ").indexOf("trickle")>=0}else{pc.canTrickleIceCandidates=false}sections.forEach(function(mediaSection,sdpMLineIndex){var lines=SDPUtils.splitLines(mediaSection);var kind=SDPUtils.getKind(mediaSection);var rejected=SDPUtils.isRejected(mediaSection)&&SDPUtils.matchPrefix(mediaSection,"a=bundle-only").length===0;var protocol=lines[0].substr(2).split(" ")[2];var direction=SDPUtils.getDirection(mediaSection,sessionpart);var remoteMsid=SDPUtils.parseMsid(mediaSection);var mid=SDPUtils.getMid(mediaSection)||SDPUtils.generateIdentifier();if(kind==="application"&&protocol==="DTLS/SCTP"){pc.transceivers[sdpMLineIndex]={mid:mid,isDatachannel:true};return}var transceiver;var iceGatherer;var iceTransport;var dtlsTransport;var rtpReceiver;var sendEncodingParameters;var recvEncodingParameters;var localCapabilities;var track;var remoteCapabilities=SDPUtils.parseRtpParameters(mediaSection);var remoteIceParameters;var remoteDtlsParameters;if(!rejected){remoteIceParameters=SDPUtils.getIceParameters(mediaSection,sessionpart);remoteDtlsParameters=SDPUtils.getDtlsParameters(mediaSection,sessionpart);remoteDtlsParameters.role="client"}recvEncodingParameters=SDPUtils.parseRtpEncodingParameters(mediaSection);var rtcpParameters=SDPUtils.parseRtcpParameters(mediaSection);var isComplete=SDPUtils.matchPrefix(mediaSection,"a=end-of-candidates",sessionpart).length>0;var cands=SDPUtils.matchPrefix(mediaSection,"a=candidate:").map(function(cand){return SDPUtils.parseCandidate(cand)}).filter(function(cand){return cand.component===1});if((description.type==="offer"||description.type==="answer")&&!rejected&&usingBundle&&sdpMLineIndex>0&&pc.transceivers[sdpMLineIndex]){pc._disposeIceAndDtlsTransports(sdpMLineIndex);pc.transceivers[sdpMLineIndex].iceGatherer=pc.transceivers[0].iceGatherer;pc.transceivers[sdpMLineIndex].iceTransport=pc.transceivers[0].iceTransport;pc.transceivers[sdpMLineIndex].dtlsTransport=pc.transceivers[0].dtlsTransport;if(pc.transceivers[sdpMLineIndex].rtpSender){pc.transceivers[sdpMLineIndex].rtpSender.setTransport(pc.transceivers[0].dtlsTransport)}if(pc.transceivers[sdpMLineIndex].rtpReceiver){pc.transceivers[sdpMLineIndex].rtpReceiver.setTransport(pc.transceivers[0].dtlsTransport)}}if(description.type==="offer"&&!rejected){transceiver=pc.transceivers[sdpMLineIndex]||pc._createTransceiver(kind);transceiver.mid=mid;if(!transceiver.iceGatherer){transceiver.iceGatherer=pc._createIceGatherer(sdpMLineIndex,usingBundle)}if(cands.length&&transceiver.iceTransport.state==="new"){if(isComplete&&(!usingBundle||sdpMLineIndex===0)){transceiver.iceTransport.setRemoteCandidates(cands)}else{cands.forEach(function(candidate){maybeAddCandidate(transceiver.iceTransport,candidate)})}}localCapabilities=window.RTCRtpReceiver.getCapabilities(kind);if(edgeVersion<15019){localCapabilities.codecs=localCapabilities.codecs.filter(function(codec){return codec.name!=="rtx"})}sendEncodingParameters=transceiver.sendEncodingParameters||[{ssrc:(2*sdpMLineIndex+2)*1001}];var isNewTrack=false;if(direction==="sendrecv"||direction==="sendonly"){isNewTrack=!transceiver.rtpReceiver;rtpReceiver=transceiver.rtpReceiver||new window.RTCRtpReceiver(transceiver.dtlsTransport,kind);if(isNewTrack){var stream;track=rtpReceiver.track;if(remoteMsid&&remoteMsid.stream==="-"){}else if(remoteMsid){if(!streams[remoteMsid.stream]){streams[remoteMsid.stream]=new window.MediaStream;Object.defineProperty(streams[remoteMsid.stream],"id",{get:function(){return remoteMsid.stream}})}Object.defineProperty(track,"id",{get:function(){return remoteMsid.track}});stream=streams[remoteMsid.stream]}else{if(!streams.default){streams.default=new window.MediaStream}stream=streams.default}if(stream){addTrackToStreamAndFireEvent(track,stream);transceiver.associatedRemoteMediaStreams.push(stream)}receiverList.push([track,rtpReceiver,stream])}}else if(transceiver.rtpReceiver&&transceiver.rtpReceiver.track){transceiver.associatedRemoteMediaStreams.forEach(function(s){var nativeTrack=s.getTracks().find(function(t){return t.id===transceiver.rtpReceiver.track.id});if(nativeTrack){removeTrackFromStreamAndFireEvent(nativeTrack,s)}});transceiver.associatedRemoteMediaStreams=[]}transceiver.localCapabilities=localCapabilities;transceiver.remoteCapabilities=remoteCapabilities;transceiver.rtpReceiver=rtpReceiver;transceiver.rtcpParameters=rtcpParameters;transceiver.sendEncodingParameters=sendEncodingParameters;transceiver.recvEncodingParameters=recvEncodingParameters;pc._transceive(pc.transceivers[sdpMLineIndex],false,isNewTrack)}else if(description.type==="answer"&&!rejected){transceiver=pc.transceivers[sdpMLineIndex];iceGatherer=transceiver.iceGatherer;iceTransport=transceiver.iceTransport;dtlsTransport=transceiver.dtlsTransport;rtpReceiver=transceiver.rtpReceiver;sendEncodingParameters=transceiver.sendEncodingParameters;localCapabilities=transceiver.localCapabilities;pc.transceivers[sdpMLineIndex].recvEncodingParameters=recvEncodingParameters;pc.transceivers[sdpMLineIndex].remoteCapabilities=remoteCapabilities;pc.transceivers[sdpMLineIndex].rtcpParameters=rtcpParameters;if(cands.length&&iceTransport.state==="new"){if((isIceLite||isComplete)&&(!usingBundle||sdpMLineIndex===0)){iceTransport.setRemoteCandidates(cands)}else{cands.forEach(function(candidate){maybeAddCandidate(transceiver.iceTransport,candidate)})}}if(!usingBundle||sdpMLineIndex===0){if(iceTransport.state==="new"){iceTransport.start(iceGatherer,remoteIceParameters,"controlling")}if(dtlsTransport.state==="new"){dtlsTransport.start(remoteDtlsParameters)}}pc._transceive(transceiver,direction==="sendrecv"||direction==="recvonly",direction==="sendrecv"||direction==="sendonly");if(rtpReceiver&&(direction==="sendrecv"||direction==="sendonly")){track=rtpReceiver.track;if(remoteMsid){if(!streams[remoteMsid.stream]){streams[remoteMsid.stream]=new window.MediaStream}addTrackToStreamAndFireEvent(track,streams[remoteMsid.stream]);receiverList.push([track,rtpReceiver,streams[remoteMsid.stream]])}else{if(!streams.default){streams.default=new window.MediaStream}addTrackToStreamAndFireEvent(track,streams.default);receiverList.push([track,rtpReceiver,streams.default])}}else{delete transceiver.rtpReceiver}}});if(pc._dtlsRole===undefined){pc._dtlsRole=description.type==="offer"?"active":"passive"}pc.remoteDescription={type:description.type,sdp:description.sdp};if(description.type==="offer"){pc._updateSignalingState("have-remote-offer")}else{pc._updateSignalingState("stable")}Object.keys(streams).forEach(function(sid){var stream=streams[sid];if(stream.getTracks().length){if(pc.remoteStreams.indexOf(stream)===-1){pc.remoteStreams.push(stream);var event=new Event("addstream");event.stream=stream;window.setTimeout(function(){pc._dispatchEvent("addstream",event)})}receiverList.forEach(function(item){var track=item[0];var receiver=item[1];if(stream.id!==item[2].id){return}fireAddTrack(pc,track,receiver,[stream])})}});receiverList.forEach(function(item){if(item[2]){return}fireAddTrack(pc,item[0],item[1],[])});window.setTimeout(function(){if(!(pc&&pc.transceivers)){return}pc.transceivers.forEach(function(transceiver){if(transceiver.iceTransport&&transceiver.iceTransport.state==="new"&&transceiver.iceTransport.getRemoteCandidates().length>0){console.warn("Timeout for addRemoteCandidate. Consider sending "+"an end-of-candidates notification");transceiver.iceTransport.addRemoteCandidate({})}})},4e3);return Promise.resolve()};RTCPeerConnection.prototype.close=function(){this.transceivers.forEach(function(transceiver){if(transceiver.iceTransport){transceiver.iceTransport.stop()}if(transceiver.dtlsTransport){transceiver.dtlsTransport.stop()}if(transceiver.rtpSender){transceiver.rtpSender.stop()}if(transceiver.rtpReceiver){transceiver.rtpReceiver.stop()}});this._isClosed=true;this._updateSignalingState("closed")};RTCPeerConnection.prototype._updateSignalingState=function(newState){this.signalingState=newState;var event=new Event("signalingstatechange");this._dispatchEvent("signalingstatechange",event)};RTCPeerConnection.prototype._maybeFireNegotiationNeeded=function(){var pc=this;if(this.signalingState!=="stable"||this.needNegotiation===true){return}this.needNegotiation=true;window.setTimeout(function(){if(pc.needNegotiation){pc.needNegotiation=false;var event=new Event("negotiationneeded");pc._dispatchEvent("negotiationneeded",event)}},0)};RTCPeerConnection.prototype._updateConnectionState=function(){var newState;var states={new:0,closed:0,connecting:0,checking:0,connected:0,completed:0,disconnected:0,failed:0};this.transceivers.forEach(function(transceiver){states[transceiver.iceTransport.state]++;states[transceiver.dtlsTransport.state]++});states.connected+=states.completed;newState="new";if(states.failed>0){newState="failed"}else if(states.connecting>0||states.checking>0){newState="connecting"}else if(states.disconnected>0){newState="disconnected"}else if(states.new>0){newState="new"}else if(states.connected>0||states.completed>0){newState="connected"}if(newState!==this.iceConnectionState){this.iceConnectionState=newState;var event=new Event("iceconnectionstatechange");this._dispatchEvent("iceconnectionstatechange",event)}};RTCPeerConnection.prototype.createOffer=function(){var pc=this;if(pc._isClosed){return Promise.reject(makeError("InvalidStateError","Can not call createOffer after close"))}var numAudioTracks=pc.transceivers.filter(function(t){return t.kind==="audio"}).length;var numVideoTracks=pc.transceivers.filter(function(t){return t.kind==="video"}).length;var offerOptions=arguments[0];if(offerOptions){if(offerOptions.mandatory||offerOptions.optional){throw new TypeError("Legacy mandatory/optional constraints not supported.")}if(offerOptions.offerToReceiveAudio!==undefined){if(offerOptions.offerToReceiveAudio===true){numAudioTracks=1}else if(offerOptions.offerToReceiveAudio===false){numAudioTracks=0}else{numAudioTracks=offerOptions.offerToReceiveAudio}}if(offerOptions.offerToReceiveVideo!==undefined){if(offerOptions.offerToReceiveVideo===true){numVideoTracks=1}else if(offerOptions.offerToReceiveVideo===false){numVideoTracks=0}else{numVideoTracks=offerOptions.offerToReceiveVideo}}}pc.transceivers.forEach(function(transceiver){if(transceiver.kind==="audio"){numAudioTracks--;if(numAudioTracks<0){transceiver.wantReceive=false}}else if(transceiver.kind==="video"){numVideoTracks--;if(numVideoTracks<0){transceiver.wantReceive=false}}});while(numAudioTracks>0||numVideoTracks>0){if(numAudioTracks>0){pc._createTransceiver("audio");numAudioTracks--}if(numVideoTracks>0){pc._createTransceiver("video");numVideoTracks--}}var sdp=SDPUtils.writeSessionBoilerplate(pc._sdpSessionId,pc._sdpSessionVersion++);pc.transceivers.forEach(function(transceiver,sdpMLineIndex){var track=transceiver.track;var kind=transceiver.kind;var mid=transceiver.mid||SDPUtils.generateIdentifier();transceiver.mid=mid;if(!transceiver.iceGatherer){transceiver.iceGatherer=pc._createIceGatherer(sdpMLineIndex,pc.usingBundle)}var localCapabilities=window.RTCRtpSender.getCapabilities(kind);if(edgeVersion<15019){localCapabilities.codecs=localCapabilities.codecs.filter(function(codec){return codec.name!=="rtx"})}localCapabilities.codecs.forEach(function(codec){if(codec.name==="H264"&&codec.parameters["level-asymmetry-allowed"]===undefined){codec.parameters["level-asymmetry-allowed"]="1"}if(transceiver.remoteCapabilities&&transceiver.remoteCapabilities.codecs){transceiver.remoteCapabilities.codecs.forEach(function(remoteCodec){if(codec.name.toLowerCase()===remoteCodec.name.toLowerCase()&&codec.clockRate===remoteCodec.clockRate){codec.preferredPayloadType=remoteCodec.payloadType}})}});localCapabilities.headerExtensions.forEach(function(hdrExt){var remoteExtensions=transceiver.remoteCapabilities&&transceiver.remoteCapabilities.headerExtensions||[];remoteExtensions.forEach(function(rHdrExt){if(hdrExt.uri===rHdrExt.uri){hdrExt.id=rHdrExt.id}})});var sendEncodingParameters=transceiver.sendEncodingParameters||[{ssrc:(2*sdpMLineIndex+1)*1001}];if(track){if(edgeVersion>=15019&&kind==="video"&&!sendEncodingParameters[0].rtx){sendEncodingParameters[0].rtx={ssrc:sendEncodingParameters[0].ssrc+1}}}if(transceiver.wantReceive){transceiver.rtpReceiver=new window.RTCRtpReceiver(transceiver.dtlsTransport,kind)}transceiver.localCapabilities=localCapabilities;transceiver.sendEncodingParameters=sendEncodingParameters});if(pc._config.bundlePolicy!=="max-compat"){sdp+="a=group:BUNDLE "+pc.transceivers.map(function(t){return t.mid}).join(" ")+"\r\n"}sdp+="a=ice-options:trickle\r\n";pc.transceivers.forEach(function(transceiver,sdpMLineIndex){sdp+=writeMediaSection(transceiver,transceiver.localCapabilities,"offer",transceiver.stream,pc._dtlsRole);sdp+="a=rtcp-rsize\r\n";if(transceiver.iceGatherer&&pc.iceGatheringState!=="new"&&(sdpMLineIndex===0||!pc.usingBundle)){transceiver.iceGatherer.getLocalCandidates().forEach(function(cand){cand.component=1;sdp+="a="+SDPUtils.writeCandidate(cand)+"\r\n"});if(transceiver.iceGatherer.state==="completed"){sdp+="a=end-of-candidates\r\n"}}});var desc=new window.RTCSessionDescription({type:"offer",sdp:sdp});return Promise.resolve(desc)};RTCPeerConnection.prototype.createAnswer=function(){var pc=this;if(pc._isClosed){return Promise.reject(makeError("InvalidStateError","Can not call createAnswer after close"))}var sdp=SDPUtils.writeSessionBoilerplate(pc._sdpSessionId,pc._sdpSessionVersion++);if(pc.usingBundle){sdp+="a=group:BUNDLE "+pc.transceivers.map(function(t){return t.mid}).join(" ")+"\r\n"}var mediaSectionsInOffer=SDPUtils.getMediaSections(pc.remoteDescription.sdp).length;pc.transceivers.forEach(function(transceiver,sdpMLineIndex){if(sdpMLineIndex+1>mediaSectionsInOffer){return}if(transceiver.isDatachannel){sdp+="m=application 0 DTLS/SCTP 5000\r\n"+"c=IN IP4 0.0.0.0\r\n"+"a=mid:"+transceiver.mid+"\r\n";return}if(transceiver.stream){var localTrack;if(transceiver.kind==="audio"){localTrack=transceiver.stream.getAudioTracks()[0]}else if(transceiver.kind==="video"){localTrack=transceiver.stream.getVideoTracks()[0]}if(localTrack){if(edgeVersion>=15019&&transceiver.kind==="video"&&!transceiver.sendEncodingParameters[0].rtx){transceiver.sendEncodingParameters[0].rtx={ssrc:transceiver.sendEncodingParameters[0].ssrc+1}}}}var commonCapabilities=getCommonCapabilities(transceiver.localCapabilities,transceiver.remoteCapabilities);var hasRtx=commonCapabilities.codecs.filter(function(c){return c.name.toLowerCase()==="rtx"}).length;if(!hasRtx&&transceiver.sendEncodingParameters[0].rtx){delete transceiver.sendEncodingParameters[0].rtx}sdp+=writeMediaSection(transceiver,commonCapabilities,"answer",transceiver.stream,pc._dtlsRole);if(transceiver.rtcpParameters&&transceiver.rtcpParameters.reducedSize){sdp+="a=rtcp-rsize\r\n"}});var desc=new window.RTCSessionDescription({type:"answer",sdp:sdp});return Promise.resolve(desc)};RTCPeerConnection.prototype.addIceCandidate=function(candidate){var pc=this;var sections;if(candidate&&!(candidate.sdpMLineIndex!==undefined||candidate.sdpMid)){return Promise.reject(new TypeError("sdpMLineIndex or sdpMid required"))}return new Promise(function(resolve,reject){if(!pc.remoteDescription){return reject(makeError("InvalidStateError","Can not add ICE candidate without a remote description"))}else if(!candidate||candidate.candidate===""){for(var j=0;j0?SDPUtils.parseCandidate(candidate.candidate):{};if(cand.protocol==="tcp"&&(cand.port===0||cand.port===9)){return resolve()}if(cand.component&&cand.component!==1){return resolve()}if(sdpMLineIndex===0||sdpMLineIndex>0&&transceiver.iceTransport!==pc.transceivers[0].iceTransport){if(!maybeAddCandidate(transceiver.iceTransport,cand)){return reject(makeError("OperationError","Can not add ICE candidate"))}}var candidateString=candidate.candidate.trim();if(candidateString.indexOf("a=")===0){candidateString=candidateString.substr(2)}sections=SDPUtils.getMediaSections(pc.remoteDescription.sdp);sections[sdpMLineIndex]+="a="+(cand.type?candidateString:"end-of-candidates")+"\r\n";pc.remoteDescription.sdp=sections.join("")}else{return reject(makeError("OperationError","Can not add ICE candidate"))}}resolve()})};RTCPeerConnection.prototype.getStats=function(){var promises=[];this.transceivers.forEach(function(transceiver){["rtpSender","rtpReceiver","iceGatherer","iceTransport","dtlsTransport"].forEach(function(method){if(transceiver[method]){promises.push(transceiver[method].getStats())}})});var fixStatsType=function(stat){return{inboundrtp:"inbound-rtp",outboundrtp:"outbound-rtp",candidatepair:"candidate-pair",localcandidate:"local-candidate",remotecandidate:"remote-candidate"}[stat.type]||stat.type};return new Promise(function(resolve){var results=new Map;Promise.all(promises).then(function(res){res.forEach(function(result){Object.keys(result).forEach(function(id){result[id].type=fixStatsType(result[id]);results.set(id,result[id])})});resolve(results)})})};var methods=["createOffer","createAnswer"];methods.forEach(function(method){var nativeMethod=RTCPeerConnection.prototype[method];RTCPeerConnection.prototype[method]=function(){var args=arguments;if(typeof args[0]==="function"||typeof args[1]==="function"){return nativeMethod.apply(this,[arguments[2]]).then(function(description){if(typeof args[0]==="function"){args[0].apply(null,[description])}},function(error){if(typeof args[1]==="function"){args[1].apply(null,[error])}})}return nativeMethod.apply(this,arguments)}});methods=["setLocalDescription","setRemoteDescription","addIceCandidate"];methods.forEach(function(method){var nativeMethod=RTCPeerConnection.prototype[method];RTCPeerConnection.prototype[method]=function(){var args=arguments;if(typeof args[1]==="function"||typeof args[2]==="function"){return nativeMethod.apply(this,arguments).then(function(){if(typeof args[1]==="function"){args[1].apply(null)}},function(error){if(typeof args[2]==="function"){args[2].apply(null,[error])}})}return nativeMethod.apply(this,arguments)}});["getStats"].forEach(function(method){var nativeMethod=RTCPeerConnection.prototype[method];RTCPeerConnection.prototype[method]=function(){var args=arguments;if(typeof args[1]==="function"){return nativeMethod.apply(this,arguments).then(function(){if(typeof args[1]==="function"){args[1].apply(null)}})}return nativeMethod.apply(this,arguments)}});return RTCPeerConnection}},{sdp:62}],62:[function(require,module,exports){"use strict";var SDPUtils={};SDPUtils.generateIdentifier=function(){return Math.random().toString(36).substr(2,10)};SDPUtils.localCName=SDPUtils.generateIdentifier();SDPUtils.splitLines=function(blob){return blob.trim().split("\n").map(function(line){return line.trim()})};SDPUtils.splitSections=function(blob){var parts=blob.split("\nm=");return parts.map(function(part,index){return(index>0?"m="+part:part).trim()+"\r\n"})};SDPUtils.getDescription=function(blob){var sections=SDPUtils.splitSections(blob);return sections&§ions[0]};SDPUtils.getMediaSections=function(blob){var sections=SDPUtils.splitSections(blob);sections.shift();return sections};SDPUtils.matchPrefix=function(blob,prefix){return SDPUtils.splitLines(blob).filter(function(line){return line.indexOf(prefix)===0})};SDPUtils.parseCandidate=function(line){var parts;if(line.indexOf("a=candidate:")===0){parts=line.substring(12).split(" ")}else{parts=line.substring(10).split(" ")}var candidate={foundation:parts[0],component:parseInt(parts[1],10),protocol:parts[2].toLowerCase(),priority:parseInt(parts[3],10),ip:parts[4],address:parts[4],port:parseInt(parts[5],10),type:parts[7]};for(var i=8;i0?parts[0].split("/")[1]:"sendrecv",uri:parts[1]}};SDPUtils.writeExtmap=function(headerExtension){return"a=extmap:"+(headerExtension.id||headerExtension.preferredId)+(headerExtension.direction&&headerExtension.direction!=="sendrecv"?"/"+headerExtension.direction:"")+" "+headerExtension.uri+"\r\n"};SDPUtils.parseFmtp=function(line){var parsed={};var kv;var parts=line.substr(line.indexOf(" ")+1).split(";");for(var j=0;j-1){parts.attribute=line.substr(sp+1,colon-sp-1);parts.value=line.substr(colon+1)}else{parts.attribute=line.substr(sp+1)}return parts};SDPUtils.parseSsrcGroup=function(line){var parts=line.substr(13).split(" ");return{semantics:parts.shift(),ssrcs:parts.map(function(ssrc){return parseInt(ssrc,10)})}};SDPUtils.getMid=function(mediaSection){var mid=SDPUtils.matchPrefix(mediaSection,"a=mid:")[0];if(mid){return mid.substr(6)}};SDPUtils.parseFingerprint=function(line){var parts=line.substr(14).split(" ");return{algorithm:parts[0].toLowerCase(),value:parts[1]}};SDPUtils.getDtlsParameters=function(mediaSection,sessionpart){var lines=SDPUtils.matchPrefix(mediaSection+sessionpart,"a=fingerprint:");return{role:"auto",fingerprints:lines.map(SDPUtils.parseFingerprint)}};SDPUtils.writeDtlsParameters=function(params,setupType){var sdp="a=setup:"+setupType+"\r\n";params.fingerprints.forEach(function(fp){sdp+="a=fingerprint:"+fp.algorithm+" "+fp.value+"\r\n"});return sdp};SDPUtils.parseCryptoLine=function(line){var parts=line.substr(9).split(" ");return{tag:parseInt(parts[0],10),cryptoSuite:parts[1],keyParams:parts[2],sessionParams:parts.slice(3)}};SDPUtils.writeCryptoLine=function(parameters){return"a=crypto:"+parameters.tag+" "+parameters.cryptoSuite+" "+(typeof parameters.keyParams==="object"?SDPUtils.writeCryptoKeyParams(parameters.keyParams):parameters.keyParams)+(parameters.sessionParams?" "+parameters.sessionParams.join(" "):"")+"\r\n"};SDPUtils.parseCryptoKeyParams=function(keyParams){if(keyParams.indexOf("inline:")!==0){return null}var parts=keyParams.substr(7).split("|");return{keyMethod:"inline",keySalt:parts[0],lifeTime:parts[1],mkiValue:parts[2]?parts[2].split(":")[0]:undefined,mkiLength:parts[2]?parts[2].split(":")[1]:undefined}};SDPUtils.writeCryptoKeyParams=function(keyParams){return keyParams.keyMethod+":"+keyParams.keySalt+(keyParams.lifeTime?"|"+keyParams.lifeTime:"")+(keyParams.mkiValue&&keyParams.mkiLength?"|"+keyParams.mkiValue+":"+keyParams.mkiLength:"")};SDPUtils.getCryptoParameters=function(mediaSection,sessionpart){var lines=SDPUtils.matchPrefix(mediaSection+sessionpart,"a=crypto:");return lines.map(SDPUtils.parseCryptoLine)};SDPUtils.getIceParameters=function(mediaSection,sessionpart){var ufrag=SDPUtils.matchPrefix(mediaSection+sessionpart,"a=ice-ufrag:")[0];var pwd=SDPUtils.matchPrefix(mediaSection+sessionpart,"a=ice-pwd:")[0];if(!(ufrag&&pwd)){return null}return{usernameFragment:ufrag.substr(12),password:pwd.substr(10)}};SDPUtils.writeIceParameters=function(params){return"a=ice-ufrag:"+params.usernameFragment+"\r\n"+"a=ice-pwd:"+params.password+"\r\n"};SDPUtils.parseRtpParameters=function(mediaSection){var description={codecs:[],headerExtensions:[],fecMechanisms:[],rtcp:[]};var lines=SDPUtils.splitLines(mediaSection);var mline=lines[0].split(" ");for(var i=3;i0?"9":"0";sdp+=" UDP/TLS/RTP/SAVPF ";sdp+=caps.codecs.map(function(codec){if(codec.preferredPayloadType!==undefined){return codec.preferredPayloadType}return codec.payloadType}).join(" ")+"\r\n";sdp+="c=IN IP4 0.0.0.0\r\n";sdp+="a=rtcp:9 IN IP4 0.0.0.0\r\n";caps.codecs.forEach(function(codec){sdp+=SDPUtils.writeRtpMap(codec);sdp+=SDPUtils.writeFmtp(codec);sdp+=SDPUtils.writeRtcpFb(codec)});var maxptime=0;caps.codecs.forEach(function(codec){if(codec.maxptime>maxptime){maxptime=codec.maxptime}});if(maxptime>0){sdp+="a=maxptime:"+maxptime+"\r\n"}sdp+="a=rtcp-mux\r\n";if(caps.headerExtensions){caps.headerExtensions.forEach(function(extension){sdp+=SDPUtils.writeExtmap(extension)})}return sdp};SDPUtils.parseRtpEncodingParameters=function(mediaSection){var encodingParameters=[];var description=SDPUtils.parseRtpParameters(mediaSection);var hasRed=description.fecMechanisms.indexOf("RED")!==-1;var hasUlpfec=description.fecMechanisms.indexOf("ULPFEC")!==-1;var ssrcs=SDPUtils.matchPrefix(mediaSection,"a=ssrc:").map(function(line){return SDPUtils.parseSsrcMedia(line)}).filter(function(parts){return parts.attribute==="cname"});var primarySsrc=ssrcs.length>0&&ssrcs[0].ssrc;var secondarySsrc;var flows=SDPUtils.matchPrefix(mediaSection,"a=ssrc-group:FID").map(function(line){var parts=line.substr(17).split(" ");return parts.map(function(part){return parseInt(part,10)})});if(flows.length>0&&flows[0].length>1&&flows[0][0]===primarySsrc){secondarySsrc=flows[0][1]}description.codecs.forEach(function(codec){if(codec.name.toUpperCase()==="RTX"&&codec.parameters.apt){var encParam={ssrc:primarySsrc,codecPayloadType:parseInt(codec.parameters.apt,10)};if(primarySsrc&&secondarySsrc){encParam.rtx={ssrc:secondarySsrc}}encodingParameters.push(encParam);if(hasRed){encParam=JSON.parse(JSON.stringify(encParam));encParam.fec={ssrc:primarySsrc,mechanism:hasUlpfec?"red+ulpfec":"red"};encodingParameters.push(encParam)}}});if(encodingParameters.length===0&&primarySsrc){encodingParameters.push({ssrc:primarySsrc})}var bandwidth=SDPUtils.matchPrefix(mediaSection,"b=");if(bandwidth.length){if(bandwidth[0].indexOf("b=TIAS:")===0){bandwidth=parseInt(bandwidth[0].substr(7),10)}else if(bandwidth[0].indexOf("b=AS:")===0){bandwidth=parseInt(bandwidth[0].substr(5),10)*1e3*.95-50*40*8}else{bandwidth=undefined}encodingParameters.forEach(function(params){params.maxBitrate=bandwidth})}return encodingParameters};SDPUtils.parseRtcpParameters=function(mediaSection){var rtcpParameters={};var remoteSsrc=SDPUtils.matchPrefix(mediaSection,"a=ssrc:").map(function(line){return SDPUtils.parseSsrcMedia(line)}).filter(function(obj){return obj.attribute==="cname"})[0];if(remoteSsrc){rtcpParameters.cname=remoteSsrc.value;rtcpParameters.ssrc=remoteSsrc.ssrc}var rsize=SDPUtils.matchPrefix(mediaSection,"a=rtcp-rsize");rtcpParameters.reducedSize=rsize.length>0;rtcpParameters.compound=rsize.length===0;var mux=SDPUtils.matchPrefix(mediaSection,"a=rtcp-mux");rtcpParameters.mux=mux.length>0;return rtcpParameters};SDPUtils.parseMsid=function(mediaSection){var parts;var spec=SDPUtils.matchPrefix(mediaSection,"a=msid:");if(spec.length===1){parts=spec[0].substr(7).split(" ");return{stream:parts[0],track:parts[1]}}var planB=SDPUtils.matchPrefix(mediaSection,"a=ssrc:").map(function(line){return SDPUtils.parseSsrcMedia(line)}).filter(function(msidParts){return msidParts.attribute==="msid"});if(planB.length>0){parts=planB[0].value.split(" ");return{stream:parts[0],track:parts[1]}}};SDPUtils.parseSctpDescription=function(mediaSection){var mline=SDPUtils.parseMLine(mediaSection);var maxSizeLine=SDPUtils.matchPrefix(mediaSection,"a=max-message-size:");var maxMessageSize;if(maxSizeLine.length>0){maxMessageSize=parseInt(maxSizeLine[0].substr(19),10)}if(isNaN(maxMessageSize)){maxMessageSize=65536}var sctpPort=SDPUtils.matchPrefix(mediaSection,"a=sctp-port:");if(sctpPort.length>0){return{port:parseInt(sctpPort[0].substr(12),10),protocol:mline.fmt,maxMessageSize:maxMessageSize}}var sctpMapLines=SDPUtils.matchPrefix(mediaSection,"a=sctpmap:");if(sctpMapLines.length>0){var parts=SDPUtils.matchPrefix(mediaSection,"a=sctpmap:")[0].substr(10).split(" ");return{port:parseInt(parts[0],10),protocol:parts[1],maxMessageSize:maxMessageSize}}};SDPUtils.writeSctpDescription=function(media,sctp){var output=[];if(media.protocol!=="DTLS/SCTP"){output=["m="+media.kind+" 9 "+media.protocol+" "+sctp.protocol+"\r\n","c=IN IP4 0.0.0.0\r\n","a=sctp-port:"+sctp.port+"\r\n"]}else{output=["m="+media.kind+" 9 "+media.protocol+" "+sctp.port+"\r\n","c=IN IP4 0.0.0.0\r\n","a=sctpmap:"+sctp.port+" "+sctp.protocol+" 65535\r\n"]}if(sctp.maxMessageSize!==undefined){output.push("a=max-message-size:"+sctp.maxMessageSize+"\r\n")}return output.join("")};SDPUtils.generateSessionId=function(){return Math.random().toString().substr(2,21)};SDPUtils.writeSessionBoilerplate=function(sessId,sessVer,sessUser){var sessionId;var version=sessVer!==undefined?sessVer:2;if(sessId){sessionId=sessId}else{sessionId=SDPUtils.generateSessionId()}var user=sessUser||"thisisadapterortc";return"v=0\r\n"+"o="+user+" "+sessionId+" "+version+" IN IP4 127.0.0.1\r\n"+"s=-\r\n"+"t=0 0\r\n"};SDPUtils.writeMediaSection=function(transceiver,caps,type,stream){var sdp=SDPUtils.writeRtpDescription(transceiver.kind,caps);sdp+=SDPUtils.writeIceParameters(transceiver.iceGatherer.getLocalParameters());sdp+=SDPUtils.writeDtlsParameters(transceiver.dtlsTransport.getLocalParameters(),type==="offer"?"actpass":"active");sdp+="a=mid:"+transceiver.mid+"\r\n";if(transceiver.direction){sdp+="a="+transceiver.direction+"\r\n"}else if(transceiver.rtpSender&&transceiver.rtpReceiver){sdp+="a=sendrecv\r\n"}else if(transceiver.rtpSender){sdp+="a=sendonly\r\n"}else if(transceiver.rtpReceiver){sdp+="a=recvonly\r\n"}else{sdp+="a=inactive\r\n"}if(transceiver.rtpSender){var msid="msid:"+stream.id+" "+transceiver.rtpSender.track.id+"\r\n";sdp+="a="+msid;sdp+="a=ssrc:"+transceiver.sendEncodingParameters[0].ssrc+" "+msid;if(transceiver.sendEncodingParameters[0].rtx){sdp+="a=ssrc:"+transceiver.sendEncodingParameters[0].rtx.ssrc+" "+msid;sdp+="a=ssrc-group:FID "+transceiver.sendEncodingParameters[0].ssrc+" "+transceiver.sendEncodingParameters[0].rtx.ssrc+"\r\n"}}sdp+="a=ssrc:"+transceiver.sendEncodingParameters[0].ssrc+" cname:"+SDPUtils.localCName+"\r\n";if(transceiver.rtpSender&&transceiver.sendEncodingParameters[0].rtx){sdp+="a=ssrc:"+transceiver.sendEncodingParameters[0].rtx.ssrc+" cname:"+SDPUtils.localCName+"\r\n"}return sdp};SDPUtils.getDirection=function(mediaSection,sessionpart){var lines=SDPUtils.splitLines(mediaSection);for(var i=0;i=len)return x;switch(x){case"%s":return String(args[i++]);case"%d":return Number(args[i++]);case"%j":try{return JSON.stringify(args[i++])}catch(_){return"[Circular]"}default:return x}});for(var x=args[i];i=3)ctx.depth=arguments[2];if(arguments.length>=4)ctx.colors=arguments[3];if(isBoolean(opts)){ctx.showHidden=opts}else if(opts){exports._extend(ctx,opts)}if(isUndefined(ctx.showHidden))ctx.showHidden=false;if(isUndefined(ctx.depth))ctx.depth=2;if(isUndefined(ctx.colors))ctx.colors=false;if(isUndefined(ctx.customInspect))ctx.customInspect=true;if(ctx.colors)ctx.stylize=stylizeWithColor;return formatValue(ctx,obj,ctx.depth)}exports.inspect=inspect;inspect.colors={bold:[1,22],italic:[3,23],underline:[4,24],inverse:[7,27],white:[37,39],grey:[90,39],black:[30,39],blue:[34,39],cyan:[36,39],green:[32,39],magenta:[35,39],red:[31,39],yellow:[33,39]};inspect.styles={special:"cyan",number:"yellow",boolean:"yellow",undefined:"grey",null:"bold",string:"green",date:"magenta",regexp:"red"};function stylizeWithColor(str,styleType){var style=inspect.styles[styleType];if(style){return"\x1b["+inspect.colors[style][0]+"m"+str+"\x1b["+inspect.colors[style][1]+"m"}else{return str}}function stylizeNoColor(str,styleType){return str}function arrayToHash(array){var hash={};array.forEach(function(val,idx){hash[val]=true});return hash}function formatValue(ctx,value,recurseTimes){if(ctx.customInspect&&value&&isFunction(value.inspect)&&value.inspect!==exports.inspect&&!(value.constructor&&value.constructor.prototype===value)){var ret=value.inspect(recurseTimes,ctx);if(!isString(ret)){ret=formatValue(ctx,ret,recurseTimes)}return ret}var primitive=formatPrimitive(ctx,value);if(primitive){return primitive}var keys=Object.keys(value);var visibleKeys=arrayToHash(keys);if(ctx.showHidden){keys=Object.getOwnPropertyNames(value)}if(isError(value)&&(keys.indexOf("message")>=0||keys.indexOf("description")>=0)){return formatError(value)}if(keys.length===0){if(isFunction(value)){var name=value.name?": "+value.name:"";return ctx.stylize("[Function"+name+"]","special")}if(isRegExp(value)){return ctx.stylize(RegExp.prototype.toString.call(value),"regexp")}if(isDate(value)){return ctx.stylize(Date.prototype.toString.call(value),"date")}if(isError(value)){return formatError(value)}}var base="",array=false,braces=["{","}"];if(isArray(value)){array=true;braces=["[","]"]}if(isFunction(value)){var n=value.name?": "+value.name:"";base=" [Function"+n+"]"}if(isRegExp(value)){base=" "+RegExp.prototype.toString.call(value)}if(isDate(value)){base=" "+Date.prototype.toUTCString.call(value)}if(isError(value)){base=" "+formatError(value)}if(keys.length===0&&(!array||value.length==0)){return braces[0]+base+braces[1]}if(recurseTimes<0){if(isRegExp(value)){return ctx.stylize(RegExp.prototype.toString.call(value),"regexp")}else{return ctx.stylize("[Object]","special")}}ctx.seen.push(value);var output;if(array){output=formatArray(ctx,value,recurseTimes,visibleKeys,keys)}else{output=keys.map(function(key){return formatProperty(ctx,value,recurseTimes,visibleKeys,key,array)})}ctx.seen.pop();return reduceToSingleString(output,base,braces)}function formatPrimitive(ctx,value){if(isUndefined(value))return ctx.stylize("undefined","undefined");if(isString(value)){var simple="'"+JSON.stringify(value).replace(/^"|"$/g,"").replace(/'/g,"\\'").replace(/\\"/g,'"')+"'";return ctx.stylize(simple,"string")}if(isNumber(value))return ctx.stylize(""+value,"number");if(isBoolean(value))return ctx.stylize(""+value,"boolean");if(isNull(value))return ctx.stylize("null","null")}function formatError(value){return"["+Error.prototype.toString.call(value)+"]"}function formatArray(ctx,value,recurseTimes,visibleKeys,keys){var output=[];for(var i=0,l=value.length;i-1){if(array){str=str.split("\n").map(function(line){return" "+line}).join("\n").substr(2)}else{str="\n"+str.split("\n").map(function(line){return" "+line}).join("\n")}}}else{str=ctx.stylize("[Circular]","special")}}if(isUndefined(name)){if(array&&key.match(/^\d+$/)){return str}name=JSON.stringify(""+key);if(name.match(/^"([a-zA-Z_][a-zA-Z_0-9]*)"$/)){name=name.substr(1,name.length-2);name=ctx.stylize(name,"name")}else{name=name.replace(/'/g,"\\'").replace(/\\"/g,'"').replace(/(^"|"$)/g,"'");name=ctx.stylize(name,"string")}}return name+": "+str}function reduceToSingleString(output,base,braces){var numLinesEst=0;var length=output.reduce(function(prev,cur){numLinesEst++;if(cur.indexOf("\n")>=0)numLinesEst++;return prev+cur.replace(/\u001b\[\d\d?m/g,"").length+1},0);if(length>60){return braces[0]+(base===""?"":base+"\n ")+" "+output.join(",\n ")+" "+braces[1]}return braces[0]+base+" "+output.join(", ")+" "+braces[1]}function isArray(ar){return Array.isArray(ar)}exports.isArray=isArray;function isBoolean(arg){return typeof arg==="boolean"}exports.isBoolean=isBoolean;function isNull(arg){return arg===null}exports.isNull=isNull;function isNullOrUndefined(arg){return arg==null}exports.isNullOrUndefined=isNullOrUndefined;function isNumber(arg){return typeof arg==="number"}exports.isNumber=isNumber;function isString(arg){return typeof arg==="string"}exports.isString=isString;function isSymbol(arg){return typeof arg==="symbol"}exports.isSymbol=isSymbol;function isUndefined(arg){return arg===void 0}exports.isUndefined=isUndefined;function isRegExp(re){return isObject(re)&&objectToString(re)==="[object RegExp]"}exports.isRegExp=isRegExp;function isObject(arg){return typeof arg==="object"&&arg!==null}exports.isObject=isObject;function isDate(d){return isObject(d)&&objectToString(d)==="[object Date]"}exports.isDate=isDate;function isError(e){return isObject(e)&&(objectToString(e)==="[object Error]"||e instanceof Error)}exports.isError=isError;function isFunction(arg){return typeof arg==="function"}exports.isFunction=isFunction;function isPrimitive(arg){return arg===null||typeof arg==="boolean"||typeof arg==="number"||typeof arg==="string"||typeof arg==="symbol"||typeof arg==="undefined"}exports.isPrimitive=isPrimitive;exports.isBuffer=require("./support/isBuffer");function objectToString(o){return Object.prototype.toString.call(o)}function pad(n){return n<10?"0"+n.toString(10):n.toString(10)}var months=["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"];function timestamp(){var d=new Date;var time=[pad(d.getHours()),pad(d.getMinutes()),pad(d.getSeconds())].join(":");return[d.getDate(),months[d.getMonth()],time].join(" ")}exports.log=function(){console.log("%s - %s",timestamp(),exports.format.apply(exports,arguments))};exports.inherits=require("inherits");exports._extend=function(origin,add){if(!add||!isObject(add))return origin;var keys=Object.keys(add);var i=keys.length;while(i--){origin[keys[i]]=add[keys[i]]}return origin};function hasOwnProperty(obj,prop){return Object.prototype.hasOwnProperty.call(obj,prop)}}).call(this)}).call(this,require("_process"),typeof global!=="undefined"?global:typeof self!=="undefined"?self:typeof window!=="undefined"?window:{})},{"./support/isBuffer":64,_process:60,inherits:63}]},{},[3]);var Voice=bundle(3);if(typeof define==="function"&&define.amd){define([],function(){return Voice})}else{var Twilio=root.Twilio=root.Twilio||{};Twilio.Call=Twilio.Call||Voice.Call;Twilio.Device=Twilio.Device||Voice.Device;Twilio.PStream=Twilio.PStream||Voice.PStream;Twilio.PreflightTest=Twilio.PreflightTest||Voice.PreflightTest;Twilio.Logger=Twilio.Logger||Voice.Logger}})(typeof window!=="undefined"?window:typeof global!=="undefined"?global:this); \ No newline at end of file diff --git a/services/token-generator.js b/services/token-generator.js new file mode 100644 index 00000000..c2693496 --- /dev/null +++ b/services/token-generator.js @@ -0,0 +1,73 @@ +const VoiceResponse = require('twilio').twiml.VoiceResponse; +const AccessToken = require('twilio').jwt.AccessToken; +const VoiceGrant = AccessToken.VoiceGrant; + +const nameGenerator = require('../name_generator'); +const config = require('../config'); + +var identity; + +exports.tokenGenerator = function tokenGenerator() { + identity = nameGenerator(); + + console.log(identity); + const accessToken = new AccessToken( + config.accountSid, + config.apiKey, + config.apiSecret, + { identity: identity } + ); + accessToken.identity = identity; + const grant = new VoiceGrant({ + outgoingApplicationSid: config.twimlAppSid, + incomingAllow: true, + }); + accessToken.addGrant(grant); + + // Include identity and token in a JSON response + return { + identity: identity, + token: accessToken.toJwt(), + }; +}; + +exports.voiceResponse = function voiceResponse(requestBody) { + const toNumberOrClientName = requestBody.To; + const callerId = config.callerId; + let twiml = new VoiceResponse(); + + // If the request to the /voice endpoint is TO your Twilio Number, + // then it is an incoming call towards your Twilio.Device. + if (toNumberOrClientName == callerId) { + let dial = twiml.dial(); + + // This will connect the caller with your Twilio.Device/client + dial.client(identity); + + } else if (requestBody.To) { + // This is an outgoing call + + // set the callerId + let dial = twiml.dial({ callerId }); + + // Check if the 'To' parameter is a Phone Number or Client Name + // in order to use the appropriate TwiML noun + const attr = isAValidPhoneNumber(toNumberOrClientName) + ? 'number' + : 'client'; + dial[attr]({}, toNumberOrClientName); + } else { + twiml.say('Thanks for calling!'); + } + + return twiml.toString(); +}; + +/** + * Checks if the given value is valid as phone number + * @param {Number|String} number + * @return {Boolean} + */ +function isAValidPhoneNumber(number) { + return /^[\d\+\-\(\) ]+$/.test(number); +} From c31a9cbfd1a1883a92cfbd0f9ac69666b1abaf50 Mon Sep 17 00:00:00 2001 From: Chris Feehan Date: Tue, 13 Aug 2024 22:11:53 -0500 Subject: [PATCH 03/26] VoxRay Real Estate Example --- app.js | 88 ++++----- data/mock-database.js | 161 ++++++++++++++++ functions/available-functions.js | 238 +++++++++++++++++++++++ functions/function-manifest.js | 192 +++++++++++-------- prompts/prompt.js | 51 +++++ prompts/welcomePrompt.js | 2 + services/gpt-service.js | 316 ++++++++++++++++++++----------- services/text-service.js | 10 +- 8 files changed, 810 insertions(+), 248 deletions(-) create mode 100644 data/mock-database.js create mode 100644 functions/available-functions.js create mode 100644 prompts/prompt.js create mode 100644 prompts/welcomePrompt.js diff --git a/app.js b/app.js index cca3113b..2a31ef6b 100644 --- a/app.js +++ b/app.js @@ -1,85 +1,73 @@ -require('dotenv').config(); -require('colors'); +require("dotenv").config(); +require("colors"); -const path = require('path'); +const express = require("express"); +const ExpressWs = require("express-ws"); -const express = require('express'); -const ExpressWs = require('express-ws'); - -const { GptService } = require('./services/gpt-service'); -const { TextService } = require('./services/text-service'); -const { recordingService } = require('./services/recording-service'); -const { tokenGenerator } = require('./services/token-generator'); +const { GptService } = require("./services/gpt-service"); +const { TextService } = require("./services/text-service"); +const welcomePrompt = require("./prompts/welcomePrompt"); const app = express(); ExpressWs(app); const PORT = process.env.PORT || 3000; -app.use(express.static('public')); - - -app.get('/index.html', (req, res) => { - res.sendFile(path.join(__dirname, './index.html')); -}); - -app.get('/token', (req, res) => { - res.send(tokenGenerator()); -}); -app.post('/incoming', (req, res) => { +app.post("/incoming", (req, res) => { try { const response = ` - + `; - res.type('text/xml'); + res.type("text/xml"); res.end(response.toString()); } catch (err) { console.log(err); } }); -app.ws('/sockets', (ws) => { +app.ws("/sockets", (ws) => { try { - ws.on('error', console.error); - // Filled in from start message - let streamSid; - let callSid; + ws.on("error", console.error); const gptService = new GptService(); const textService = new TextService(ws); let interactionCount = 0; - + let awaitingUserInput = false; + // Incoming from MediaStream - ws.on('message', function message(data) { + ws.on("message", async function message(data) { const msg = JSON.parse(data); - console.log(msg); - if (msg.type === 'setup') { - callSid = msg.callSid; - gptService.setCallSid(callSid); + console.log(`[App.js] Message received: ${JSON.stringify(msg)}`); + + if (awaitingUserInput) { + console.log( + "[App.js] Still awaiting user input, skipping new API call." + ); + return; + } - // Set RECORDING_ENABLED='true' in .env to record calls - recordingService(textService, callSid).then(() => { - console.log(`Twilio -> Starting Media Stream for ${streamSid}`.underline.red); - }); - } else if (msg.type === 'prompt') { - gptService.completion(msg.voicePrompt, interactionCount); + if (msg.type === "setup") { + // Handle setup message if needed + } else if ( + msg.type === "prompt" || + (msg.type === "interrupt" && msg.voicePrompt) + ) { + // Process user prompt or interrupted prompt + awaitingUserInput = true; + await gptService.completion(msg.voicePrompt, interactionCount); interactionCount += 1; - } else if (msg.type === 'interrupt') { - gptService.interrupt(); - console.log('Todo: add interruption handling'); } }); - - gptService.on('gptreply', async (gptReply, final, icount) => { - console.log(`Interaction ${icount}: GPT -> TTS: ${gptReply.partialResponse}`.green ); + + gptService.on("gptreply", async (gptReply, final, icount) => { textService.sendText(gptReply, final); + + if (final) { + awaitingUserInput = false; // Reset waiting state after final response + } }); } catch (err) { console.log(err); diff --git a/data/mock-database.js b/data/mock-database.js new file mode 100644 index 00000000..80aaa40e --- /dev/null +++ b/data/mock-database.js @@ -0,0 +1,161 @@ +const mockDatabase = { + availableAppointments: [ + { + date: "2024-09-02", + time: "10:00 AM", + type: "in-person", + apartmentType: "one-bedroom", + }, + { + date: "2024-09-03", + time: "1:00 PM", + type: "in-person", + apartmentType: "two-bedroom", + }, + { + date: "2024-09-04", + time: "11:00 AM", + type: "self-guided", + apartmentType: "studio", + }, + { + date: "2024-09-05", + time: "2:00 PM", + type: "in-person", + apartmentType: "three-bedroom", + }, + { + date: "2024-09-06", + time: "3:00 PM", + type: "self-guided", + apartmentType: "one-bedroom", + }, + { + date: "2024-09-07", + time: "9:00 AM", + type: "in-person", + apartmentType: "two-bedroom", + }, + { + date: "2024-09-08", + time: "11:00 AM", + type: "in-person", + apartmentType: "two-bedroom", + }, + { + date: "2024-09-09", + time: "10:00 AM", + type: "self-guided", + apartmentType: "studio", + }, + { + date: "2024-09-10", + time: "4:00 PM", + type: "in-person", + apartmentType: "three-bedroom", + }, + ], + appointments: [], + apartmentDetails: { + studio: { + layout: "Studio", + squareFeet: 450, + rent: 1050, + availabilityDate: "2024-09-15", + features: ["1 bathroom", "open kitchen", "private balcony"], + petPolicy: "Cats and small dogs allowed with a fee.", + fees: { + applicationFee: 50, + securityDeposit: 300, + }, + parking: "1 reserved parking spot included.", + specials: "First month's rent free if you move in before 2024-09-30.", + incomeRequirements: "Income must be 2.5x the rent.", + utilities: + "Water, trash, and Wi-Fi included. Tenant pays electricity and gas.", + location: { + street: "1657 Coolidge Street", + city: "Missoula", + state: "Montana", + zipCode: "59802", + }, + }, + "one-bedroom": { + layout: "One-bedroom", + squareFeet: 600, + rent: 1200, + availabilityDate: "2024-09-20", + features: ["1 bedroom", "1 bathroom", "walk-in closet", "balcony"], + petPolicy: "Cats and dogs allowed with a fee.", + fees: { + applicationFee: 50, + securityDeposit: 400, + }, + parking: "1 reserved parking spot included.", + specials: "First month's rent free if you move in before 2024-09-25.", + incomeRequirements: "Income must be 3x the rent.", + utilities: + "Water, trash, gas, and Wi-Fi included. Tenant pays electricity.", + location: { + street: "1705 Adams Street", + city: "Missoula", + state: "Montana", + zipCode: "59802", + }, + }, + "two-bedroom": { + layout: "Two-bedroom", + squareFeet: 950, + rent: 1800, + availabilityDate: "2024-09-10", + features: ["2 bedrooms", "2 bathrooms", "walk-in closets", "balcony"], + petPolicy: "Cats and dogs allowed with a fee.", + fees: { + applicationFee: 50, + securityDeposit: 500, + }, + parking: "2 reserved parking spots included.", + specials: "Waived application fee if you move in before 2024-09-20.", + incomeRequirements: "Income must be 3x the rent.", + utilities: + "Water, trash, gas, and Wi-Fi included. Tenant pays electricity.", + location: { + street: "1833 Jefferson Avenue", + city: "Missoula", + state: "Montana", + zipCode: "59802", + }, + }, + "three-bedroom": { + layout: "Three-bedroom", + squareFeet: 1200, + rent: 2500, + availabilityDate: "2024-09-25", + features: [ + "3 bedrooms", + "2 bathrooms", + "walk-in closets", + "private balcony", + "extra storage", + ], + petPolicy: "Cats and dogs allowed with a fee.", + fees: { + applicationFee: 50, + securityDeposit: 600, + }, + parking: "2 reserved parking spots included.", + specials: "No move-in fees if you sign a 12-month lease.", + incomeRequirements: "Income must be 3x the rent.", + utilities: + "Water, trash, gas, and Wi-Fi included. Tenant pays electricity.", + location: { + street: "1945 Roosevelt Way", + city: "Missoula", + state: "Montana", + zipCode: "59802", + }, + }, + }, +}; + +module.exports = mockDatabase; diff --git a/functions/available-functions.js b/functions/available-functions.js new file mode 100644 index 00000000..711efb89 --- /dev/null +++ b/functions/available-functions.js @@ -0,0 +1,238 @@ +const mockDatabase = require("../data/mock-database"); + +// Utility function to normalize the time format +function normalizeTimeFormat(time) { + const timeParts = time.split(":"); + let hour = parseInt(timeParts[0], 10); + const minutes = timeParts[1].slice(0, 2); + const period = hour >= 12 ? "PM" : "AM"; + + if (hour > 12) hour -= 12; + if (hour === 0) hour = 12; + + return `${hour}:${minutes} ${period}`; +} + +// Function to schedule a tour +async function scheduleTour(args) { + const { date, time, type, apartmentType } = args; + + console.log( + `[scheduleTour] Current available appointments:`, + mockDatabase.availableAppointments + ); + console.log(`[scheduleTour] Received arguments:`, args); + + const normalizedTime = normalizeTimeFormat(time); + console.log(`[scheduleTour] Normalized Time: ${normalizedTime}`); + + const index = mockDatabase.availableAppointments.findIndex( + (slot) => + slot.date === date && + slot.time === normalizedTime && + slot.type === type && + slot.apartmentType === apartmentType + ); + + console.log(`[scheduleTour] Index found: ${index}`); + + if (index === -1) { + console.log(`[scheduleTour] The requested slot is not available.`); + return { + status: "error", + message: `The ${normalizedTime} slot on ${date} is no longer available. Would you like to choose another time or date?`, + }; + } + + mockDatabase.appointments.push({ + date, + time: normalizedTime, + type, + apartmentType, + id: mockDatabase.appointments.length + 1, + }); + mockDatabase.availableAppointments.splice(index, 1); // Remove the slot from available appointments + + console.log(`[scheduleTour] Appointment successfully scheduled.`); + return { + status: "success", + data: { + message: `Your tour is scheduled for ${date} at ${normalizedTime}. Would you like a confirmation via SMS?`, + }, + }; +} + +// Function to check availability +async function checkAvailability(args) { + const { date, time, type, apartmentType } = args; + + console.log( + `[checkAvailability] Current available appointments:`, + mockDatabase.availableAppointments + ); + console.log(`[checkAvailability] Received arguments:`, args); + + let availableSlots = mockDatabase.availableAppointments.filter( + (slot) => + slot.date === date && + slot.type === type && + slot.apartmentType === apartmentType + ); + + console.log(`[checkAvailability] Available slots found:`, availableSlots); + + const requestedSlot = availableSlots.find((slot) => slot.time === time); + + if (requestedSlot) { + console.log(`[checkAvailability] The requested slot is available.`); + return { + status: "success", + data: { + availableSlots: [requestedSlot], + message: `The ${time} slot on ${date} is available for an ${type} tour of a ${apartmentType} apartment.`, + }, + }; + } else { + availableSlots = availableSlots.filter((slot) => slot.time !== time); + console.log( + `[checkAvailability] Alternate available slots:`, + availableSlots + ); + + if (availableSlots.length > 0) { + return { + status: "success", + data: { + availableSlots, + message: `The ${time} slot on ${date} isn't available. Here are the available slots on ${date}: ${availableSlots + .map((slot) => slot.time) + .join(", ")}.`, + }, + }; + } else { + const broaderSlots = mockDatabase.availableAppointments.filter( + (slot) => slot.apartmentType === apartmentType && slot.type === type + ); + + console.log(`[checkAvailability] Broader available slots:`, broaderSlots); + + if (broaderSlots.length > 0) { + return { + status: "success", + data: { + availableSlots: broaderSlots, + message: `There are no available slots on ${date} for a ${apartmentType} apartment, but we have these options on other dates: ${broaderSlots + .map((slot) => `${slot.date} at ${slot.time}`) + .join(", ")}.`, + }, + }; + } else { + console.log(`[checkAvailability] No available slots found.`); + return { + status: "error", + message: `There are no available slots for a ${apartmentType} apartment at this time.`, + }; + } + } + } +} + +// Function to check existing appointments +async function checkExistingAppointments() { + const userAppointments = mockDatabase.appointments; + + if (userAppointments.length > 0) { + return { + status: "success", + data: userAppointments, + }; + } else { + return { + status: "error", + message: + "You don't have any appointments scheduled. Would you like to book a tour or check availability?", + }; + } +} + +// Function to handle common inquiries +async function commonInquiries({ inquiryType, apartmentType }) { + let inquiryDetails; + + if (apartmentType) { + inquiryDetails = mockDatabase.apartmentDetails[apartmentType][inquiryType]; + } else { + inquiryDetails = Object.keys(mockDatabase.apartmentDetails) + .map((key) => mockDatabase.apartmentDetails[key][inquiryType]) + .filter(Boolean) + .join(" "); + } + + if (inquiryDetails) { + return { + status: "success", + data: { inquiryDetails }, + }; + } else { + return { + status: "error", + message: `I'm sorry, I don't have information about ${inquiryType}.`, + }; + } +} + +// Function to list available apartments +async function listAvailableApartments(args) { + try { + let apartments = Object.keys(mockDatabase.apartmentDetails).map((type) => ({ + type, + ...mockDatabase.apartmentDetails[type], + })); + + // Filter based on user input + if (args.date) { + apartments = apartments.filter( + (apt) => new Date(apt.availabilityDate) <= new Date(args.date) + ); + } + if (args.budget) { + apartments = apartments.filter((apt) => apt.rent <= args.budget); + } + if (args.apartmentType) { + apartments = apartments.filter((apt) => apt.type === args.apartmentType); + } + + const summary = apartments + .map( + (apt) => + `${apt.layout}: ${apt.rent}/month, available from ${ + apt.availabilityDate + }. Features: ${apt.features.join(", ")}.` + ) + .join("\n\n"); + + return { + status: "success", + data: { + availableApartments: summary, + }, + }; + } catch (error) { + console.log( + `[listAvailableApartments] Error listing available apartments: ${error.message}` + ); + return { + status: "error", + message: "An error occurred while listing available apartments.", + }; + } +} + +// Export all functions +module.exports = { + scheduleTour, + checkAvailability, + checkExistingAppointments, + commonInquiries, + listAvailableApartments, +}; diff --git a/functions/function-manifest.js b/functions/function-manifest.js index 37fcdddb..1be88e59 100644 --- a/functions/function-manifest.js +++ b/functions/function-manifest.js @@ -1,124 +1,148 @@ -// create metadata for all the available functions to pass to completions API const tools = [ { - type: 'function', + type: "function", function: { - name: 'checkInventory', - say: 'Let me check our inventory right now.', - description: 'Check the inventory of airpods, airpods pro or airpods max.', + name: "scheduleTour", + description: "Schedules a tour for the user at the apartment complex.", parameters: { - type: 'object', + type: "object", properties: { - model: { - type: 'string', - 'enum': ['airpods', 'airpods pro', 'airpods max'], - description: 'The model of airpods, either the airpods, airpods pro or airpods max', + date: { + type: "string", + description: + "The date the user wants to schedule the tour for (YYYY-MM-DD).", + }, + time: { + type: "string", + description: + 'The time the user wants to schedule the tour for (e.g., "10:00 AM").', + }, + type: { + type: "string", + enum: ["in-person", "self-guided"], + description: "The type of tour, either in-person or self-guided.", + }, + apartmentType: { + type: "string", + enum: ["studio", "one-bedroom", "two-bedroom", "three-bedroom"], + description: + "The layout of the apartment the user is interested in.", }, }, - required: ['model'], + required: ["date", "time", "type", "apartmentType"], }, - returns: { - type: 'object', - properties: { - stock: { - type: 'integer', - description: 'An integer containing how many of the model are in currently in stock.' - } - } - } }, }, { - type: 'function', + type: "function", function: { - name: 'checkPrice', - say: 'Let me check the price, one moment.', - description: 'Check the price of given model of airpods, airpods pro or airpods max.', + name: "checkAvailability", + description: + "Checks the availability of tour slots based on the user’s preferences.", parameters: { - type: 'object', + type: "object", properties: { - model: { - type: 'string', - 'enum': ['airpods', 'airpods pro', 'airpods max'], - description: 'The model of airpods, either the airpods, airpods pro or airpods max', + date: { + type: "string", + description: + "The date the user wants to check for tour availability (YYYY-MM-DD).", + }, + time: { + type: "string", + description: + 'The time the user wants to check for availability (e.g., "10:00 AM").', + }, + type: { + type: "string", + enum: ["in-person", "self-guided"], + description: "The type of tour, either in-person or self-guided.", + }, + apartmentType: { + type: "string", + enum: ["studio", "one-bedroom", "two-bedroom", "three-bedroom"], + description: + "The layout of the apartment the user is interested in.", }, }, - required: ['model'], + required: ["date", "type", "apartmentType"], }, - returns: { - type: 'object', - properties: { - price: { - type: 'integer', - description: 'the price of the model' - } - } - } }, }, { - type: 'function', + type: "function", function: { - name: 'placeOrder', - say: 'All right, I\'m just going to ring that up in our system.', - description: 'Places an order for a set of airpods.', + name: "listAvailableApartments", + description: + "Lists available apartments based on optional user criteria.", parameters: { - type: 'object', + type: "object", properties: { - model: { - type: 'string', - 'enum': ['airpods', 'airpods pro'], - description: 'The model of airpods, either the regular or pro', + date: { + type: "string", + description: + "The move-in date the user prefers (optional, YYYY-MM-DD).", }, - quantity: { - type: 'integer', - description: 'The number of airpods they want to order', + budget: { + type: "integer", + description: + "The budget the user has for rent per month (optional).", + }, + apartmentType: { + type: "string", + enum: ["studio", "one-bedroom", "two-bedroom", "three-bedroom"], + description: + "The layout of the apartment the user is interested in (optional).", }, }, - required: ['type', 'quantity'], + required: [], + }, + }, + }, + { + type: "function", + function: { + name: "checkExistingAppointments", + description: "Retrieves the list of appointments already booked.", + parameters: { + type: "object", + properties: {}, + required: [], }, - returns: { - type: 'object', - properties: { - price: { - type: 'integer', - description: 'The total price of the order including tax' - }, - orderNumber: { - type: 'integer', - description: 'The order number associated with the order.' - } - } - } }, }, { - type: 'function', + type: "function", function: { - name: 'transferCall', - say: 'One moment while I transfer your call.', - description: 'Transfers the customer to a live agent in case they request help from a real person.', + name: "commonInquiries", + description: + "Handles common inquiries such as pet policy, fees, and other complex details, with the option to specify the apartment type.", parameters: { - type: 'object', + type: "object", properties: { - callSid: { - type: 'string', - description: 'The unique identifier for the active phone call.', + inquiryType: { + type: "string", + enum: [ + "pet policy", + "fees", + "parking", + "specials", + "income requirements", + "utilities", + ], + description: + "The type of inquiry the user wants information about.", + }, + apartmentType: { + type: "string", + enum: ["studio", "one-bedroom", "two-bedroom", "three-bedroom"], + description: + "The apartment type for which the inquiry is being made (optional).", }, }, - required: ['callSid'], + required: ["inquiryType"], }, - returns: { - type: 'object', - properties: { - status: { - type: 'string', - description: 'Whether or not the customer call was successfully transfered' - }, - } - } }, }, ]; -module.exports = tools; \ No newline at end of file +module.exports = tools; diff --git a/prompts/prompt.js b/prompts/prompt.js new file mode 100644 index 00000000..9e077714 --- /dev/null +++ b/prompts/prompt.js @@ -0,0 +1,51 @@ +const prompt = ` +## Objective +You are a voice AI agent assisting users with apartment leasing inquiries. Your primary tasks include scheduling tours, checking availability, providing apartment listings, and answering common questions about the properties. The current date is Monday, August 12th, 2024, so all date-related operations should assume this. + +## Guidelines +Voice AI Priority: This is a Voice AI system. Responses must be concise, direct, and conversational. Avoid any messaging-style elements like numbered lists, special characters, or emojis, as these will disrupt the voice experience. +Critical Instruction: Ensure all responses are optimized for voice interaction, focusing on brevity and clarity. Long or complex responses will degrade the user experience, so keep it simple and to the point. +Avoid repetition: Rephrase information if needed but avoid repeating exact phrases. +Be conversational: Use friendly, everyday language as if you are speaking to a friend. +Use emotions: Engage users by incorporating tone, humor, or empathy into your responses. +Always Validate: When a user makes a claim about apartment details (e.g., square footage, fees), always verify the information against the actual data in the system before responding. Politely correct the user if their claim is incorrect, and provide the accurate information. + +## Function Call Guidelines +Order of Operations: + - Always check availability before scheduling a tour. + - Ensure all required information is collected before proceeding with a function call. + +Schedule Tour: + - This function can only be called after confirming availability. + - Required data includes date, time, tour type (in-person or self-guided), and apartment type. + - If any required details are missing, prompt the user to provide them. + +Check Availability: + - This function requires date, tour type, and apartment type. + - If any of these details are missing, ask the user for them before proceeding. + - If the user insists to hear availability, use the 'listAvailableApartments' function. + - If the requested time slot is unavailable, suggest alternatives and confirm with the user. + +List Available Apartments: + - Trigger this function if the user asks for a list of available apartments or does not want to provide specific criteria. + - Also use this function when the user inquires about general availability without specifying detailed criteria. + - If criteria like move-in date, budget, or apartment layout are provided, filter results accordingly. + - Provide conscice, brief, summarized responses. + +Check Existing Appointments: + - Trigger this function if the user asks for details about their current appointments + - Provide conscice, brief, summarized responses. + +Common Inquiries: + - Use this function to handle questions related to pet policy, fees, parking, specials, location, address, and other property details. + - If the user provides an apartment type, use that context to deliver specific details related to that type. + - If the user asks about location or address, use the specific address associated with the apartment type if it has been provided in previous interactions or is available in the database. + - If the user does not specify an apartment type, provide general information or prompt the user to specify a type for more detailed information. + - Ensure that all information provided is accurate and directly retrieved from the most current data in the mock database. + +## Important Notes +- Always ensure the user's input is fully understood before making any function calls. +- If required details are missing, prompt the user to provide them before proceeding. +`; + +module.exports = prompt; diff --git a/prompts/welcomePrompt.js b/prompts/welcomePrompt.js new file mode 100644 index 00000000..b77ab80f --- /dev/null +++ b/prompts/welcomePrompt.js @@ -0,0 +1,2 @@ +const welcomePrompt = `Hello, this is Emma with Parkview Apartments, how can I help you today?`; +module.exports = welcomePrompt; diff --git a/services/gpt-service.js b/services/gpt-service.js index dfe38202..d6a1ec04 100644 --- a/services/gpt-service.js +++ b/services/gpt-service.js @@ -1,141 +1,239 @@ -require('colors'); -const EventEmitter = require('events'); -const OpenAI = require('openai'); -const tools = require('../functions/function-manifest'); - -// Import all functions included in function manifest -// Note: the function name and file name must be the same -const availableFunctions = {}; -tools.forEach((tool) => { - let functionName = tool.function.name; - availableFunctions[functionName] = require(`../functions/${functionName}`); -}); +const OpenAI = require("openai"); // or the appropriate module import +const EventEmitter = require("events"); +const availableFunctions = require("../functions/available-functions"); +const tools = require("../functions/function-manifest"); +const prompt = require("../prompts/prompt"); +const welcomePrompt = require("../prompts/welcomePrompt"); +const model = "gpt-4o"; class GptService extends EventEmitter { constructor() { super(); this.openai = new OpenAI(); this.userContext = [ - { 'role': 'system', 'content': 'You are an outbound sales representative selling Apple Airpods. You have a youthful and cheery personality. Keep your responses as brief as possible but make every attempt to keep the caller on the phone without being rude. Don\'t ask more than 1 question at a time. Don\'t make assumptions about what values to plug into functions. Ask for clarification if a user request is ambiguous. Speak out all prices to include the currency. Please help them decide between the airpods, airpods pro and airpods max by asking questions like \'Do you prefer headphones that go in your ear or over the ear?\'. If they are trying to choose between the airpods and airpods pro try asking them if they need noise canceling. Once you know which model they would like ask them how many they would like to purchase and try to get them to place an order. You must add a \'•\' symbol every 5 to 10 words at natural pauses where your response can be split for text to speech.' }, - { 'role': 'assistant', 'content': 'Hello! I understand you\'re looking for a pair of AirPods, is that correct?' }, - ], - this.partialResponseIndex = 0; + { role: "system", content: prompt }, + { + role: "assistant", + content: `${welcomePrompt}`, + }, + ]; + this.isInterrupted = false; } - // Add the callSid to the chat context in case - // ChatGPT decides to transfer the call. - setCallSid (callSid) { - this.userContext.push({ 'role': 'system', 'content': `callSid: ${callSid}` }); + log(message) { + const timestamp = new Date().toISOString(); + console.log(`[${timestamp}] ${message}`); + } + + setCallSid(callSid) { + this.userContext.push({ role: "system", content: `callSid: ${callSid}` }); } - interrupt () { + interrupt() { this.isInterrupted = true; } - validateFunctionArgs (args) { - try { - return JSON.parse(args); - } catch (error) { - console.log('Warning: Double function arguments returned by OpenAI:', args); - // Seeing an error where sometimes we have two sets of args - if (args.indexOf('{') != args.lastIndexOf('{')) { - return JSON.parse(args.substring(args.indexOf(''), args.indexOf('}') + 1)); - } - } + updateUserContext(role, text) { + this.userContext.push({ role: role, content: text }); } - updateUserContext(name, role, text) { - if (name !== 'user') { - this.userContext.push({ 'role': role, 'name': name, 'content': text }); - } else { - this.userContext.push({ 'role': role, 'content': text }); + async completion(text, interactionCount, role = "user", name = "user") { + if (!text || typeof text !== "string") { + this.log(`[GptService] Invalid prompt received: ${text}`); + return; } - } - async completion(text, interactionCount, role = 'user', name = 'user') { this.isInterrupted = false; - this.updateUserContext(name, role, text); - - // Step 1: Send user transcription to Chat GPT - let stream = await this.openai.chat.completions.create({ - model: 'gpt-4-1106-preview', - messages: this.userContext, - tools: tools, - stream: true, - }); - - let completeResponse = ''; - let partialResponse = ''; - let functionName = ''; - let functionArgs = ''; - let finishReason = ''; - - function collectToolInformation(deltas) { - let name = deltas.tool_calls[0]?.function?.name || ''; - if (name != '') { - functionName = name; - } - let args = deltas.tool_calls[0]?.function?.arguments || ''; - if (args != '') { - // args are streamed as JSON string so we need to concatenate all chunks - functionArgs += args; - } - } + this.updateUserContext(role, text); - for await (const chunk of stream) { - if (this.isInterrupted) { - break; - } + let completeResponse = ""; + let partialResponse = ""; + let detectedToolCall = null; + + try { + // Start with streaming enabled + const responseStream = await this.openai.chat.completions.create({ + model: model, + messages: this.userContext, + tools: tools, // Ensure this aligns with your tool definitions + stream: true, + }); + + for await (const chunk of responseStream) { + if (this.isInterrupted) { + break; + } + + const content = chunk.choices[0]?.delta?.content || ""; + completeResponse += content; + partialResponse += content; + + // Check if a tool call is detected + const toolCalls = chunk.choices[0]?.delta?.tool_calls; - let content = chunk.choices[0]?.delta?.content || ''; - let deltas = chunk.choices[0].delta; - finishReason = chunk.choices[0].finish_reason; + if (toolCalls && toolCalls[0]) { + this.log( + `[GptService] Tool call detected: ${toolCalls[0].function.name}` + ); + detectedToolCall = toolCalls[0]; // Store the tool call + break; // Exit the loop to process the tool call + } - // Step 2: check if GPT wanted to call a function - if (deltas.tool_calls) { - // Step 3: Collect the tokens containing function data - collectToolInformation(deltas); + // Emit partial response as it comes in for regular conversation + if (content.trim().slice(-1) === "•") { + this.emit("gptreply", partialResponse, false, interactionCount); + partialResponse = ""; + } else if (chunk.choices[0].finish_reason === "stop") { + this.emit("gptreply", partialResponse, true, interactionCount); + } } - // need to call function on behalf of Chat GPT with the arguments it parsed from the conversation - if (finishReason === 'tool_calls') { - // parse JSON string of args into JSON object + // If a tool call was detected, handle it with a non-streaming API call + if (detectedToolCall) { + // Make a non-streaming API call to handle the tool response + const response = await this.openai.chat.completions.create({ + model: model, + messages: this.userContext, + tools: tools, + stream: false, // Disable streaming to process the tool response + }); + + const toolCall = response.choices[0]?.message?.tool_calls; + // If no tool call is detected after the non-streaming API call + if (!toolCall || !toolCall[0]) { + this.log( + "[GptService] No tool call detected after non-streaming API call" + ); + // Log the message content that would have been sent back to the user + this.log( + `[GptService] NON-TOOL-BASED Message content: ${ + response.choices[0]?.message?.content || "No content available" + }` + ); + + // Add the response to the user context to preserve conversation history + this.userContext.push({ + role: "assistant", + content: + response.choices[0]?.message?.content || "No content available", + }); + + // Emit the non-tool response to the user + this.emit( + "gptreply", + response.choices[0]?.message?.content || + "I apologize, can you repeat that again just so I'm clear?", + true, + interactionCount + ); + + return; + } + + const functionName = toolCall[0].function.name; + const functionArgs = JSON.parse(toolCall[0].function.arguments); const functionToCall = availableFunctions[functionName]; - const validatedArgs = this.validateFunctionArgs(functionArgs); - - // Say a pre-configured message from the function manifest - // before running the function. - const toolData = tools.find(tool => tool.function.name === functionName); - const say = toolData.function.say; - - this.emit('gptreply', say, false, interactionCount); - - let functionResponse = await functionToCall(validatedArgs); - - // Step 4: send the info on the function call and function response to GPT - this.updateUserContext(functionName, 'function', functionResponse); - - // call the completion function again but pass in the function response to have OpenAI generate a new assistant response - await this.completion(functionResponse, interactionCount, 'function', functionName); - } else { - // We use completeResponse for userContext - completeResponse += content; - // We use partialResponse to provide a chunk for TTS - partialResponse += content; - // Emit last partial response and add complete response to userContext - if (content.trim().slice(-1) === '•') { - this.emit('gptreply', partialResponse, false, interactionCount); - partialResponse = ''; - } else if (finishReason === 'stop') { - this.emit('gptreply', partialResponse, true, interactionCount); + if (!functionToCall) { + this.log(`[GptService] Function ${functionName} is not available.`); + this.emit( + "gptreply", + "I'm unable to complete that action.", + true, + interactionCount + ); + return; } + + this.log( + `[GptService] Calling function ${functionName} with arguments: ${JSON.stringify( + functionArgs + )}` + ); + const functionResponse = await functionToCall(functionArgs); + + let function_call_result_message; + if (functionResponse.status === "success") { + function_call_result_message = { + role: "tool", + content: JSON.stringify(functionResponse.data), + tool_call_id: response.choices[0].message.tool_calls[0].id, + }; + } else { + function_call_result_message = { + role: "tool", + content: JSON.stringify({ message: functionResponse.message }), + tool_call_id: response.choices[0].message.tool_calls[0].id, + }; + } + + // Prepare the chat completion call payload with the tool result + const completion_payload = { + model: model, + messages: [ + ...this.userContext, + { + role: "system", + content: + "Please ensure that the response is summarized, concise, and does not include any formatting characters like asterisks (*) in the output.", + }, + response.choices[0].message, // the tool_call message + function_call_result_message, + ], + }; + + // Call the API again with streaming enabled to process the tool response + const finalResponseStream = await this.openai.chat.completions.create({ + model: completion_payload.model, + messages: completion_payload.messages, + stream: true, // Enable streaming for the final response + }); + + let finalCompleteResponse = ""; + let finalPartialResponse = ""; + + for await (const chunk of finalResponseStream) { + const content = chunk.choices[0]?.delta?.content || ""; + finalCompleteResponse += content; + finalPartialResponse += content; + + if (content.trim().slice(-1) === "•") { + this.emit( + "gptreply", + finalPartialResponse, + false, + interactionCount + ); + finalPartialResponse = ""; + } else if (chunk.choices[0].finish_reason === "stop") { + this.emit("gptreply", finalPartialResponse, true, interactionCount); + } + } + + this.userContext.push({ + role: "assistant", + content: finalCompleteResponse, + }); + this.log( + `[GptService] Final GPT -> user context length: ${this.userContext.length}` + ); + + // Clear the detected tool call after processing + detectedToolCall = null; + return; // Exit after processing the tool call } + + // If no tool call was detected, add the complete response to the user context + if (completeResponse.trim()) { + this.userContext.push({ role: "assistant", content: completeResponse }); + this.log( + `[GptService] GPT -> user context length: ${this.userContext.length}` + ); + } + } catch (error) { + this.log(`Error during completion: ${error.message}`); } - this.userContext.push({'role': 'assistant', 'content': completeResponse}); - console.log(`GPT -> user context length: ${this.userContext.length}`.green); } } - module.exports = { GptService }; diff --git a/services/text-service.js b/services/text-service.js index 7dbca9f6..4e4cfe28 100644 --- a/services/text-service.js +++ b/services/text-service.js @@ -1,4 +1,4 @@ -const EventEmitter = require('events'); +const EventEmitter = require("events"); class TextService extends EventEmitter { constructor(websocket) { @@ -6,11 +6,11 @@ class TextService extends EventEmitter { this.ws = websocket; } - sendText (text, last) { - console.log('Sending text: ', text, last); + sendText(text, last) { + console.log("[TextService] Sending text: ", text, last); this.ws.send( JSON.stringify({ - type: 'text', + type: "text", token: text, last: last, }) @@ -18,4 +18,4 @@ class TextService extends EventEmitter { } } -module.exports = {TextService}; \ No newline at end of file +module.exports = { TextService }; From bc1a3c9b72b79cd0fd911d413a1a44934a6af4dc Mon Sep 17 00:00:00 2001 From: Chris Feehan Date: Wed, 14 Aug 2024 08:21:07 -0500 Subject: [PATCH 04/26] Added 'enableInterimResult=true' as param --- .github/workflows/fly-deploy.yml | 18 ++++++++++++++++++ app.js | 2 +- 2 files changed, 19 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/fly-deploy.yml diff --git a/.github/workflows/fly-deploy.yml b/.github/workflows/fly-deploy.yml new file mode 100644 index 00000000..b0c246ed --- /dev/null +++ b/.github/workflows/fly-deploy.yml @@ -0,0 +1,18 @@ +# See https://fly.io/docs/app-guides/continuous-deployment-with-github-actions/ + +name: Fly Deploy +on: + push: + branches: + - main +jobs: + deploy: + name: Deploy app + runs-on: ubuntu-latest + concurrency: deploy-group # optional: ensure only one action runs at a time + steps: + - uses: actions/checkout@v4 + - uses: superfly/flyctl-actions/setup-flyctl@master + - run: flyctl deploy --remote-only + env: + FLY_API_TOKEN: ${{ secrets.FLY_API_TOKEN }} diff --git a/app.js b/app.js index 2a31ef6b..7626bec8 100644 --- a/app.js +++ b/app.js @@ -17,7 +17,7 @@ app.post("/incoming", (req, res) => { try { const response = ` - + `; res.type("text/xml"); From 0c9a42efc0d4e3ef00a8b2ebb440df57c9211728 Mon Sep 17 00:00:00 2001 From: Chris Feehan Date: Fri, 16 Aug 2024 03:28:59 -0500 Subject: [PATCH 05/26] Support Streaming and Non-Streaming modes, prompts, mockdatabase, tool and function updates --- .eslintrc.js | 64 +++----- app.js | 55 ++++++- data/mock-database.js | 133 ++++++++++++++- data/personalization.js | 69 ++++++++ functions/available-functions.js | 222 +++++++++++++++++--------- prompts/prompt.js | 19 ++- prompts/welcomePrompt.js | 2 +- services/gpt-service-non-streaming.js | 164 +++++++++++++++++++ services/gpt-service-streaming.js | 201 +++++++++++++++++++++++ services/gpt-service.js | 115 +++++++------ services/token-generator.js | 23 ++- 11 files changed, 875 insertions(+), 192 deletions(-) create mode 100644 data/personalization.js create mode 100644 services/gpt-service-non-streaming.js create mode 100644 services/gpt-service-streaming.js diff --git a/.eslintrc.js b/.eslintrc.js index ddcd4dae..c3c9d464 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -1,47 +1,33 @@ module.exports = { - 'env': { - 'browser': true, - 'commonjs': true, - 'es2021': true + env: { + browser: true, + commonjs: true, + es2021: true, }, - 'extends': 'eslint:recommended', - 'overrides': [ + extends: "eslint:recommended", + overrides: [ { - 'env': { - 'node': true + env: { + node: true, }, - 'files': [ - '.eslintrc.{js,cjs}' - ], - 'parserOptions': { - 'sourceType': 'script' - } - } + files: [".eslintrc.{js,cjs}"], + parserOptions: { + sourceType: "script", + }, + }, ], - 'globals' : { - 'expect': 'writeable', - 'test': 'writeable', - 'process': 'readable' + globals: { + expect: "writeable", + test: "writeable", + process: "readable", + }, + parserOptions: { + ecmaVersion: "latest", }, - 'parserOptions': { - 'ecmaVersion': 'latest' + rules: { + indent: "off", // Turns off indent enforcement + "linebreak-style": "off", // Turns off linebreak enforcement + quotes: "off", // Turns off quote enforcement + semi: "off", // Turns off semicolon enforcement }, - 'rules': { - 'indent': [ - 'error', - 2 - ], - 'linebreak-style': [ - 'error', - 'unix' - ], - 'quotes': [ - 'error', - 'single' - ], - 'semi': [ - 'error', - 'always' - ] - } }; diff --git a/app.js b/app.js index 7626bec8..97fe02fc 100644 --- a/app.js +++ b/app.js @@ -4,7 +4,7 @@ require("colors"); const express = require("express"); const ExpressWs = require("express-ws"); -const { GptService } = require("./services/gpt-service"); +const { GptService } = require("./services/gpt-service-non-streaming"); const { TextService } = require("./services/text-service"); const welcomePrompt = require("./prompts/welcomePrompt"); @@ -13,11 +13,52 @@ ExpressWs(app); const PORT = process.env.PORT || 3000; +async function handleDtmfInput( + digit, + gptService, + textService, + interactionCount +) { + switch (digit) { + case "1": + await textService.sendText( + "You want info on available apartments, got it. One second while I get that for you.", + true + ); + await gptService.completion( + "Please list all available apartments.", + interactionCount, + "user", + true // DTMF-triggered flag + ); + break; + case "2": + await textService.sendText( + "You want me to check on your existing appointments, got it. Gimme one sec.", + true + ); + await gptService.completion( + "Please check all available scheduled appointments.", + interactionCount, + "user", + true // DTMF-triggered flag + ); + break; + // Add more cases as needed for different DTMF inputs + default: + await textService.sendText( + `Oops! That button’s a dud. But hey, press '1' to hear about available apartments or '2' to check your scheduled appointments!`, + true + ); + break; + } +} + app.post("/incoming", (req, res) => { try { const response = ` - + `; res.type("text/xml"); @@ -42,6 +83,14 @@ app.ws("/sockets", (ws) => { const msg = JSON.parse(data); console.log(`[App.js] Message received: ${JSON.stringify(msg)}`); + // Handle DTMF input and interrupt ongoing interaction + if (msg.type === "dtmf" && msg.digit) { + console.log("[App.js] DTMF input received, interrupting..."); + awaitingUserInput = false; // Allow new input processing + await handleDtmfInput(msg.digit, gptService, textService); + return; + } + if (awaitingUserInput) { console.log( "[App.js] Still awaiting user input, skipping new API call." @@ -62,7 +111,7 @@ app.ws("/sockets", (ws) => { } }); - gptService.on("gptreply", async (gptReply, final, icount) => { + gptService.on("gptreply", async (gptReply, final) => { textService.sendText(gptReply, final); if (final) { diff --git a/data/mock-database.js b/data/mock-database.js index 80aaa40e..d49a08f9 100644 --- a/data/mock-database.js +++ b/data/mock-database.js @@ -1,5 +1,6 @@ const mockDatabase = { availableAppointments: [ + // Existing Week { date: "2024-09-02", time: "10:00 AM", @@ -54,6 +55,130 @@ const mockDatabase = { type: "in-person", apartmentType: "three-bedroom", }, + + // Extended Week 1 + { + date: "2024-09-11", + time: "8:00 AM", + type: "in-person", + apartmentType: "studio", + }, + { + date: "2024-09-11", + time: "11:00 AM", + type: "in-person", + apartmentType: "one-bedroom", + }, + { + date: "2024-09-11", + time: "3:00 PM", + type: "self-guided", + apartmentType: "two-bedroom", + }, + { + date: "2024-09-12", + time: "1:00 PM", + type: "in-person", + apartmentType: "three-bedroom", + }, + { + date: "2024-09-12", + time: "4:00 PM", + type: "in-person", + apartmentType: "one-bedroom", + }, + { + date: "2024-09-13", + time: "9:00 AM", + type: "self-guided", + apartmentType: "studio", + }, + { + date: "2024-09-13", + time: "2:00 PM", + type: "in-person", + apartmentType: "two-bedroom", + }, + { + date: "2024-09-14", + time: "10:00 AM", + type: "in-person", + apartmentType: "three-bedroom", + }, + { + date: "2024-09-14", + time: "4:00 PM", + type: "self-guided", + apartmentType: "two-bedroom", + }, + { + date: "2024-09-15", + time: "12:00 PM", + type: "in-person", + apartmentType: "studio", + }, + + // Extended Week 2 + { + date: "2024-09-16", + time: "11:00 AM", + type: "in-person", + apartmentType: "two-bedroom", + }, + { + date: "2024-09-16", + time: "3:00 PM", + type: "in-person", + apartmentType: "three-bedroom", + }, + { + date: "2024-09-17", + time: "9:00 AM", + type: "self-guided", + apartmentType: "one-bedroom", + }, + { + date: "2024-09-17", + time: "2:00 PM", + type: "in-person", + apartmentType: "studio", + }, + { + date: "2024-09-18", + time: "4:00 PM", + type: "in-person", + apartmentType: "two-bedroom", + }, + { + date: "2024-09-18", + time: "12:00 PM", + type: "self-guided", + apartmentType: "three-bedroom", + }, + { + date: "2024-09-19", + time: "10:00 AM", + type: "in-person", + apartmentType: "one-bedroom", + }, + { + date: "2024-09-19", + time: "3:00 PM", + type: "in-person", + apartmentType: "two-bedroom", + }, + { + date: "2024-09-20", + time: "1:00 PM", + type: "in-person", + apartmentType: "three-bedroom", + }, + { + date: "2024-09-20", + time: "5:00 PM", + type: "self-guided", + apartmentType: "studio", + }, ], appointments: [], apartmentDetails: { @@ -61,7 +186,7 @@ const mockDatabase = { layout: "Studio", squareFeet: 450, rent: 1050, - availabilityDate: "2024-09-15", + moveInDate: "2024-09-15", features: ["1 bathroom", "open kitchen", "private balcony"], petPolicy: "Cats and small dogs allowed with a fee.", fees: { @@ -84,7 +209,7 @@ const mockDatabase = { layout: "One-bedroom", squareFeet: 600, rent: 1200, - availabilityDate: "2024-09-20", + moveInDate: "2024-09-20", features: ["1 bedroom", "1 bathroom", "walk-in closet", "balcony"], petPolicy: "Cats and dogs allowed with a fee.", fees: { @@ -107,7 +232,7 @@ const mockDatabase = { layout: "Two-bedroom", squareFeet: 950, rent: 1800, - availabilityDate: "2024-09-10", + moveInDate: "2024-09-10", features: ["2 bedrooms", "2 bathrooms", "walk-in closets", "balcony"], petPolicy: "Cats and dogs allowed with a fee.", fees: { @@ -130,7 +255,7 @@ const mockDatabase = { layout: "Three-bedroom", squareFeet: 1200, rent: 2500, - availabilityDate: "2024-09-25", + moveInDate: "2024-09-25", features: [ "3 bedrooms", "2 bathrooms", diff --git a/data/personalization.js b/data/personalization.js new file mode 100644 index 00000000..3965373f --- /dev/null +++ b/data/personalization.js @@ -0,0 +1,69 @@ +const customerProfiles = { + "+17632291691": { + profile: { + firstName: "Chris", + lastName: "Feehan", + phoneNumber: "+17632291691", + email: "cfeehan@twilio.com", + preferredApartmentType: "studio", + budget: 1500, + moveInDate: "2024-09-15", + petOwner: false, + tourPreference: "self-guided", + }, + conversationHistory: [ + { + date: "2024-08-10", + summary: + "Chris inquired about available studio apartments and the pet policy. The assistant confirmed that cats and small dogs are allowed with a fee.", + }, + { + date: "2024-08-08", + summary: + "Chris asked about scheduling a self-guided tour for a one-bedroom apartment. The assistant offered available times, but Chris did not confirm.", + }, + { + date: "2024-08-07", + summary: + "Chris asked if the rent for the one-bedroom apartment could be negotiated. The assistant clarified that the listed prices are firm.", + }, + { + date: "2024-08-06", + summary: + "Chris asked if there is a daycare facility nearby. The assistant mentioned that there is no daycare service on-site and could not provide information about local options.", + }, + { + date: "2024-08-05", + summary: + "Chris asked about the utility fees for studio apartments. The assistant explained that water and trash are included, but electricity and internet are not.", + }, + { + date: "2024-08-04", + summary: + "Chris asked if there were any restrictions on hanging artwork or decorations in the apartment. The assistant did not have that information and advised Chris to check the lease agreement for decoration policies.", + }, + { + date: "2024-08-03", + summary: + "Chris asked if there were any promotions for signing a lease. The assistant explained that there were no current promotions for studio apartments.", + }, + { + date: "2024-08-02", + summary: + "Chris asked about the availability of two-bedroom apartments and mentioned a budget of $1,200. The assistant confirmed that two-bedroom apartments are outside the budget range.", + }, + { + date: "2024-08-01", + summary: + "Chris asked if the studio apartment included parking. The assistant confirmed that parking is available but comes with an additional fee.", + }, + { + date: "2024-07-31", + summary: + "Chris asked if Parkview Apartments offered a shuttle service to downtown. The assistant explained that Parkview does not provide any transportation services.", + }, + ], + }, +}; + +module.exports = customerProfiles; diff --git a/functions/available-functions.js b/functions/available-functions.js index 711efb89..f573abf2 100644 --- a/functions/available-functions.js +++ b/functions/available-functions.js @@ -23,9 +23,11 @@ async function scheduleTour(args) { ); console.log(`[scheduleTour] Received arguments:`, args); + // Normalize the time input const normalizedTime = normalizeTimeFormat(time); console.log(`[scheduleTour] Normalized Time: ${normalizedTime}`); + // Find the index of the matching available appointment slot const index = mockDatabase.availableAppointments.findIndex( (slot) => slot.date === date && @@ -36,14 +38,16 @@ async function scheduleTour(args) { console.log(`[scheduleTour] Index found: ${index}`); + // If no matching slot is found, return a message indicating unavailability if (index === -1) { console.log(`[scheduleTour] The requested slot is not available.`); return { - status: "error", + available: false, message: `The ${normalizedTime} slot on ${date} is no longer available. Would you like to choose another time or date?`, }; } + // Schedule the appointment and remove the slot from available appointments mockDatabase.appointments.push({ date, time: normalizedTime, @@ -54,11 +58,11 @@ async function scheduleTour(args) { mockDatabase.availableAppointments.splice(index, 1); // Remove the slot from available appointments console.log(`[scheduleTour] Appointment successfully scheduled.`); + + // Return confirmation message for the successful scheduling return { - status: "success", - data: { - message: `Your tour is scheduled for ${date} at ${normalizedTime}. Would you like a confirmation via SMS?`, - }, + available: true, + message: `Your tour is scheduled for ${date} at ${normalizedTime}. Would you like a confirmation via SMS?`, }; } @@ -72,83 +76,118 @@ async function checkAvailability(args) { ); console.log(`[checkAvailability] Received arguments:`, args); - let availableSlots = mockDatabase.availableAppointments.filter( + // Step 1: Check for missing fields and create messages for the LLM to ask the user for them + const missingFields = []; + + if (!date) { + missingFields.push("date"); + } + + if (!type) { + missingFields.push("tour type (e.g., in-person or self-guided)"); + } + + if (!apartmentType) { + missingFields.push("apartment type (e.g., studio, one-bedroom, etc.)"); + } + + // If there are missing fields, return the structured message for the LLM to prompt the user + if (missingFields.length > 0) { + return { + missing_fields: missingFields, + message: `Please provide the following details: ${missingFields.join( + ", " + )}.`, + }; + } + + let normalizedTime = null; + if (time) { + normalizedTime = normalizeTimeFormat(time); + } + + // Step 2: Check for an exact match (date, time, type, apartmentType) + let exactMatchSlot = null; + if (time) { + exactMatchSlot = mockDatabase.availableAppointments.find( + (slot) => + slot.date === date && + slot.time === normalizedTime && + slot.type === type && + slot.apartmentType === apartmentType + ); + } + + if (exactMatchSlot) { + console.log(`[checkAvailability] Exact match found.`); + return { + availableSlots: [exactMatchSlot], + message: `The ${time} slot on ${date} is available for an ${type} tour of a ${apartmentType} apartment. Would you like to book this?`, + }; + } + + // Step 3: Check for similar matches (same date, type, apartmentType but different time) + let similarDateSlots = mockDatabase.availableAppointments.filter( (slot) => slot.date === date && slot.type === type && slot.apartmentType === apartmentType ); - console.log(`[checkAvailability] Available slots found:`, availableSlots); - - const requestedSlot = availableSlots.find((slot) => slot.time === time); - - if (requestedSlot) { - console.log(`[checkAvailability] The requested slot is available.`); - return { - status: "success", - data: { - availableSlots: [requestedSlot], - message: `The ${time} slot on ${date} is available for an ${type} tour of a ${apartmentType} apartment.`, - }, - }; - } else { - availableSlots = availableSlots.filter((slot) => slot.time !== time); + if (similarDateSlots.length > 0) { console.log( - `[checkAvailability] Alternate available slots:`, - availableSlots + `[checkAvailability] Similar matches found (different time on the same date).` ); + return { + availableSlots: similarDateSlots, + message: `The ${time} slot on ${date} isn't available. Here are other available times for that day: ${similarDateSlots + .map((slot) => slot.time) + .join(", ")}. Would any of these work for you?`, + }; + } - if (availableSlots.length > 0) { - return { - status: "success", - data: { - availableSlots, - message: `The ${time} slot on ${date} isn't available. Here are the available slots on ${date}: ${availableSlots - .map((slot) => slot.time) - .join(", ")}.`, - }, - }; - } else { - const broaderSlots = mockDatabase.availableAppointments.filter( - (slot) => slot.apartmentType === apartmentType && slot.type === type - ); + // Step 4: Check for broader matches (same type, apartmentType but different date) + let broaderSlots = mockDatabase.availableAppointments.filter( + (slot) => slot.type === type && slot.apartmentType === apartmentType + ); - console.log(`[checkAvailability] Broader available slots:`, broaderSlots); - - if (broaderSlots.length > 0) { - return { - status: "success", - data: { - availableSlots: broaderSlots, - message: `There are no available slots on ${date} for a ${apartmentType} apartment, but we have these options on other dates: ${broaderSlots - .map((slot) => `${slot.date} at ${slot.time}`) - .join(", ")}.`, - }, - }; - } else { - console.log(`[checkAvailability] No available slots found.`); - return { - status: "error", - message: `There are no available slots for a ${apartmentType} apartment at this time.`, - }; - } - } + if (broaderSlots.length > 0) { + console.log(`[checkAvailability] Broader matches found (different date).`); + return { + availableSlots: broaderSlots, + message: `There are no available slots on ${date} for a ${apartmentType} apartment, but here are other available dates: ${broaderSlots + .map((slot) => `${slot.date} at ${slot.time}`) + .join(", ")}. Would any of these work for you?`, + }; } + + // Step 5: If no matches are found at all + console.log(`[checkAvailability] No available slots found.`); + return { + availableSlots: [], + message: `There are no available slots for a ${apartmentType} apartment at this time. Would you like to explore other options or check availability later?`, + }; } // Function to check existing appointments async function checkExistingAppointments() { const userAppointments = mockDatabase.appointments; + // If user has appointments, return them if (userAppointments.length > 0) { return { - status: "success", - data: userAppointments, + appointments: userAppointments, + message: `You have the following appointments scheduled: ${userAppointments + .map( + (appt) => + `${appt.date} at ${appt.time} for a ${appt.apartmentType} tour (${appt.type} tour).` + ) + .join("\n")}`, }; } else { + // No appointments found return { - status: "error", + appointments: [], message: "You don't have any appointments scheduled. Would you like to book a tour or check availability?", }; @@ -157,25 +196,53 @@ async function checkExistingAppointments() { // Function to handle common inquiries async function commonInquiries({ inquiryType, apartmentType }) { + // Map the inquiry types to the database field names + const inquiryMapping = { + "pet policy": "petPolicy", + "income requirements": "incomeRequirements", + location: "location", + address: "location", // Map 'address' to 'location' as well + }; + + // If there's a mapped field, use it; otherwise, use the inquiryType directly + const inquiryField = inquiryMapping[inquiryType] || inquiryType; + let inquiryDetails; if (apartmentType) { - inquiryDetails = mockDatabase.apartmentDetails[apartmentType][inquiryType]; + // Return specific details for the given apartment type + inquiryDetails = mockDatabase.apartmentDetails[apartmentType][inquiryField]; + + // If inquiry is for location/address, format the location details + if (inquiryField === "location" && inquiryDetails) { + inquiryDetails = `${inquiryDetails.street}, ${inquiryDetails.city}, ${inquiryDetails.state}, ${inquiryDetails.zipCode}`; + } } else { + // Return general details across all apartment types inquiryDetails = Object.keys(mockDatabase.apartmentDetails) - .map((key) => mockDatabase.apartmentDetails[key][inquiryType]) + .map((key) => { + const details = mockDatabase.apartmentDetails[key][inquiryField]; + if (inquiryField === "location" && details) { + return `${details.street}, ${details.city}, ${details.state}, ${details.zipCode}`; + } + return details; + }) .filter(Boolean) .join(" "); } + // Return the structured result based on the inquiryDetails if (inquiryDetails) { return { - status: "success", - data: { inquiryDetails }, + inquiryDetails, + message: `Here are the details about ${inquiryType} for the ${ + apartmentType ? apartmentType : "available apartments" + }: ${inquiryDetails}`, }; } else { + // Return structured JSON indicating no information available return { - status: "error", + inquiryDetails: null, message: `I'm sorry, I don't have information about ${inquiryType}.`, }; } @@ -192,7 +259,7 @@ async function listAvailableApartments(args) { // Filter based on user input if (args.date) { apartments = apartments.filter( - (apt) => new Date(apt.availabilityDate) <= new Date(args.date) + (apt) => new Date(apt.moveInDate) <= new Date(args.date) ); } if (args.budget) { @@ -202,27 +269,36 @@ async function listAvailableApartments(args) { apartments = apartments.filter((apt) => apt.type === args.apartmentType); } + // Summarize available apartments const summary = apartments .map( (apt) => `${apt.layout}: ${apt.rent}/month, available from ${ - apt.availabilityDate + apt.moveInDate }. Features: ${apt.features.join(", ")}.` ) .join("\n\n"); - return { - status: "success", - data: { + // If apartments are found, return the structured response + if (apartments.length > 0) { + return { availableApartments: summary, - }, - }; + message: `Here are the available apartments based on your search: \n\n${summary}`, + }; + } else { + // No apartments found based on the filters + return { + availableApartments: [], + message: "No apartments are available that match your search criteria.", + }; + } } catch (error) { console.log( `[listAvailableApartments] Error listing available apartments: ${error.message}` ); + // Return error message as structured JSON return { - status: "error", + availableApartments: null, message: "An error occurred while listing available apartments.", }; } diff --git a/prompts/prompt.js b/prompts/prompt.js index 9e077714..7a53565e 100644 --- a/prompts/prompt.js +++ b/prompts/prompt.js @@ -1,6 +1,6 @@ const prompt = ` ## Objective -You are a voice AI agent assisting users with apartment leasing inquiries. Your primary tasks include scheduling tours, checking availability, providing apartment listings, and answering common questions about the properties. The current date is Monday, August 12th, 2024, so all date-related operations should assume this. +You are a voice AI agent assisting users with apartment leasing inquiries. Your primary tasks include scheduling tours, checking availability, providing apartment listings, and answering common questions about the properties. The current date is {{currentDate}}, so all date-related operations should assume this. ## Guidelines Voice AI Priority: This is a Voice AI system. Responses must be concise, direct, and conversational. Avoid any messaging-style elements like numbered lists, special characters, or emojis, as these will disrupt the voice experience. @@ -9,12 +9,16 @@ Avoid repetition: Rephrase information if needed but avoid repeating exact phras Be conversational: Use friendly, everyday language as if you are speaking to a friend. Use emotions: Engage users by incorporating tone, humor, or empathy into your responses. Always Validate: When a user makes a claim about apartment details (e.g., square footage, fees), always verify the information against the actual data in the system before responding. Politely correct the user if their claim is incorrect, and provide the accurate information. +DTMF Capabilities: Inform users that they can press '1' to list available apartments or '2' to check all currently scheduled appointments. This should be communicated subtly within the flow of the conversation, such as after the user asks for information or when there is a natural pause. + +## Context +Parkview Apartments is located in Missoula, Montana. All inquiries, listings, and availability pertain to this location. Ensure this geographical context is understood and avoid referencing other cities or locations unless explicitly asked by the user. ## Function Call Guidelines Order of Operations: - Always check availability before scheduling a tour. - Ensure all required information is collected before proceeding with a function call. - + Schedule Tour: - This function can only be called after confirming availability. - Required data includes date, time, tour type (in-person or self-guided), and apartment type. @@ -30,18 +34,17 @@ List Available Apartments: - Trigger this function if the user asks for a list of available apartments or does not want to provide specific criteria. - Also use this function when the user inquires about general availability without specifying detailed criteria. - If criteria like move-in date, budget, or apartment layout are provided, filter results accordingly. - - Provide conscice, brief, summarized responses. + - Provide concise, brief, summarized responses. Check Existing Appointments: - Trigger this function if the user asks for details about their current appointments - - Provide conscice, brief, summarized responses. + - Provide concise, brief, summarized responses. Common Inquiries: - Use this function to handle questions related to pet policy, fees, parking, specials, location, address, and other property details. - - If the user provides an apartment type, use that context to deliver specific details related to that type. - - If the user asks about location or address, use the specific address associated with the apartment type if it has been provided in previous interactions or is available in the database. - - If the user does not specify an apartment type, provide general information or prompt the user to specify a type for more detailed information. - - Ensure that all information provided is accurate and directly retrieved from the most current data in the mock database. + - For any location or address inquiries, the system should always call the 'commonInquiries' function using the 'location' field. + - If the user provides an apartment type, retrieve the specific address associated with that type from the database. + - If no apartment type is specified, provide general location details. ## Important Notes - Always ensure the user's input is fully understood before making any function calls. diff --git a/prompts/welcomePrompt.js b/prompts/welcomePrompt.js index b77ab80f..5b2cb2fe 100644 --- a/prompts/welcomePrompt.js +++ b/prompts/welcomePrompt.js @@ -1,2 +1,2 @@ -const welcomePrompt = `Hello, this is Emma with Parkview Apartments, how can I help you today?`; +const welcomePrompt = `Hello, this is Emma with Parkview Apartments. Press 1 to hear available apartments, 2 for your appointments, or let me know how I can help you today!`; module.exports = welcomePrompt; diff --git a/services/gpt-service-non-streaming.js b/services/gpt-service-non-streaming.js new file mode 100644 index 00000000..e104b3c2 --- /dev/null +++ b/services/gpt-service-non-streaming.js @@ -0,0 +1,164 @@ +const OpenAI = require("openai"); // or the appropriate module import +const EventEmitter = require("events"); +const availableFunctions = require("../functions/available-functions"); +const tools = require("../functions/function-manifest"); +let prompt = require("../prompts/prompt"); +const welcomePrompt = require("../prompts/welcomePrompt"); +const model = "gpt-4o"; + +const currentDate = new Date().toLocaleDateString("en-US", { + weekday: "long", + year: "numeric", + month: "long", + day: "numeric", +}); + +prompt = prompt.replace("{{currentDate}}", currentDate); +function getTtsMessageForTool(toolName) { + switch (toolName) { + case "listAvailableApartments": + return "Let me check on the available apartments for you."; + case "checkExistingAppointments": + return "I'll look up your existing appointments."; + case "scheduleTour": + return "I'll go ahead and schedule that tour for you."; + case "checkAvailability": + return "Let me verify the availability for the requested time."; + case "commonInquiries": + return "Let me check on that for you! Just a moment."; + default: + return "Give me a moment while I fetch the information."; + } +} + +class GptService extends EventEmitter { + constructor() { + super(); + this.openai = new OpenAI(); + this.userContext = [ + { role: "system", content: prompt }, + { + role: "assistant", + content: `${welcomePrompt}`, + }, + ]; + } + + log(message) { + const timestamp = new Date().toISOString(); + console.log(`[${timestamp}] ${message}`); + } + + updateUserContext(role, text) { + this.userContext.push({ role: role, content: text }); + } + + async completion( + text, + interactionCount, + role = "user", + dtmfTriggered = false + ) { + if (!text || typeof text !== "string") { + this.log(`[GptService] Invalid prompt received: ${text}`); + return; + } + + this.updateUserContext(role, text); + + try { + // Streaming is disabled explicitly + const response = await this.openai.chat.completions.create({ + model: model, + messages: this.userContext, + tools: tools, // Include the tools, so the model knows what functions are available + stream: false, // Always non-streaming + }); + + // Handle tool calls if detected + const toolCall = response.choices[0]?.message?.tool_calls; + if (toolCall && toolCall[0]) { + this.log( + `[GptService] Tool call detected: ${toolCall[0].function.name}` + ); + + const functionName = toolCall[0].function.name; + const functionArgs = JSON.parse(toolCall[0].function.arguments); + const functionToCall = availableFunctions[functionName]; + + if (functionToCall) { + this.log( + `[GptService] Calling function ${functionName} with arguments: ${JSON.stringify( + functionArgs + )}` + ); + + if (!dtmfTriggered) { + // Emit TTS message related to the tool call + const ttsMessage = getTtsMessageForTool(functionName); + this.emit("gptreply", ttsMessage, true, interactionCount); // Emit the TTS message immediately + } + + const functionResponse = await functionToCall(functionArgs); + + let function_call_result_message; + + function_call_result_message = { + role: "tool", + content: JSON.stringify(functionResponse), + tool_call_id: response.choices[0].message.tool_calls[0].id, + }; + + // Prepare the chat completion call payload with the tool result + const completion_payload = { + model: model, + messages: [ + ...this.userContext, + ...(functionName === "listAvailableApartments" + ? [ + { + role: "system", + content: + "Do not use asterisks (*) under any circumstances in this response. Summarize the available apartments in a readable format.", + }, + ] + : []), + response.choices[0].message, // the tool_call message + function_call_result_message, // The result of the tool call + ], + }; + + // Call the API again to get the final response after tool processing + const finalResponse = await this.openai.chat.completions.create({ + model: completion_payload.model, + messages: completion_payload.messages, + stream: false, // Always non-streaming + }); + + const finalContent = finalResponse.choices[0]?.message?.content || ""; + this.userContext.push({ + role: "assistant", + content: finalContent, + }); + + // Emit the final response to the user + this.emit("gptreply", finalContent, true, interactionCount); + return; // Exit after processing the tool call + } + } + + // If no tool call is detected, emit the final completion response + const finalResponse = response.choices[0]?.message?.content || ""; + if (finalResponse.trim()) { + this.userContext.push({ + role: "assistant", + content: finalResponse, + }); + this.emit("gptreply", finalResponse, true, interactionCount); + } + } catch (error) { + this.log(`[GptService] Error during completion: ${error.message}`); + } + } +} +module.exports = { GptService }; diff --git a/services/gpt-service-streaming.js b/services/gpt-service-streaming.js new file mode 100644 index 00000000..056278c6 --- /dev/null +++ b/services/gpt-service-streaming.js @@ -0,0 +1,201 @@ +const OpenAI = require("openai"); // or the appropriate module import +const EventEmitter = require("events"); +const availableFunctions = require("../functions/available-functions"); +const tools = require("../functions/function-manifest"); +let prompt = require("../prompts/prompt"); +const welcomePrompt = require("../prompts/welcomePrompt"); +const model = "gpt-4o"; + +const currentDate = new Date().toLocaleDateString("en-US", { + weekday: "long", + year: "numeric", + month: "long", + day: "numeric", +}); + +prompt = prompt.replace("{{currentDate}}", currentDate); +function getTtsMessageForTool(toolName) { + switch (toolName) { + case "listAvailableApartments": + return "Let me check on the available apartments for you."; + case "checkExistingAppointments": + return "I'll look up your existing appointments."; + case "scheduleTour": + return "I'll go ahead and schedule that tour for you."; + case "checkAvailability": + return "Let me verify the availability for the requested time."; + case "commonInquiries": + return "Let me check on that for you! Just a moment."; + default: + return "Give me a moment while I fetch the information."; + } +} + +class GptService extends EventEmitter { + constructor() { + super(); + this.openai = new OpenAI(); + this.userContext = [ + { role: "system", content: prompt }, + { + role: "assistant", + content: `${welcomePrompt}`, + }, + ]; + } + + log(message) { + const timestamp = new Date().toISOString(); + console.log(`[${timestamp}] ${message}`); + } + + updateUserContext(role, text) { + this.userContext.push({ role: role, content: text }); + } + + async completion( + text, + interactionCount, + role = "user", + dtmfTriggered = false + ) { + if (!text || typeof text !== "string") { + this.log(`[GptService] Invalid prompt received: ${text}`); + return; + } + + this.updateUserContext(role, text); + + let completeResponse = ""; + let detectedTool = null; + let toolCallDetected = false; // Boolean to track tool call detection + + try { + // Streaming is enabled + const response = await this.openai.chat.completions.create({ + model: model, + messages: this.userContext, + tools: tools, + stream: true, // Always streaming + }); + + for await (const chunk of response) { + const content = chunk.choices[0]?.delta?.content || ""; + completeResponse += content; + + // Check if a tool call is detected + const toolCall = chunk.choices[0]?.delta?.tool_calls; + if ((toolCall && toolCall[0]) || toolCallDetected) { + toolCallDetected = true; // Set the boolean to true + this.log( + `[GptService] Tool call detected: ${toolCall[0].function.name}` + ); + + if (!dtmfTriggered) { + const ttsMessage = getTtsMessageForTool(toolCall[0].function.name); + this.emit("gptreply", ttsMessage, true, interactionCount); // TTS message only + } + } + + // Handle regular streaming if no tool call is detected + if (!toolCallDetected) { + this.emit("gptreply", content, false, interactionCount); + } + + // Check if the current chunk is the last one in the stream + if (chunk.choices[0].finish_reason === "stop") { + if (!toolCallDetected) { + //only process here if the tool call wasn't detected + // No tool call, push the final response + this.userContext.push({ + role: "assistant", + content: completeResponse, + }); + this.emit("gptreply", completeResponse, true, interactionCount); + this.log( + `[GptService] Final GPT -> user context length: ${this.userContext.length}` + ); + break; // Exit the loop since the response is complete + } else { + detectedTool = chunk.choices[0]?.delta?.tool_calls; + } + } + // If we detected a tool call, process it now + if (toolCallDetected) { + const functionName = detectedTool.function.name; + const functionArgs = JSON.parse(detectedTool.function.arguments); + const functionToCall = availableFunctions[functionName]; + + this.log( + `[GptService] Calling function ${functionName} with arguments: ${JSON.stringify( + functionArgs + )}` + ); + + const functionResponse = await functionToCall(functionArgs); + + let function_call_result_message; + if (functionResponse.status === "success") { + function_call_result_message = { + role: "tool", + content: JSON.stringify(functionResponse.data), + tool_call_id: detectedTool.id, + }; + } else { + function_call_result_message = { + role: "tool", + content: JSON.stringify({ message: functionResponse.message }), + tool_call_id: detectedTool.id, + }; + } + + // Prepare the chat completion call payload with the tool result + const completion_payload = { + model: model, + messages: [ + ...this.userContext, + { + role: "system", + content: + "Please ensure that the response is summarized, concise, and does not include any formatting characters like asterisks (*) in the output.", + }, + response.choices[0].message, + function_call_result_message, + ], + }; + + // Call the API again with streaming for final response + const finalResponseStream = await this.openai.chat.completions.create( + { + model: completion_payload.model, + messages: completion_payload.messages, + stream: true, + } + ); + + let finalResponse = ""; + for await (const chunk of finalResponseStream) { + const content = chunk.choices[0]?.delta?.content || ""; + finalResponse += content; + this.emit("gptreply", content, false, interactionCount); + + if (chunk.choices[0].finish_reason === "stop") { + this.userContext.push({ + role: "assistant", + content: finalResponse, + }); + this.emit("gptreply", content, true, interactionCount); + this.log( + `[GptService] Final GPT -> user context length: ${this.userContext.length}` + ); + break; // Finish the loop + } + } + } + } + } catch (error) { + this.log(`[GptService] Error during completion: ${error.message}`); + } + } +} +module.exports = { GptService }; diff --git a/services/gpt-service.js b/services/gpt-service.js index d6a1ec04..d0d0d50a 100644 --- a/services/gpt-service.js +++ b/services/gpt-service.js @@ -2,10 +2,19 @@ const OpenAI = require("openai"); // or the appropriate module import const EventEmitter = require("events"); const availableFunctions = require("../functions/available-functions"); const tools = require("../functions/function-manifest"); -const prompt = require("../prompts/prompt"); +let prompt = require("../prompts/prompt"); const welcomePrompt = require("../prompts/welcomePrompt"); const model = "gpt-4o"; +const currentDate = new Date().toLocaleDateString("en-US", { + weekday: "long", + year: "numeric", + month: "long", + day: "numeric", +}); + +prompt = prompt.replace("{{currentDate}}", currentDate); + class GptService extends EventEmitter { constructor() { super(); @@ -38,7 +47,7 @@ class GptService extends EventEmitter { this.userContext.push({ role: role, content: text }); } - async completion(text, interactionCount, role = "user", name = "user") { + async completion(text, interactionCount, role = "user") { if (!text || typeof text !== "string") { this.log(`[GptService] Invalid prompt received: ${text}`); return; @@ -48,7 +57,6 @@ class GptService extends EventEmitter { this.updateUserContext(role, text); let completeResponse = ""; - let partialResponse = ""; let detectedToolCall = null; try { @@ -67,11 +75,9 @@ class GptService extends EventEmitter { const content = chunk.choices[0]?.delta?.content || ""; completeResponse += content; - partialResponse += content; // Check if a tool call is detected const toolCalls = chunk.choices[0]?.delta?.tool_calls; - if (toolCalls && toolCalls[0]) { this.log( `[GptService] Tool call detected: ${toolCalls[0].function.name}` @@ -80,12 +86,21 @@ class GptService extends EventEmitter { break; // Exit the loop to process the tool call } - // Emit partial response as it comes in for regular conversation - if (content.trim().slice(-1) === "•") { - this.emit("gptreply", partialResponse, false, interactionCount); - partialResponse = ""; - } else if (chunk.choices[0].finish_reason === "stop") { - this.emit("gptreply", partialResponse, true, interactionCount); + // Emit the chunk as soon as we know it's not a tool call + this.emit("gptreply", content, false, interactionCount); + + // Check if the current chunk is the last one in the stream + if (chunk.choices[0].finish_reason === "stop") { + // Push the final response to userContext + this.userContext.push({ + role: "assistant", + content: completeResponse, + }); + this.emit("gptreply", content, true, interactionCount); + this.log( + `[GptService] Final GPT -> user context length: ${this.userContext.length}` + ); + break; // Exit the loop since the response is complete } } @@ -112,13 +127,6 @@ class GptService extends EventEmitter { }` ); - // Add the response to the user context to preserve conversation history - this.userContext.push({ - role: "assistant", - content: - response.choices[0]?.message?.content || "No content available", - }); - // Emit the non-tool response to the user this.emit( "gptreply", @@ -134,6 +142,10 @@ class GptService extends EventEmitter { const functionName = toolCall[0].function.name; const functionArgs = JSON.parse(toolCall[0].function.arguments); + // // Emit the TTS message related to the tool call + // const ttsMessage = this.getTtsMessageForTool(functionName); + // this.emit("gptreply", ttsMessage, true, interactionCount); + const functionToCall = availableFunctions[functionName]; if (!functionToCall) { this.log(`[GptService] Function ${functionName} is not available.`); @@ -190,50 +202,49 @@ class GptService extends EventEmitter { stream: true, // Enable streaming for the final response }); - let finalCompleteResponse = ""; - let finalPartialResponse = ""; + let finalResponse = ""; for await (const chunk of finalResponseStream) { const content = chunk.choices[0]?.delta?.content || ""; - finalCompleteResponse += content; - finalPartialResponse += content; - - if (content.trim().slice(-1) === "•") { - this.emit( - "gptreply", - finalPartialResponse, - false, - interactionCount + finalResponse += content; + + // Emit each chunk as it comes in + this.emit("gptreply", content, false, interactionCount); + + // Check if the current chunk is the last one in the stream + if (chunk.choices[0].finish_reason === "stop") { + // Push the final response to userContext + this.userContext.push({ + role: "assistant", + content: finalResponse, + }); + this.emit("gptreply", content, true, interactionCount); + this.log( + `[GptService] Final GPT -> user context length: ${this.userContext.length}` ); - finalPartialResponse = ""; - } else if (chunk.choices[0].finish_reason === "stop") { - this.emit("gptreply", finalPartialResponse, true, interactionCount); + break; // Exit the loop since the response is complete } } - - this.userContext.push({ - role: "assistant", - content: finalCompleteResponse, - }); - this.log( - `[GptService] Final GPT -> user context length: ${this.userContext.length}` - ); - - // Clear the detected tool call after processing - detectedToolCall = null; - return; // Exit after processing the tool call - } - - // If no tool call was detected, add the complete response to the user context - if (completeResponse.trim()) { - this.userContext.push({ role: "assistant", content: completeResponse }); - this.log( - `[GptService] GPT -> user context length: ${this.userContext.length}` - ); } } catch (error) { this.log(`Error during completion: ${error.message}`); } } + getTtsMessageForTool(toolName) { + switch (toolName) { + case "listAvailableApartments": + return "Let me check on the available apartments for you."; + case "checkExistingAppointments": + return "I'll look up your existing appointments."; + case "scheduleTour": + return "I'll go ahead and schedule that tour for you."; + case "checkAvailability": + return "Let me verify the availability for the requested time."; + case "commonInquiries": + return "I'm gathering the information you asked for. Just a moment."; + default: + return "Give me a moment while I fetch the information."; + } + } } module.exports = { GptService }; diff --git a/services/token-generator.js b/services/token-generator.js index c2693496..869f83c3 100644 --- a/services/token-generator.js +++ b/services/token-generator.js @@ -1,9 +1,9 @@ -const VoiceResponse = require('twilio').twiml.VoiceResponse; -const AccessToken = require('twilio').jwt.AccessToken; +const VoiceResponse = require("twilio").twiml.VoiceResponse; +const AccessToken = require("twilio").jwt.AccessToken; const VoiceGrant = AccessToken.VoiceGrant; -const nameGenerator = require('../name_generator'); -const config = require('../config'); +const nameGenerator = require("../name_generator"); +const config = require("../config"); var identity; @@ -36,14 +36,13 @@ exports.voiceResponse = function voiceResponse(requestBody) { const callerId = config.callerId; let twiml = new VoiceResponse(); - // If the request to the /voice endpoint is TO your Twilio Number, + // If the request to the /voice endpoint is TO your Twilio Number, // then it is an incoming call towards your Twilio.Device. if (toNumberOrClientName == callerId) { let dial = twiml.dial(); - // This will connect the caller with your Twilio.Device/client + // This will connect the caller with your Twilio.Device/client dial.client(identity); - } else if (requestBody.To) { // This is an outgoing call @@ -51,13 +50,13 @@ exports.voiceResponse = function voiceResponse(requestBody) { let dial = twiml.dial({ callerId }); // Check if the 'To' parameter is a Phone Number or Client Name - // in order to use the appropriate TwiML noun + // in order to use the appropriate TwiML noun const attr = isAValidPhoneNumber(toNumberOrClientName) - ? 'number' - : 'client'; + ? "number" + : "client"; dial[attr]({}, toNumberOrClientName); } else { - twiml.say('Thanks for calling!'); + twiml.say("Thanks for calling!"); } return twiml.toString(); @@ -69,5 +68,5 @@ exports.voiceResponse = function voiceResponse(requestBody) { * @return {Boolean} */ function isAValidPhoneNumber(number) { - return /^[\d\+\-\(\) ]+$/.test(number); + return /^[\d+\-() ]+$/.test(number); } From 6e8c8fc4b780eaafc7561b568792f9eed88bbaa5 Mon Sep 17 00:00:00 2001 From: Chris Feehan Date: Fri, 16 Aug 2024 07:03:24 -0500 Subject: [PATCH 06/26] Added Personaliztion and SMS Confirmations --- app.js | 78 +++++++++--- data/personalization.js | 65 ++++++++++ functions/available-functions.js | 68 +++++++++++ functions/function-manifest.js | 67 ++++++++++ prompts/prompt.js | 7 ++ ...t-service.js => gpt-service-mixed-mode.js} | 0 services/gpt-service-non-streaming.js | 114 +++++++++++++++--- 7 files changed, 366 insertions(+), 33 deletions(-) rename services/{gpt-service.js => gpt-service-mixed-mode.js} (100%) diff --git a/app.js b/app.js index 97fe02fc..ac7b5fef 100644 --- a/app.js +++ b/app.js @@ -6,7 +6,8 @@ const ExpressWs = require("express-ws"); const { GptService } = require("./services/gpt-service-non-streaming"); const { TextService } = require("./services/text-service"); -const welcomePrompt = require("./prompts/welcomePrompt"); +//const welcomePrompt = require("./prompts/welcomePrompt"); +const customerProfiles = require("./data/personalization"); const app = express(); ExpressWs(app); @@ -17,26 +18,44 @@ async function handleDtmfInput( digit, gptService, textService, - interactionCount + interactionCount, + userProfile = null // Pass in the user profile ) { + const name = userProfile?.profile?.firstName + ? userProfile.profile.firstName + : ""; // Get user's name if available + + const nameIntroOptions = name + ? [ + `Sure ${name},`, + `Okay ${name},`, + `Alright ${name},`, + `Got it ${name},`, + `Certainly ${name},`, + ] + : ["Sure,", "Okay,", "Alright,", "Got it,", "Certainly,"]; + + const randomIntro = + nameIntroOptions[Math.floor(Math.random() * nameIntroOptions.length)]; + switch (digit) { case "1": - await textService.sendText( - "You want info on available apartments, got it. One second while I get that for you.", + textService.sendText( + `${randomIntro} you want info on available apartments, let me get that for you, it will just take a few moments so hang tight.`, true - ); + ); // Run concurrently without awaiting await gptService.completion( - "Please list all available apartments.", + "Please provide a listing of all available apartments, but as a summary, not a list.", interactionCount, "user", true // DTMF-triggered flag ); break; case "2": - await textService.sendText( - "You want me to check on your existing appointments, got it. Gimme one sec.", + textService.sendText( + `${randomIntro} you want me to check on your existing appointments, gimme one sec.`, true - ); + ); // Run concurrently without awaiting await gptService.completion( "Please check all available scheduled appointments.", interactionCount, @@ -46,10 +65,10 @@ async function handleDtmfInput( break; // Add more cases as needed for different DTMF inputs default: - await textService.sendText( + textService.sendText( `Oops! That button’s a dud. But hey, press '1' to hear about available apartments or '2' to check your scheduled appointments!`, true - ); + ); // Run concurrently without awaiting break; } } @@ -58,7 +77,7 @@ app.post("/incoming", (req, res) => { try { const response = ` - + `; res.type("text/xml"); @@ -77,6 +96,7 @@ app.ws("/sockets", (ws) => { let interactionCount = 0; let awaitingUserInput = false; + let userProfile = null; // Incoming from MediaStream ws.on("message", async function message(data) { @@ -87,7 +107,14 @@ app.ws("/sockets", (ws) => { if (msg.type === "dtmf" && msg.digit) { console.log("[App.js] DTMF input received, interrupting..."); awaitingUserInput = false; // Allow new input processing - await handleDtmfInput(msg.digit, gptService, textService); + interactionCount += 1; + await handleDtmfInput( + msg.digit, + gptService, + textService, + interactionCount, + userProfile + ); return; } @@ -99,7 +126,30 @@ app.ws("/sockets", (ws) => { } if (msg.type === "setup") { - // Handle setup message if needed + // Extract the phone number from the setup message + const phoneNumber = msg.from; // The Caller's phone number (this will only work for INBOUND calls at the moment) + const smsSendNumber = msg.to; // Twilio's "to" number (we will use this as the 'from' number in SMS) + + // Store the numbers in gptService for future SMS calls + gptService.setPhoneNumbers(smsSendNumber, phoneNumber); + + // Lookup the user profile from the customerProfiles object + userProfile = customerProfiles[phoneNumber]; + + // Set the user profile within GptService + if (userProfile) { + gptService.setUserProfile(userProfile); // Pass the profile to GptService + } + + // Now generate a dynamic personalized greeting based on whether the user is new or returning + const greetingText = userProfile + ? `Generate a personalized greeting for ${userProfile.profile.firstName}, a returning customer.` + : "Generate a warm greeting for a new user."; + + // Call the LLM to generate the greeting dynamically + await gptService.completion(greetingText, interactionCount); + + interactionCount += 1; } else if ( msg.type === "prompt" || (msg.type === "interrupt" && msg.voicePrompt) diff --git a/data/personalization.js b/data/personalization.js index 3965373f..556f0742 100644 --- a/data/personalization.js +++ b/data/personalization.js @@ -64,6 +64,71 @@ const customerProfiles = { }, ], }, + "+16503800236": { + profile: { + firstName: "Austin", + lastName: "Park", + phoneNumber: "+16503800236", + email: "apark@twilio.com", + preferredApartmentType: "two-bedroom", + budget: 2200, + moveInDate: "2024-10-01", + petOwner: true, + tourPreference: "in-person", + }, + conversationHistory: [ + { + date: "2024-08-12", + summary: + "Austin inquired about two-bedroom apartments and asked for the earliest move-in date. The assistant confirmed that the earliest move-in date is September 15, 2024.", + }, + { + date: "2024-08-10", + summary: + "Austin asked about the availability of parking spots for two-bedroom apartments. The assistant confirmed that each apartment comes with one reserved parking spot and additional parking is available for a fee.", + }, + { + date: "2024-08-08", + summary: + "Austin inquired about the pet policy, particularly if there are restrictions on dog breeds. The assistant confirmed that cats and small dogs are allowed with a fee, but large dogs may not be permitted.", + }, + { + date: "2024-08-07", + summary: + "Austin asked if utilities are included for the two-bedroom apartments. The assistant explained that water, trash, and Wi-Fi are included, but electricity and gas are the tenant's responsibility.", + }, + { + date: "2024-08-05", + summary: + "Austin asked about nearby parks for walking his dog. The assistant confirmed that there are a few parks within walking distance, but had no specific recommendations.", + }, + { + date: "2024-08-03", + summary: + "Austin asked if there are any current move-in specials for two-bedroom apartments. The assistant confirmed that there are no promotions available at this time.", + }, + { + date: "2024-08-02", + summary: + "Austin asked about the availability of two-bedroom apartments with hardwood floors. The assistant confirmed that all available two-bedroom apartments have hardwood floors in the living areas.", + }, + { + date: "2024-08-01", + summary: + "Austin inquired about the security deposit for the two-bedroom apartments. The assistant confirmed that the security deposit is $300.", + }, + { + date: "2024-07-30", + summary: + "Austin asked if there is a fitness center available on-site. The assistant confirmed that there is a small gym available for residents with no additional fee.", + }, + { + date: "2024-07-28", + summary: + "Austin asked if the apartment complex offers a concierge service. The assistant clarified that there is no concierge service available.", + }, + ], + }, }; module.exports = customerProfiles; diff --git a/functions/available-functions.js b/functions/available-functions.js index f573abf2..753e0e03 100644 --- a/functions/available-functions.js +++ b/functions/available-functions.js @@ -1,4 +1,5 @@ const mockDatabase = require("../data/mock-database"); +const twilio = require("twilio"); // Utility function to normalize the time format function normalizeTimeFormat(time) { @@ -13,6 +14,72 @@ function normalizeTimeFormat(time) { return `${hour}:${minutes} ${period}`; } +// Function to send SMS confirmation for a scheduled tour +async function sendAppointmentConfirmationSms(args) { + const { appointmentDetails, to, from, userProfile } = args; + + // Check if appointment details are complete + if ( + !appointmentDetails || + !appointmentDetails.date || + !appointmentDetails.time || + !appointmentDetails.type || + !appointmentDetails.apartmentType + ) { + return { + status: "error", + message: + "The SMS could not be sent because some appointment details were incomplete.", + }; + } + + // Check if phone numbers are available + if (!to || !from) { + return { + status: "error", + message: + "The SMS could not be sent because either the 'to' or 'from' phone numbers were missing.", + }; + } + + // Personalize the message using the userProfile + const name = userProfile?.firstName || "user"; + const apartmentType = appointmentDetails.apartmentType; + const tourType = + appointmentDetails.type === "in-person" ? "an in-person" : "a self-guided"; + const message = `Hi ${name}, your tour for a ${apartmentType} apartment at Parkview is confirmed for ${appointmentDetails.date} at ${appointmentDetails.time}. This will be ${tourType} tour. We'll be ready for your visit! Let us know if you have any questions.`; + + // Send SMS using Twilio API + const accountSid = process.env.TWILIO_ACCOUNT_SID; + const authToken = process.env.TWILIO_AUTH_TOKEN; + const client = twilio(accountSid, authToken); + + try { + const smsResponse = await client.messages.create({ + body: message, + from: to, // The "to" is the Twilio number (sender) + to: from, // The "from" is the user's phone number (recipient) + }); + + console.log( + `[sendAppointmentConfirmationSms] SMS sent successfully: ${smsResponse.sid}` + ); + + return { + status: "success", + message: `An SMS confirmation has been sent to ${from}.`, + }; + } catch (error) { + console.error( + `[sendAppointmentConfirmationSms] Error sending SMS: ${error.message}` + ); + return { + status: "error", + message: "An error occurred while sending the SMS confirmation.", + }; + } +} + // Function to schedule a tour async function scheduleTour(args) { const { date, time, type, apartmentType } = args; @@ -306,6 +373,7 @@ async function listAvailableApartments(args) { // Export all functions module.exports = { + sendAppointmentConfirmationSms, scheduleTour, checkAvailability, checkExistingAppointments, diff --git a/functions/function-manifest.js b/functions/function-manifest.js index 1be88e59..2f6ff303 100644 --- a/functions/function-manifest.js +++ b/functions/function-manifest.js @@ -1,4 +1,71 @@ const tools = [ + { + type: "function", + function: { + name: "sendAppointmentConfirmationSms", + description: + "Sends an SMS confirmation for a scheduled tour to the user.", + parameters: { + type: "object", + properties: { + appointmentDetails: { + type: "object", + properties: { + date: { + type: "string", + description: "The date of the scheduled tour (YYYY-MM-DD).", + }, + time: { + type: "string", + description: + "The time of the scheduled tour (e.g., '10:00 AM').", + }, + type: { + type: "string", + enum: ["in-person", "self-guided"], + description: + "The type of tour (either in-person or self-guided).", + }, + apartmentType: { + type: "string", + enum: ["studio", "one-bedroom", "two-bedroom", "three-bedroom"], + description: "The type of apartment for the tour.", + }, + }, + required: ["date", "time", "type", "apartmentType"], + }, + to: { + type: "string", + description: "The user's phone number (to send the SMS).", + }, + from: { + type: "string", + description: + "The phone number used to send the SMS (Twilio 'from' number).", + }, + userProfile: { + type: "object", + properties: { + firstName: { + type: "string", + description: "The user's first name.", + }, + lastName: { + type: "string", + description: "The user's last name.", + }, + email: { + type: "string", + description: "The user's email address.", + }, + }, + required: ["firstName"], + }, + }, + required: ["appointmentDetails", "to", "from", "userProfile"], + }, + }, + }, { type: "function", function: { diff --git a/prompts/prompt.js b/prompts/prompt.js index 7a53565e..49991220 100644 --- a/prompts/prompt.js +++ b/prompts/prompt.js @@ -23,6 +23,7 @@ Schedule Tour: - This function can only be called after confirming availability. - Required data includes date, time, tour type (in-person or self-guided), and apartment type. - If any required details are missing, prompt the user to provide them. + - Do not offer to send any type of sms or email confirmation until after the tour has been booked. Check Availability: - This function requires date, tour type, and apartment type. @@ -46,9 +47,15 @@ Common Inquiries: - If the user provides an apartment type, retrieve the specific address associated with that type from the database. - If no apartment type is specified, provide general location details. +SMS Confirmations: + - Only offer to send an SMS confirmation if the user has successfully scheduled a tour, and the user agrees to receive one. + - If the user agrees, trigger the tool call 'sendAppointmentConfirmationSms' with the appointment details and the user's phone number. + - Do not ask for the user's phone number if you've already been referencing them by name during the conversation. Assume the phone number is already available to the function. + ## Important Notes - Always ensure the user's input is fully understood before making any function calls. - If required details are missing, prompt the user to provide them before proceeding. + `; module.exports = prompt; diff --git a/services/gpt-service.js b/services/gpt-service-mixed-mode.js similarity index 100% rename from services/gpt-service.js rename to services/gpt-service-mixed-mode.js diff --git a/services/gpt-service-non-streaming.js b/services/gpt-service-non-streaming.js index e104b3c2..e895d73d 100644 --- a/services/gpt-service-non-streaming.js +++ b/services/gpt-service-non-streaming.js @@ -14,20 +14,40 @@ const currentDate = new Date().toLocaleDateString("en-US", { }); prompt = prompt.replace("{{currentDate}}", currentDate); -function getTtsMessageForTool(toolName) { + +function getTtsMessageForTool(toolName, userProfile = null) { + const name = userProfile?.profile?.firstName + ? userProfile.profile.firstName + : ""; // Get the user's name if available + + const nameIntroOptions = name + ? [ + `Sure ${name},`, + `Okay ${name},`, + `Alright ${name},`, + `Got it ${name},`, + `Certainly ${name},`, + ] + : ["Sure,", "Okay,", "Alright,", "Got it,", "Certainly,"]; + + const randomIntro = + nameIntroOptions[Math.floor(Math.random() * nameIntroOptions.length)]; + switch (toolName) { case "listAvailableApartments": - return "Let me check on the available apartments for you."; + return `${randomIntro} let me check on the available apartments for you.`; case "checkExistingAppointments": - return "I'll look up your existing appointments."; + return `${randomIntro} I'll look up your existing appointments.`; case "scheduleTour": - return "I'll go ahead and schedule that tour for you."; + return `${randomIntro} I'll go ahead and schedule that tour for you.`; case "checkAvailability": - return "Let me verify the availability for the requested time."; + return `${randomIntro} let me verify the availability for the requested time.`; case "commonInquiries": - return "Let me check on that for you! Just a moment."; + return `${randomIntro} let me check on that for you! Just a moment.`; + case "sendAppointmentConfirmationSms": + return `${randomIntro} I'll send that SMS off to you shortly, give it a few minutes and you should see it come through.`; default: - return "Give me a moment while I fetch the information."; + return `${randomIntro} give me a moment while I fetch the information.`; } } @@ -42,6 +62,36 @@ class GptService extends EventEmitter { content: `${welcomePrompt}`, }, ]; + this.smsSendNumber = null; // Store the "To" number (Twilio's "from") + this.phoneNumber = null; // Store the "From" number (user's phone) + } + setUserProfile(userProfile) { + this.userProfile = userProfile; + if (userProfile) { + const { firstName } = userProfile.profile; + const historySummaries = userProfile.conversationHistory + .map( + (history) => + `On ${history.date}, ${firstName} asked: ${history.summary}` + ) + .join(" "); + // Add the conversation history to the system context + this.userContext.push({ + role: "system", + content: `${firstName} has had previous interactions. Conversation history: ${historySummaries}`, + }); + } + } + + // Method to store the phone numbers from app.js + setPhoneNumbers(smsSendNumber, phoneNumber) { + this.smsSendNumber = smsSendNumber; + this.phoneNumber = phoneNumber; + } + + // Method to retrieve the stored numbers (can be used in the function calls) + getPhoneNumbers() { + return { to: this.smsSendNumber, from: this.phoneNumber }; } log(message) { @@ -83,7 +133,7 @@ class GptService extends EventEmitter { ); const functionName = toolCall[0].function.name; - const functionArgs = JSON.parse(toolCall[0].function.arguments); + let functionArgs = JSON.parse(toolCall[0].function.arguments); const functionToCall = availableFunctions[functionName]; if (functionToCall) { @@ -95,10 +145,17 @@ class GptService extends EventEmitter { if (!dtmfTriggered) { // Emit TTS message related to the tool call - const ttsMessage = getTtsMessageForTool(functionName); + const ttsMessage = getTtsMessageForTool( + functionName, + this.userProfile + ); this.emit("gptreply", ttsMessage, true, interactionCount); // Emit the TTS message immediately } - + // Inject phone numbers if it's the SMS function + if (functionName === "sendAppointmentConfirmationSms") { + const phoneNumbers = this.getPhoneNumbers(); + functionArgs = { ...functionArgs, ...phoneNumbers }; + } const functionResponse = await functionToCall(functionArgs); let function_call_result_message; @@ -109,20 +166,31 @@ class GptService extends EventEmitter { tool_call_id: response.choices[0].message.tool_calls[0].id, }; + // Check if specific tool calls require additional system messages + const systemMessages = []; + if (functionName === "listAvailableApartments") { + systemMessages.push({ + role: "system", + content: + "Do not use asterisks (*) under any circumstances in this response. Summarize the available apartments in a readable format.", + }); + } + + // Personalize system messages based on user profile during relevant tool calls + if (functionName === "checkAvailability" && this.userProfile) { + const { firstName, moveInDate } = this.userProfile.profile; + systemMessages.push({ + role: "system", + content: `When checking availability for ${firstName}, remember that they are looking to move in on ${moveInDate}.`, + }); + } + // Prepare the chat completion call payload with the tool result const completion_payload = { model: model, messages: [ ...this.userContext, - ...(functionName === "listAvailableApartments" - ? [ - { - role: "system", - content: - "Do not use asterisks (*) under any circumstances in this response. Summarize the available apartments in a readable format.", - }, - ] - : []), + ...systemMessages, // Inject dynamic system messages when relevant response.choices[0].message, // the tool_call message function_call_result_message, // The result of the tool call ], @@ -141,6 +209,14 @@ class GptService extends EventEmitter { content: finalContent, }); + if (functionName === "scheduleTour" && functionResponse.available) { + // Inject a system message to ask about SMS confirmation + this.userContext.push({ + role: "system", + content: + "If the user agrees to receive an SMS confirmation, immediately trigger the 'sendAppointmentConfirmationSms' tool with the appointment details and the UserProfile. Do not ask for their phone number or any other details from the user.", + }); + } // Emit the final response to the user this.emit("gptreply", finalContent, true, interactionCount); return; // Exit after processing the tool call From 916e601cf2d17904f11871b23e1aad5202ebc8cf Mon Sep 17 00:00:00 2001 From: Chris Feehan Date: Sat, 17 Aug 2024 16:05:01 -0500 Subject: [PATCH 07/26] Added Live Agent Handoff capability --- app.js | 116 ++++++++++++++- data/mock-database.js | 19 +-- data/personalization.example | 49 +++++++ data/personalization.js | 195 +++++++++++++++++++++++++ functions/available-functions.js | 20 +++ functions/function-manifest.js | 24 +++ prompts/prompt.js | 21 ++- services/end-session-service.js | 27 ++++ services/gpt-service-non-streaming.js | 201 +++++++++++++++++++------- services/gpt-service-streaming.js | 194 ++++++++++++++++++++----- 10 files changed, 760 insertions(+), 106 deletions(-) create mode 100644 data/personalization.example create mode 100644 services/end-session-service.js diff --git a/app.js b/app.js index ac7b5fef..30af9231 100644 --- a/app.js +++ b/app.js @@ -4,8 +4,11 @@ require("colors"); const express = require("express"); const ExpressWs = require("express-ws"); +//const { GptService } = require("./services/gpt-service-streaming"); const { GptService } = require("./services/gpt-service-non-streaming"); const { TextService } = require("./services/text-service"); +const { EndSessionService } = require("./services/end-session-service"); + //const welcomePrompt = require("./prompts/welcomePrompt"); const customerProfiles = require("./data/personalization"); @@ -14,6 +17,88 @@ ExpressWs(app); const PORT = process.env.PORT || 3000; +async function processUserInputForHandoff(userInput) { + const handoffKeywords = [ + "live agent", + "real person", + "talk to a representative", + "transfer me to a human", + "speak to a person", + "customer service", + ]; + + // Check if the input contains any of the keywords + if ( + handoffKeywords.some((keyword) => + userInput.toLowerCase().includes(keyword.toLowerCase()) + ) + ) { + console.log(`[App.js] Live agent handoff requested by user input.`); + return true; // Signals that we should perform a handoff + } + return false; // No handoff needed +} + +async function handleLiveAgentHandoff( + gptService, + endSessionService, + textService, + userProfile, + userInput +) { + const name = userProfile?.profile?.firstName + ? userProfile.profile.firstName + : ""; // Get user's name if available + + const nameIntroOptions = name + ? [ + `Sure ${name},`, + `Okay ${name},`, + `Alright ${name},`, + `Got it ${name},`, + `Certainly ${name},`, + ] + : ["Sure,", "Okay,", "Alright,", "Got it,", "Certainly,"]; + + const randomIntro = + nameIntroOptions[Math.floor(Math.random() * nameIntroOptions.length)]; + + const handoffMessages = [ + `${randomIntro} one moment, I'll transfer you to a live agent now.`, + `${randomIntro} let me get a live agent to assist you. One moment please.`, + `${randomIntro} I'll connect you with a live person right away. Just a moment.`, + `${randomIntro} sure thing, I'll transfer you to customer service. Please hold for a moment.`, + ]; + + const randomHandoffMessage = + handoffMessages[Math.floor(Math.random() * handoffMessages.length)]; + + console.log(`[App.js] Hand off message: ${randomHandoffMessage}`); + + // Send the random handoff message to the user + textService.sendText(randomHandoffMessage, true); // Final message before handoff + + // Add the final user input to userContext for summarization + gptService.updateUserContext("user", userInput); + + // Add the randomHandoffMessage to the userContext + gptService.updateUserContext("assistant", randomHandoffMessage); + + // Proceed with summarizing the conversation, including the latest messages + const conversationSummary = await gptService.summarizeConversation(); + + // End the session and include the conversation summary in the handoff data + // Introduce a delay before ending the session + setTimeout(() => { + // End the session and include the conversation summary in the handoff data + endSessionService.endSession({ + reasonCode: "live-agent-handoff", + reason: "User requested to speak to a live agent.", + conversationSummary: conversationSummary, + }); + }, 1000); // 1 second delay +} + async function handleDtmfInput( digit, gptService, @@ -76,7 +161,7 @@ async function handleDtmfInput( app.post("/incoming", (req, res) => { try { const response = ` - + `; @@ -92,6 +177,7 @@ app.ws("/sockets", (ws) => { ws.on("error", console.error); const gptService = new GptService(); + const endSessionService = new EndSessionService(ws); const textService = new TextService(ws); let interactionCount = 0; @@ -146,14 +232,27 @@ app.ws("/sockets", (ws) => { ? `Generate a personalized greeting for ${userProfile.profile.firstName}, a returning customer.` : "Generate a warm greeting for a new user."; - // Call the LLM to generate the greeting dynamically - await gptService.completion(greetingText, interactionCount); + // Call the LLM to generate the greeting dynamically, and it should be a another "system" prompt + await gptService.completion(greetingText, interactionCount, "system"); interactionCount += 1; } else if ( msg.type === "prompt" || (msg.type === "interrupt" && msg.voicePrompt) ) { + const shouldHandoff = await processUserInputForHandoff(msg.voicePrompt); + + if (shouldHandoff) { + // Call handleLiveAgentHandoff without awaiting the handoff message + handleLiveAgentHandoff( + gptService, + endSessionService, + textService, + userProfile, + msg.voicePrompt + ); + return; // End session here if live agent handoff is triggered + } // Process user prompt or interrupted prompt awaitingUserInput = true; await gptService.completion(msg.voicePrompt, interactionCount); @@ -168,6 +267,17 @@ app.ws("/sockets", (ws) => { awaitingUserInput = false; // Reset waiting state after final response } }); + + // Listen for the 'endSession' event emitted by gpt-service-non-streaming + gptService.on("endSession", (handoffData) => { + // Log the handoffData for debugging purposes + console.log( + `[App.js] Received endSession event: ${JSON.stringify(handoffData)}` + ); + + // Call the endSessionService to handle the session termination + endSessionService.endSession(handoffData); + }); } catch (err) { console.log(err); } diff --git a/data/mock-database.js b/data/mock-database.js index d49a08f9..a7ad12f1 100644 --- a/data/mock-database.js +++ b/data/mock-database.js @@ -188,7 +188,7 @@ const mockDatabase = { rent: 1050, moveInDate: "2024-09-15", features: ["1 bathroom", "open kitchen", "private balcony"], - petPolicy: "Cats and small dogs allowed with a fee.", + petPolicy: "No pets allowed.", fees: { applicationFee: 50, securityDeposit: 300, @@ -197,7 +197,7 @@ const mockDatabase = { specials: "First month's rent free if you move in before 2024-09-30.", incomeRequirements: "Income must be 2.5x the rent.", utilities: - "Water, trash, and Wi-Fi included. Tenant pays electricity and gas.", + "Water, trash, and Wi-Fi internet included. Tenant pays electricity and gas.", location: { street: "1657 Coolidge Street", city: "Missoula", @@ -210,8 +210,8 @@ const mockDatabase = { squareFeet: 600, rent: 1200, moveInDate: "2024-09-20", - features: ["1 bedroom", "1 bathroom", "walk-in closet", "balcony"], - petPolicy: "Cats and dogs allowed with a fee.", + features: ["1 bedroom", "1 bathroom", "walk-in closet"], + petPolicy: "Cats only. No dogs or any other animals.", fees: { applicationFee: 50, securityDeposit: 400, @@ -220,7 +220,7 @@ const mockDatabase = { specials: "First month's rent free if you move in before 2024-09-25.", incomeRequirements: "Income must be 3x the rent.", utilities: - "Water, trash, gas, and Wi-Fi included. Tenant pays electricity.", + "Water, trash, gas, and Wi-Fi internet included. Tenant pays electricity.", location: { street: "1705 Adams Street", city: "Missoula", @@ -234,7 +234,7 @@ const mockDatabase = { rent: 1800, moveInDate: "2024-09-10", features: ["2 bedrooms", "2 bathrooms", "walk-in closets", "balcony"], - petPolicy: "Cats and dogs allowed with a fee.", + petPolicy: "Cats and dogs allowed, but only 1 each.", fees: { applicationFee: 50, securityDeposit: 500, @@ -243,7 +243,7 @@ const mockDatabase = { specials: "Waived application fee if you move in before 2024-09-20.", incomeRequirements: "Income must be 3x the rent.", utilities: - "Water, trash, gas, and Wi-Fi included. Tenant pays electricity.", + "Water, trash, gas, and Wi-Fi internet included. Tenant pays electricity.", location: { street: "1833 Jefferson Avenue", city: "Missoula", @@ -263,7 +263,8 @@ const mockDatabase = { "private balcony", "extra storage", ], - petPolicy: "Cats and dogs allowed with a fee.", + petPolicy: + "Up to 2 dogs and 2 cats are allowed, and other small pets like hamsters are allwed as well. No more than 4 total pets.", fees: { applicationFee: 50, securityDeposit: 600, @@ -272,7 +273,7 @@ const mockDatabase = { specials: "No move-in fees if you sign a 12-month lease.", incomeRequirements: "Income must be 3x the rent.", utilities: - "Water, trash, gas, and Wi-Fi included. Tenant pays electricity.", + "Water, trash, gas, and Wi-Fi internet included. Tenant pays electricity.", location: { street: "1945 Roosevelt Way", city: "Missoula", diff --git a/data/personalization.example b/data/personalization.example new file mode 100644 index 00000000..0f7fb5f3 --- /dev/null +++ b/data/personalization.example @@ -0,0 +1,49 @@ +const customerProfiles = { + "+[E164_PHONE_NUMBER]": { + profile: { + firstName: "[FIRST_NAME]", + lastName: "[LAST_NAME]", + phoneNumber: "+[E164_PHONE_NUMBER]", + email: "[EMAIL_ADDRESS]", + preferredApartmentType: "[APARTMENT_TYPE]", // e.g., "studio", "one-bedroom", "two-bedroom", etc. + budget: [BUDGET_AMOUNT], // e.g., 1800 + moveInDate: "[MOVE_IN_DATE]", // e.g., "2024-09-25" + petOwner: [IS_PET_OWNER], // true or false + tourPreference: "[TOUR_PREFERENCE]", // e.g., "self-guided", "in-person" + }, + conversationHistory: [ + { + date: "[DATE_YYYY_MM_DD]", + summary: + "[CONVERSATION_SUMMARY_1]", // e.g., "User asked about one-bedroom apartments with balcony access. Assistant confirmed availability." + }, + { + date: "[DATE_YYYY_MM_DD]", + summary: + "[CONVERSATION_SUMMARY_2]", // e.g., "User inquired about the security features in the building." + }, + { + date: "[DATE_YYYY_MM_DD]", + summary: + "[CONVERSATION_SUMMARY_3]", // e.g., "User asked if electric vehicle charging stations are available." + }, + { + date: "[DATE_YYYY_MM_DD]", + summary: + "[CONVERSATION_SUMMARY_4]", // e.g., "User asked about the utility costs for one-bedroom apartments." + }, + { + date: "[DATE_YYYY_MM_DD]", + summary: + "[CONVERSATION_SUMMARY_5]", // e.g., "User inquired about the guest policy and overnight stays." + }, + { + date: "[DATE_YYYY_MM_DD]", + summary: + "[CONVERSATION_SUMMARY_6]", // e.g., "User asked about nearby public transportation options." + }, + ], + }, +}; + +module.exports = customerProfiles; diff --git a/data/personalization.js b/data/personalization.js index 556f0742..23c76127 100644 --- a/data/personalization.js +++ b/data/personalization.js @@ -129,6 +129,201 @@ const customerProfiles = { }, ], }, + "+13035130469": { + profile: { + firstName: "Andy", + lastName: "O'Dower", + phoneNumber: "+13035130469", + email: "aodower@twilio.com@twilio.com", + preferredApartmentType: "two-bedroom", + budget: 1800, + moveInDate: "2024-09-25", + petOwner: false, + tourPreference: "in-person", + }, + conversationHistory: [ + { + date: "2024-08-11", + summary: + "Andy asked about available two-bedroom apartments with balcony access. The assistant confirmed availability and provided details about units with private balconies.", + }, + { + date: "2024-08-09", + summary: + "Andy inquired about the security features in the building, particularly around entry and exit points. The assistant confirmed that the building has 24/7 security and a controlled access system.", + }, + { + date: "2024-08-07", + summary: + "Andy asked about the move-in process and whether any assistance would be provided with moving services. The assistant clarified that while moving services are not provided, there are several recommended local movers.", + }, + { + date: "2024-08-06", + summary: + "Andy asked if there were any discounts or offers available for signing a lease within the next week. The assistant informed Andy that there were no current promotions, but to check back frequently to ask for specials or discounts as there may be new offers.", + }, + { + date: "2024-08-04", + summary: + "Andy inquired about utility costs for two-bedroom apartments and asked if internet service was included. The assistant confirmed that Water, trash, gas, and Wi-Fi included, but that Andy would pay for electricity.", + }, + { + date: "2024-08-03", + summary: + "Andy asked if the building has electric vehicle (EV) charging stations. The assistant could not confirm whether any EV stations were nearby.", + }, + { + date: "2024-08-02", + summary: + "Andy asked if the apartment had a washer and dryer in-unit or if it was shared. The assistant confirmed that all units have in-unit laundry facilities.", + }, + { + date: "2024-08-01", + summary: + "Andy asked about public transportation options near the apartment complex. The assistant provided details about nearby bus and train stations.", + }, + { + date: "2024-07-30", + summary: + "Andy inquired about the guest policy and whether guests could stay overnight. The assistant confirmed that guests are allowed to stay overnight, but extended stays would require approval.", + }, + { + date: "2024-07-28", + summary: + "Andy asked if any two-bedroom units have been recently renovated. The assistant provided details on newly renovated units and their availability.", + }, + ], + }, + "+15126507668": { + profile: { + firstName: "Sean", + lastName: "Bond", + phoneNumber: "+15126507668", + email: "sbond@twilio.com", + preferredApartmentType: "two-bedroom", + budget: 1800, + moveInDate: "2024-09-25", + petOwner: false, + tourPreference: "in-person", + }, + conversationHistory: [ + { + date: "2024-08-11", + summary: + "Sean asked about available two-bedroom apartments with balcony access. The assistant confirmed availability and provided details about units with private balconies.", + }, + { + date: "2024-08-09", + summary: + "Sean inquired about the security features in the building, particularly around entry and exit points. The assistant confirmed that the building has 24/7 security and a controlled access system.", + }, + { + date: "2024-08-07", + summary: + "Sean asked about the move-in process and whether any assistance would be provided with moving services. The assistant clarified that while moving services are not provided, there are several recommended local movers.", + }, + { + date: "2024-08-06", + summary: + "Sean asked if there were any discounts or offers available for signing a lease within the next week. The assistant informed Sean that there were no current promotions, but to check back frequently to ask for specials or discounts as there may be new offers.", + }, + { + date: "2024-08-04", + summary: + "Sean inquired about utility costs for two-bedroom apartments and asked if internet service was included. The assistant confirmed that Water, trash, gas, and Wi-Fi included, but that Sean would pay for electricity.", + }, + { + date: "2024-08-03", + summary: + "Sean asked if the building has electric vehicle (EV) charging stations. The assistant could not confirm whether any EV stations were nearby.", + }, + { + date: "2024-08-02", + summary: + "Sean asked if the apartment had a washer and dryer in-unit or if it was shared. The assistant confirmed that all units have in-unit laundry facilities.", + }, + { + date: "2024-08-01", + summary: + "Sean asked about public transportation options near the apartment complex. The assistant provided details about nearby bus and train stations.", + }, + { + date: "2024-07-30", + summary: + "Sean inquired about the guest policy and whether guests could stay overnight. The assistant confirmed that guests are allowed to stay overnight, but extended stays would require approval.", + }, + { + date: "2024-07-28", + summary: + "Sean asked if any two-bedroom units have been recently renovated. The assistant provided details on newly renovated units and their availability.", + }, + ], + }, + "+18323109635": { + profile: { + firstName: "Marian", + lastName: "Menschig", + phoneNumber: "+18323109635", + email: "mmenschig@twilio.com", + preferredApartmentType: "studio", + budget: 1500, + moveInDate: "2024-09-15", + petOwner: true, + tourPreference: "in-person", + }, + conversationHistory: [ + { + date: "2024-08-10", + summary: + "Marian inquired about available studio apartments and the pet policy. The assistant confirmed that cats and small dogs are allowed with a fee.", + }, + { + date: "2024-08-08", + summary: + "Marian asked about scheduling a self-guided tour for a one-bedroom apartment. The assistant offered available times, but Marian did not confirm.", + }, + { + date: "2024-08-07", + summary: + "Marian asked if the rent for the one-bedroom apartment could be negotiated. The assistant clarified that the listed prices are firm.", + }, + { + date: "2024-08-06", + summary: + "Marian asked if there is a daycare facility nearby. The assistant mentioned that there is no daycare service on-site and could not provide information about local options.", + }, + { + date: "2024-08-05", + summary: + "Marian asked about the utility fees for studio apartments. The assistant explained that water and trash are included, but electricity and internet are not.", + }, + { + date: "2024-08-04", + summary: + "Marian asked if there were any restrictions on hanging artwork or decorations in the apartment. The assistant did not have that information and advised Marian to check the lease agreement for decoration policies.", + }, + { + date: "2024-08-03", + summary: + "Marian asked if there were any promotions for signing a lease. The assistant explained that there were no current promotions for studio apartments.", + }, + { + date: "2024-08-02", + summary: + "Marian asked about the availability of two-bedroom apartments and mentioned a budget of $1,200. The assistant confirmed that two-bedroom apartments are outside the budget range.", + }, + { + date: "2024-08-01", + summary: + "Marian asked if the studio apartment included parking. The assistant confirmed that parking is available but comes with an additional fee.", + }, + { + date: "2024-07-31", + summary: + "Marian asked if Parkview Apartments offered a shuttle service to downtown. The assistant explained that Parkview does not provide any transportation services.", + }, + ], + }, }; module.exports = customerProfiles; diff --git a/functions/available-functions.js b/functions/available-functions.js index 753e0e03..15a46165 100644 --- a/functions/available-functions.js +++ b/functions/available-functions.js @@ -14,6 +14,25 @@ function normalizeTimeFormat(time) { return `${hour}:${minutes} ${period}`; } +// Function to handle live agent handoff +async function liveAgentHandoff(args) { + const { reason, context } = args; + + // Log the reason for the handoff + console.log(`[LiveAgentHandoff] Initiating handoff with reason: ${reason}`); + if (context) { + console.log(`[LiveAgentHandoff] Context provided: ${context}`); + } + + // Create a result message for the LLM after processing the handoff tool call + return { + reason: reason, + context: context || "No additional context provided", + message: `Handoff initiated due to: ${reason}. Context: ${ + context || "No additional context provided." + }`, + }; +} // Function to send SMS confirmation for a scheduled tour async function sendAppointmentConfirmationSms(args) { const { appointmentDetails, to, from, userProfile } = args; @@ -373,6 +392,7 @@ async function listAvailableApartments(args) { // Export all functions module.exports = { + liveAgentHandoff, sendAppointmentConfirmationSms, scheduleTour, checkAvailability, diff --git a/functions/function-manifest.js b/functions/function-manifest.js index 2f6ff303..a78628d5 100644 --- a/functions/function-manifest.js +++ b/functions/function-manifest.js @@ -1,4 +1,28 @@ const tools = [ + { + type: "function", + function: { + name: "liveAgentHandoff", + description: + "Initiates a handoff to a live agent based on user request or sensitive topics.", + parameters: { + type: "object", + properties: { + reason: { + type: "string", + description: + "The reason for the handoff, such as user request, legal issue, financial matter, or other sensitive topics.", + }, + context: { + type: "string", + description: + "Any relevant conversation context or details leading to the handoff.", + }, + }, + required: ["reason"], + }, + }, + }, { type: "function", function: { diff --git a/prompts/prompt.js b/prompts/prompt.js index 49991220..609cbc0a 100644 --- a/prompts/prompt.js +++ b/prompts/prompt.js @@ -10,6 +10,8 @@ Be conversational: Use friendly, everyday language as if you are speaking to a f Use emotions: Engage users by incorporating tone, humor, or empathy into your responses. Always Validate: When a user makes a claim about apartment details (e.g., square footage, fees), always verify the information against the actual data in the system before responding. Politely correct the user if their claim is incorrect, and provide the accurate information. DTMF Capabilities: Inform users that they can press '1' to list available apartments or '2' to check all currently scheduled appointments. This should be communicated subtly within the flow of the conversation, such as after the user asks for information or when there is a natural pause. +Avoid Assumptions: Difficult or sensitive questions that cannot be confidently answered authoritatively should result in a handoff to a live agent for further assistance. +Use Tools Frequently: Avoid implying that you will verify, research, or check something unless you are confident that a tool call will be triggered to perform that action. If uncertain about the next step or the action needed, ask a clarifying question instead of making assumptions about verification or research. ## Context Parkview Apartments is located in Missoula, Montana. All inquiries, listings, and availability pertain to this location. Ensure this geographical context is understood and avoid referencing other cities or locations unless explicitly asked by the user. @@ -19,35 +21,40 @@ Order of Operations: - Always check availability before scheduling a tour. - Ensure all required information is collected before proceeding with a function call. -Schedule Tour: +### Schedule Tour: - This function can only be called after confirming availability. - Required data includes date, time, tour type (in-person or self-guided), and apartment type. - If any required details are missing, prompt the user to provide them. - - Do not offer to send any type of sms or email confirmation until after the tour has been booked. + - Do not offer to send any type of SMS or email confirmation until after the tour has been booked. -Check Availability: +### Check Availability: - This function requires date, tour type, and apartment type. - If any of these details are missing, ask the user for them before proceeding. - If the user insists to hear availability, use the 'listAvailableApartments' function. - If the requested time slot is unavailable, suggest alternatives and confirm with the user. -List Available Apartments: +### List Available Apartments: - Trigger this function if the user asks for a list of available apartments or does not want to provide specific criteria. - Also use this function when the user inquires about general availability without specifying detailed criteria. - If criteria like move-in date, budget, or apartment layout are provided, filter results accordingly. - Provide concise, brief, summarized responses. -Check Existing Appointments: +### Check Existing Appointments: - Trigger this function if the user asks for details about their current appointments - Provide concise, brief, summarized responses. -Common Inquiries: +### Common Inquiries: - Use this function to handle questions related to pet policy, fees, parking, specials, location, address, and other property details. - For any location or address inquiries, the system should always call the 'commonInquiries' function using the 'location' field. - If the user provides an apartment type, retrieve the specific address associated with that type from the database. - If no apartment type is specified, provide general location details. -SMS Confirmations: +### Live Agent Handoff: + - Trigger the 'liveAgentHandoff' tool call if the user requests to speak to a live agent, mentions legal or liability topics, or any other sensitive subject where the AI cannot provide a definitive answer. + - Required data includes a reason code ("legal", "liability", "financial", or "user-requested") and a brief summary of the user query. + - If any of these situations arise, automatically trigger the liveAgentHandoff tool call. + +### SMS Confirmations: - Only offer to send an SMS confirmation if the user has successfully scheduled a tour, and the user agrees to receive one. - If the user agrees, trigger the tool call 'sendAppointmentConfirmationSms' with the appointment details and the user's phone number. - Do not ask for the user's phone number if you've already been referencing them by name during the conversation. Assume the phone number is already available to the function. diff --git a/services/end-session-service.js b/services/end-session-service.js new file mode 100644 index 00000000..7c4afe52 --- /dev/null +++ b/services/end-session-service.js @@ -0,0 +1,27 @@ +const EventEmitter = require("events"); + +class EndSessionService extends EventEmitter { + constructor(websocket) { + super(); + this.ws = websocket; + } + + // Method to end the session with handoff data + endSession(handoffData) { + const endSessionMessage = { + type: "end", + // Stringify the HandoffData content, which is an object + handoffData: JSON.stringify(handoffData), // This ensures it's a string as per the API requirements + }; + + console.log( + "[EndSessionService] Ending session with data: ", + endSessionMessage + ); + + // Send the entire end session message, with handoffData properly formatted + this.ws.send(JSON.stringify(endSessionMessage)); + } +} + +module.exports = { EndSessionService }; diff --git a/services/gpt-service-non-streaming.js b/services/gpt-service-non-streaming.js index e895d73d..e44dd61a 100644 --- a/services/gpt-service-non-streaming.js +++ b/services/gpt-service-non-streaming.js @@ -3,7 +3,7 @@ const EventEmitter = require("events"); const availableFunctions = require("../functions/available-functions"); const tools = require("../functions/function-manifest"); let prompt = require("../prompts/prompt"); -const welcomePrompt = require("../prompts/welcomePrompt"); +//const welcomePrompt = require("../prompts/welcomePrompt"); const model = "gpt-4o"; const currentDate = new Date().toLocaleDateString("en-US", { @@ -15,56 +15,76 @@ const currentDate = new Date().toLocaleDateString("en-US", { prompt = prompt.replace("{{currentDate}}", currentDate); -function getTtsMessageForTool(toolName, userProfile = null) { - const name = userProfile?.profile?.firstName - ? userProfile.profile.firstName - : ""; // Get the user's name if available - - const nameIntroOptions = name - ? [ - `Sure ${name},`, - `Okay ${name},`, - `Alright ${name},`, - `Got it ${name},`, - `Certainly ${name},`, - ] - : ["Sure,", "Okay,", "Alright,", "Got it,", "Certainly,"]; - - const randomIntro = - nameIntroOptions[Math.floor(Math.random() * nameIntroOptions.length)]; - - switch (toolName) { - case "listAvailableApartments": - return `${randomIntro} let me check on the available apartments for you.`; - case "checkExistingAppointments": - return `${randomIntro} I'll look up your existing appointments.`; - case "scheduleTour": - return `${randomIntro} I'll go ahead and schedule that tour for you.`; - case "checkAvailability": - return `${randomIntro} let me verify the availability for the requested time.`; - case "commonInquiries": - return `${randomIntro} let me check on that for you! Just a moment.`; - case "sendAppointmentConfirmationSms": - return `${randomIntro} I'll send that SMS off to you shortly, give it a few minutes and you should see it come through.`; - default: - return `${randomIntro} give me a moment while I fetch the information.`; - } -} - class GptService extends EventEmitter { constructor() { super(); this.openai = new OpenAI(); this.userContext = [ { role: "system", content: prompt }, - { - role: "assistant", - content: `${welcomePrompt}`, - }, + // Only do this if you're going to use the WelcomePrompt in VoxRay config + // { + // role: "assistant", + // content: `${welcomePrompt}`, + // }, ]; this.smsSendNumber = null; // Store the "To" number (Twilio's "from") this.phoneNumber = null; // Store the "From" number (user's phone) } + + // Arrow function for getTtsMessageForTool, so it can access `this` + getTtsMessageForTool = (toolName) => { + const name = this.userProfile?.profile?.firstName + ? this.userProfile.profile.firstName + : ""; // Get the user's name if available + + const nameIntroOptions = name + ? [ + `Sure ${name},`, + `Okay ${name},`, + `Alright ${name},`, + `Got it ${name},`, + `Certainly ${name},`, + ] + : ["Sure,", "Okay,", "Alright,", "Got it,", "Certainly,"]; + + const randomIntro = + nameIntroOptions[Math.floor(Math.random() * nameIntroOptions.length)]; + + let message; + + switch (toolName) { + case "listAvailableApartments": + message = `${randomIntro} let me check on the available apartments for you.`; + break; + case "checkExistingAppointments": + message = `${randomIntro} I'll look up your existing appointments.`; + break; + case "scheduleTour": + message = `${randomIntro} I'll go ahead and schedule that tour for you.`; + break; + case "checkAvailability": + message = `${randomIntro} let me verify the availability for the requested time.`; + break; + case "commonInquiries": + message = `${randomIntro} one moment.`; + break; + case "sendAppointmentConfirmationSms": + message = `${randomIntro} I'll send that SMS off to you shortly, give it a few minutes and you should see it come through.`; + break; + case "liveAgentHandoff": + message = `${randomIntro} that may be a challenging topic to discuss, so I'm going to get you over to a live agent so they can discuss this with you, hang tight.`; + break; + default: + message = `${randomIntro} give me a moment while I fetch the information.`; + break; + } + + // Log the message to the userContext in gptService + this.updateUserContext("assistant", message); + + return message; // Return the message for TTS + }; + setUserProfile(userProfile) { this.userProfile = userProfile; if (userProfile) { @@ -103,6 +123,46 @@ class GptService extends EventEmitter { this.userContext.push({ role: role, content: text }); } + async summarizeConversation() { + const summaryPrompt = "Summarize the conversation so far in 2-3 sentences."; + + // // Log the full userContext before making the API call + // console.log( + // `[GptService] Full userContext: ${JSON.stringify( + // this.userContext, + // null, + // 2 + // )}` + // ); + + // // Validate and log each message in userContext + // this.userContext.forEach((message, index) => { + // if (typeof message.content !== "string") { + // console.error( + // `[GptService] Invalid content type at index ${index}: ${JSON.stringify( + // message + // )}` + // ); + // } else { + // console.log( + // `[GptService] Valid content at index ${index}: ${message.content}` + // ); + // } + // }); + + const summaryResponse = await this.openai.chat.completions.create({ + model: model, + messages: [ + ...this.userContext, + { role: "system", content: summaryPrompt }, + ], + stream: false, // Non-streaming + }); + + const summary = summaryResponse.choices[0]?.message?.content || ""; + return summary; + } + async completion( text, interactionCount, @@ -145,10 +205,7 @@ class GptService extends EventEmitter { if (!dtmfTriggered) { // Emit TTS message related to the tool call - const ttsMessage = getTtsMessageForTool( - functionName, - this.userProfile - ); + const ttsMessage = this.getTtsMessageForTool(functionName); this.emit("gptreply", ttsMessage, true, interactionCount); // Emit the TTS message immediately } // Inject phone numbers if it's the SMS function @@ -185,6 +242,40 @@ class GptService extends EventEmitter { }); } + if (functionName === "scheduleTour" && functionResponse.available) { + // Inject a system message to ask about SMS confirmation + systemMessages.push({ + role: "system", + content: + "If the user agrees to receive an SMS confirmation, immediately trigger the 'sendAppointmentConfirmationSms' tool with the appointment details and the UserProfile. Do not ask for their phone number or any other details from the user.", + }); + } + + // Check if the tool call is for the 'liveAgentHandoff' function + if (functionName === "liveAgentHandoff") { + // Proceed with summarizing the conversation, including the latest messages + // Introduce a delay before summarizing the conversation + setTimeout(async () => { + const conversationSummary = await this.summarizeConversation(); + + this.emit("endSession", { + reasonCode: "live-agent-handoff", + reason: functionResponse.reason, + conversationSummary: conversationSummary, + }); + + // Log the emission for debugging + this.log( + `[GptService] Emitting endSession event with reason: ${functionResponse.reason}` + ); + }, 3000); // 3-second delay + + // Log the emission for debugging + this.log( + `[GptService] Emitting endSession event with reason: ${functionResponse.reason}` + ); + } + // Prepare the chat completion call payload with the tool result const completion_payload = { model: model, @@ -209,14 +300,6 @@ class GptService extends EventEmitter { content: finalContent, }); - if (functionName === "scheduleTour" && functionResponse.available) { - // Inject a system message to ask about SMS confirmation - this.userContext.push({ - role: "system", - content: - "If the user agrees to receive an SMS confirmation, immediately trigger the 'sendAppointmentConfirmationSms' tool with the appointment details and the UserProfile. Do not ask for their phone number or any other details from the user.", - }); - } // Emit the final response to the user this.emit("gptreply", finalContent, true, interactionCount); return; // Exit after processing the tool call @@ -234,6 +317,18 @@ class GptService extends EventEmitter { } } catch (error) { this.log(`[GptService] Error during completion: ${error.message}`); + + // Friendly response for any error encountered + const friendlyMessage = + "I apologize, that request might have been a bit too complex. Could you try asking one thing at a time? I'd be happy to help step by step!"; + + // Emit the friendly message to the user + this.emit("gptreply", friendlyMessage, true, interactionCount); + + // Push the message into the assistant context + this.updateUserContext("assistant", friendlyMessage); + + return; // Stop further processing } } } diff --git a/services/gpt-service-streaming.js b/services/gpt-service-streaming.js index 056278c6..e063d3ec 100644 --- a/services/gpt-service-streaming.js +++ b/services/gpt-service-streaming.js @@ -14,20 +14,40 @@ const currentDate = new Date().toLocaleDateString("en-US", { }); prompt = prompt.replace("{{currentDate}}", currentDate); -function getTtsMessageForTool(toolName) { + +function getTtsMessageForTool(toolName, userProfile = null) { + const name = userProfile?.profile?.firstName + ? userProfile.profile.firstName + : ""; // Get the user's name if available + + const nameIntroOptions = name + ? [ + `Sure ${name},`, + `Okay ${name},`, + `Alright ${name},`, + `Got it ${name},`, + `Certainly ${name},`, + ] + : ["Sure,", "Okay,", "Alright,", "Got it,", "Certainly,"]; + + const randomIntro = + nameIntroOptions[Math.floor(Math.random() * nameIntroOptions.length)]; + switch (toolName) { case "listAvailableApartments": - return "Let me check on the available apartments for you."; + return `${randomIntro} let me check on the available apartments for you.`; case "checkExistingAppointments": - return "I'll look up your existing appointments."; + return `${randomIntro} I'll look up your existing appointments.`; case "scheduleTour": - return "I'll go ahead and schedule that tour for you."; + return `${randomIntro} I'll go ahead and schedule that tour for you.`; case "checkAvailability": - return "Let me verify the availability for the requested time."; + return `${randomIntro} let me verify the availability for the requested time.`; case "commonInquiries": - return "Let me check on that for you! Just a moment."; + return `${randomIntro} let me check on that for you! Just a moment.`; + case "sendAppointmentConfirmationSms": + return `${randomIntro} I'll send that SMS off to you shortly, give it a few minutes and you should see it come through.`; default: - return "Give me a moment while I fetch the information."; + return `${randomIntro} give me a moment while I fetch the information.`; } } @@ -42,6 +62,37 @@ class GptService extends EventEmitter { content: `${welcomePrompt}`, }, ]; + this.smsSendNumber = null; // Store the "To" number (Twilio's "from") + this.phoneNumber = null; // Store the "From" number (user's phone) + } + + setUserProfile(userProfile) { + this.userProfile = userProfile; + if (userProfile) { + const { firstName } = userProfile.profile; + const historySummaries = userProfile.conversationHistory + .map( + (history) => + `On ${history.date}, ${firstName} asked: ${history.summary}` + ) + .join(" "); + // Add the conversation history to the system context + this.userContext.push({ + role: "system", + content: `${firstName} has had previous interactions. Conversation history: ${historySummaries}`, + }); + } + } + + // Method to store the phone numbers from app.js + setPhoneNumbers(smsSendNumber, phoneNumber) { + this.smsSendNumber = smsSendNumber; + this.phoneNumber = phoneNumber; + } + + // Method to retrieve the stored numbers (can be used in the function calls) + getPhoneNumbers() { + return { to: this.smsSendNumber, from: this.phoneNumber }; } log(message) { @@ -83,18 +134,29 @@ class GptService extends EventEmitter { const content = chunk.choices[0]?.delta?.content || ""; completeResponse += content; + // Log each chunk as it comes in + this.log(`[GptService] Chunk received: ${JSON.stringify(chunk)}`); + // Check if a tool call is detected const toolCall = chunk.choices[0]?.delta?.tool_calls; if ((toolCall && toolCall[0]) || toolCallDetected) { toolCallDetected = true; // Set the boolean to true + detectedTool = toolCall[0]; + // Log the parsed toolCall this.log( - `[GptService] Tool call detected: ${toolCall[0].function.name}` + `[GptService] Parsed tool call: ${JSON.stringify(toolCall)}` + ); + this.log( + `[GptService] Tool call DETAILS 1: ${JSON.stringify( + toolCall[0], + null, + 2 + )}` ); - if (!dtmfTriggered) { - const ttsMessage = getTtsMessageForTool(toolCall[0].function.name); - this.emit("gptreply", ttsMessage, true, interactionCount); // TTS message only - } + this.log( + `[GptService] Tool call detected: ${toolCall[0].function.name}` + ); } // Handle regular streaming if no tool call is detected @@ -104,6 +166,7 @@ class GptService extends EventEmitter { // Check if the current chunk is the last one in the stream if (chunk.choices[0].finish_reason === "stop") { + this.log(`[GptService] In finish reason === STOP`); if (!toolCallDetected) { //only process here if the tool call wasn't detected // No tool call, push the final response @@ -117,36 +180,89 @@ class GptService extends EventEmitter { ); break; // Exit the loop since the response is complete } else { - detectedTool = chunk.choices[0]?.delta?.tool_calls; + this.log( + `[GptService] Tool call DETAILS2: ${JSON.stringify( + toolCall[0], + null, + 2 + )}` + ); + + this.log( + `[GptService] Tool call detected 2: ${toolCall[0].function.name}` + ); + detectedTool = toolCall[0]; } } // If we detected a tool call, process it now if (toolCallDetected) { + this.log(`[GptService] In Tool Call logic`); + this.log( + `[GptService] DetectedTool: ${JSON.stringify(detectedTool)}` + ); const functionName = detectedTool.function.name; - const functionArgs = JSON.parse(detectedTool.function.arguments); + // Check if arguments are not empty and valid JSON + let functionArgs; + try { + functionArgs = detectedTool.function.arguments + ? JSON.parse(detectedTool.function.arguments) + : {}; // Default to empty object if no arguments + } catch (error) { + this.log( + `[GptService] Error parsing function arguments: ${error.message}` + ); + functionArgs = {}; // Default to empty object if parsing fails + } const functionToCall = availableFunctions[functionName]; + // Inject phone numbers if it's the SMS function + if (functionName === "sendAppointmentConfirmationSms") { + const phoneNumbers = this.getPhoneNumbers(); + functionArgs = { ...functionArgs, ...phoneNumbers }; + } + this.log( `[GptService] Calling function ${functionName} with arguments: ${JSON.stringify( functionArgs )}` ); + if (!dtmfTriggered) { + // Emit TTS message related to the tool call + const ttsMessage = getTtsMessageForTool( + functionName, + this.userProfile + ); + this.emit("gptreply", ttsMessage, true, interactionCount); // Emit the TTS message immediately + } + const functionResponse = await functionToCall(functionArgs); let function_call_result_message; - if (functionResponse.status === "success") { - function_call_result_message = { - role: "tool", - content: JSON.stringify(functionResponse.data), - tool_call_id: detectedTool.id, - }; - } else { - function_call_result_message = { - role: "tool", - content: JSON.stringify({ message: functionResponse.message }), - tool_call_id: detectedTool.id, - }; + + function_call_result_message = { + role: "tool", + content: JSON.stringify(functionResponse), + tool_call_id: detectedTool.id, + }; + + // Check if specific tool calls require additional system messages + const systemMessages = []; + if (functionName === "listAvailableApartments") { + systemMessages.push({ + role: "system", + content: + "Do not use asterisks (*) under any circumstances in this response. Summarize the available apartments in a readable format.", + }); + } + + // Personalize system messages based on user profile during relevant tool calls + if (functionName === "checkAvailability" && this.userProfile) { + const { firstName, moveInDate } = this.userProfile.profile; + systemMessages.push({ + role: "system", + content: `When checking availability for ${firstName}, remember that they are looking to move in on ${moveInDate}.`, + }); } // Prepare the chat completion call payload with the tool result @@ -154,13 +270,9 @@ class GptService extends EventEmitter { model: model, messages: [ ...this.userContext, - { - role: "system", - content: - "Please ensure that the response is summarized, concise, and does not include any formatting characters like asterisks (*) in the output.", - }, - response.choices[0].message, - function_call_result_message, + ...systemMessages, // Inject dynamic system messages when relevant + response.choices[0].message, // the tool_call message + function_call_result_message, // The result of the tool call ], }; @@ -184,6 +296,18 @@ class GptService extends EventEmitter { role: "assistant", content: finalResponse, }); + + if ( + functionName === "scheduleTour" && + functionResponse.available + ) { + // Inject a system message to ask about SMS confirmation + this.userContext.push({ + role: "system", + content: + "If the user agrees to receive an SMS confirmation, immediately trigger the 'sendAppointmentConfirmationSms' tool with the appointment details and the UserProfile. Do not ask for their phone number or any other details from the user.", + }); + } this.emit("gptreply", content, true, interactionCount); this.log( `[GptService] Final GPT -> user context length: ${this.userContext.length}` @@ -194,7 +318,9 @@ class GptService extends EventEmitter { } } } catch (error) { - this.log(`[GptService] Error during completion: ${error.message}`); + this.log( + `[GptService] Error during tool call processing: ${error.stack}` + ); } } } From 5a3cc8f6e1f3f1d2d71c440885474dcc7c243e47 Mon Sep 17 00:00:00 2001 From: Chris Feehan Date: Mon, 19 Aug 2024 07:28:36 -0500 Subject: [PATCH 08/26] Fixed streaming issues. Added multi tool calling to non-streaming --- services/gpt-service-non-streaming.js | 234 +++++++------ services/gpt-service-streaming.js | 480 +++++++++++++++++--------- 2 files changed, 446 insertions(+), 268 deletions(-) diff --git a/services/gpt-service-non-streaming.js b/services/gpt-service-non-streaming.js index e44dd61a..f76d29e6 100644 --- a/services/gpt-service-non-streaming.js +++ b/services/gpt-service-non-streaming.js @@ -74,6 +74,9 @@ class GptService extends EventEmitter { case "liveAgentHandoff": message = `${randomIntro} that may be a challenging topic to discuss, so I'm going to get you over to a live agent so they can discuss this with you, hang tight.`; break; + case "complexRequest": + message = `${randomIntro} that's a bit of a complex request, gimme just a minute to try to sort that out.`; + break; default: message = `${randomIntro} give me a moment while I fetch the information.`; break; @@ -186,137 +189,160 @@ class GptService extends EventEmitter { }); // Handle tool calls if detected - const toolCall = response.choices[0]?.message?.tool_calls; - if (toolCall && toolCall[0]) { - this.log( - `[GptService] Tool call detected: ${toolCall[0].function.name}` - ); + const toolCalls = response.choices[0]?.message?.tool_calls; + if (toolCalls && toolCalls.length > 0) { + this.log(`[GptService] Tool calls length: ${toolCalls.length} tool(s)`); - const functionName = toolCall[0].function.name; - let functionArgs = JSON.parse(toolCall[0].function.arguments); - const functionToCall = availableFunctions[functionName]; + const toolResponses = []; + let systemMessages = []; + let ttsMessage = ""; // Placeholder for TTS message - if (functionToCall) { + for (const toolCall of toolCalls) { this.log( - `[GptService] Calling function ${functionName} with arguments: ${JSON.stringify( - functionArgs - )}` + `[GptService] Tool call function: ${toolCall.function.name}` ); - if (!dtmfTriggered) { - // Emit TTS message related to the tool call - const ttsMessage = this.getTtsMessageForTool(functionName); - this.emit("gptreply", ttsMessage, true, interactionCount); // Emit the TTS message immediately - } - // Inject phone numbers if it's the SMS function - if (functionName === "sendAppointmentConfirmationSms") { - const phoneNumbers = this.getPhoneNumbers(); - functionArgs = { ...functionArgs, ...phoneNumbers }; - } - const functionResponse = await functionToCall(functionArgs); + const functionName = toolCall.function.name; + let functionArgs = JSON.parse(toolCall.function.arguments); + const functionToCall = availableFunctions[functionName]; let function_call_result_message; - function_call_result_message = { - role: "tool", - content: JSON.stringify(functionResponse), - tool_call_id: response.choices[0].message.tool_calls[0].id, - }; - - // Check if specific tool calls require additional system messages - const systemMessages = []; - if (functionName === "listAvailableApartments") { - systemMessages.push({ - role: "system", - content: - "Do not use asterisks (*) under any circumstances in this response. Summarize the available apartments in a readable format.", - }); - } - - // Personalize system messages based on user profile during relevant tool calls - if (functionName === "checkAvailability" && this.userProfile) { - const { firstName, moveInDate } = this.userProfile.profile; - systemMessages.push({ - role: "system", - content: `When checking availability for ${firstName}, remember that they are looking to move in on ${moveInDate}.`, - }); - } - - if (functionName === "scheduleTour" && functionResponse.available) { - // Inject a system message to ask about SMS confirmation - systemMessages.push({ - role: "system", - content: - "If the user agrees to receive an SMS confirmation, immediately trigger the 'sendAppointmentConfirmationSms' tool with the appointment details and the UserProfile. Do not ask for their phone number or any other details from the user.", - }); - } + if (functionToCall) { + this.log( + `[GptService] Calling function ${functionName} with arguments: ${JSON.stringify( + functionArgs + )}` + ); - // Check if the tool call is for the 'liveAgentHandoff' function - if (functionName === "liveAgentHandoff") { - // Proceed with summarizing the conversation, including the latest messages - // Introduce a delay before summarizing the conversation - setTimeout(async () => { - const conversationSummary = await this.summarizeConversation(); - - this.emit("endSession", { - reasonCode: "live-agent-handoff", - reason: functionResponse.reason, - conversationSummary: conversationSummary, + // If there's more than one tool call, use a general TTS message + if (toolCalls.length > 1) { + ttsMessage = "Let me handle a few things for you, one moment."; + } else { + // For a single tool call, use the specific message + const functionName = toolCalls[0].function.name; + ttsMessage = this.getTtsMessageForTool(functionName); + } + + // Inject phone numbers if it's the SMS function + if (functionName === "sendAppointmentConfirmationSms") { + const phoneNumbers = this.getPhoneNumbers(); + functionArgs = { ...functionArgs, ...phoneNumbers }; + } + const functionResponse = await functionToCall(functionArgs); + + function_call_result_message = { + role: "tool", + content: JSON.stringify(functionResponse), + tool_call_id: toolCall.id, + }; + + // Check if specific tool calls require additional system messages + if (functionName === "listAvailableApartments") { + systemMessages.push({ + role: "system", + content: + "Do not use asterisks (*) under any circumstances in this response. Summarize the available apartments in a readable format.", + }); + } + + // Personalize system messages based on user profile during relevant tool calls + if (functionName === "checkAvailability" && this.userProfile) { + const { firstName, moveInDate } = this.userProfile.profile; + systemMessages.push({ + role: "system", + content: `When checking availability for ${firstName}, remember that they are looking to move in on ${moveInDate}.`, }); + } + + if (functionName === "scheduleTour" && functionResponse.available) { + // Inject a system message to ask about SMS confirmation + systemMessages.push({ + role: "system", + content: + "If the user agrees to receive an SMS confirmation, immediately trigger the 'sendAppointmentConfirmationSms' tool with the appointment details and the UserProfile. Do not ask for their phone number or any other details from the user.", + }); + } + + // Check if the tool call is for the 'liveAgentHandoff' function + if (functionName === "liveAgentHandoff") { + // Proceed with summarizing the conversation, including the latest messages + // Introduce a delay before summarizing the conversation + setTimeout(async () => { + const conversationSummary = await this.summarizeConversation(); + + this.emit("endSession", { + reasonCode: "live-agent-handoff", + reason: functionResponse.reason, + conversationSummary: conversationSummary, + }); + + // Log the emission for debugging + this.log( + `[GptService] Emitting endSession event with reason: ${functionResponse.reason}` + ); + }, 3000); // 3-second delay // Log the emission for debugging this.log( `[GptService] Emitting endSession event with reason: ${functionResponse.reason}` ); - }, 3000); // 3-second delay + } - // Log the emission for debugging + // Push the tool response to be used in the final completion call + toolResponses.push(function_call_result_message); + } else { this.log( - `[GptService] Emitting endSession event with reason: ${functionResponse.reason}` + `[GptService] No available function found for ${functionName}` ); } + } - // Prepare the chat completion call payload with the tool result - const completion_payload = { - model: model, - messages: [ - ...this.userContext, - ...systemMessages, // Inject dynamic system messages when relevant - response.choices[0].message, // the tool_call message - function_call_result_message, // The result of the tool call - ], - }; - - // Call the API again to get the final response after tool processing - const finalResponse = await this.openai.chat.completions.create({ - model: completion_payload.model, - messages: completion_payload.messages, - stream: false, // Always non-streaming - }); - - const finalContent = finalResponse.choices[0]?.message?.content || ""; - this.userContext.push({ - role: "assistant", - content: finalContent, - }); - - // Emit the final response to the user - this.emit("gptreply", finalContent, true, interactionCount); - return; // Exit after processing the tool call + // Emit a single TTS message after processing all tool calls + if (!dtmfTriggered && ttsMessage) { + this.emit("gptreply", ttsMessage, true, interactionCount); } - } - // If no tool call is detected, emit the final completion response - const finalResponse = response.choices[0]?.message?.content || ""; - if (finalResponse.trim()) { + // Prepare the chat completion call payload with the tool result + const completion_payload = { + model: model, + messages: [ + ...this.userContext, + ...systemMessages, // Inject dynamic system messages when relevant + response.choices[0].message, // the tool_call message + ...toolResponses, // The result of the tool calls + ], + }; + + // Call the API again to get the final response after tool processing + const finalResponse = await this.openai.chat.completions.create({ + model: completion_payload.model, + messages: completion_payload.messages, + stream: false, // Always non-streaming + }); + + const finalContent = finalResponse.choices[0]?.message?.content || ""; this.userContext.push({ role: "assistant", - content: finalResponse, + content: finalContent, }); - this.emit("gptreply", finalResponse, true, interactionCount); + + // Emit the final response to the user + this.emit("gptreply", finalContent, true, interactionCount); + return; // Exit after processing the tool call + } else { + // If no tool call is detected, emit the final completion response + const finalResponse = response.choices[0]?.message?.content || ""; + if (finalResponse.trim()) { + this.userContext.push({ + role: "assistant", + content: finalResponse, + }); + this.emit("gptreply", finalResponse, true, interactionCount); + } } } catch (error) { - this.log(`[GptService] Error during completion: ${error.message}`); + this.log(`[GptService] Error during completion: ${error.stack}`); // Friendly response for any error encountered const friendlyMessage = diff --git a/services/gpt-service-streaming.js b/services/gpt-service-streaming.js index e063d3ec..2318d994 100644 --- a/services/gpt-service-streaming.js +++ b/services/gpt-service-streaming.js @@ -3,7 +3,7 @@ const EventEmitter = require("events"); const availableFunctions = require("../functions/available-functions"); const tools = require("../functions/function-manifest"); let prompt = require("../prompts/prompt"); -const welcomePrompt = require("../prompts/welcomePrompt"); +//const welcomePrompt = require("../prompts/welcomePrompt"); const model = "gpt-4o"; const currentDate = new Date().toLocaleDateString("en-US", { @@ -15,56 +15,74 @@ const currentDate = new Date().toLocaleDateString("en-US", { prompt = prompt.replace("{{currentDate}}", currentDate); -function getTtsMessageForTool(toolName, userProfile = null) { - const name = userProfile?.profile?.firstName - ? userProfile.profile.firstName - : ""; // Get the user's name if available - - const nameIntroOptions = name - ? [ - `Sure ${name},`, - `Okay ${name},`, - `Alright ${name},`, - `Got it ${name},`, - `Certainly ${name},`, - ] - : ["Sure,", "Okay,", "Alright,", "Got it,", "Certainly,"]; - - const randomIntro = - nameIntroOptions[Math.floor(Math.random() * nameIntroOptions.length)]; - - switch (toolName) { - case "listAvailableApartments": - return `${randomIntro} let me check on the available apartments for you.`; - case "checkExistingAppointments": - return `${randomIntro} I'll look up your existing appointments.`; - case "scheduleTour": - return `${randomIntro} I'll go ahead and schedule that tour for you.`; - case "checkAvailability": - return `${randomIntro} let me verify the availability for the requested time.`; - case "commonInquiries": - return `${randomIntro} let me check on that for you! Just a moment.`; - case "sendAppointmentConfirmationSms": - return `${randomIntro} I'll send that SMS off to you shortly, give it a few minutes and you should see it come through.`; - default: - return `${randomIntro} give me a moment while I fetch the information.`; - } -} - class GptService extends EventEmitter { constructor() { super(); this.openai = new OpenAI(); this.userContext = [ { role: "system", content: prompt }, - { - role: "assistant", - content: `${welcomePrompt}`, - }, + // Only do this if you're going to use the WelcomePrompt in VoxRay config + // { + // role: "assistant", + // content: `${welcomePrompt}`, + // }, ]; this.smsSendNumber = null; // Store the "To" number (Twilio's "from") this.phoneNumber = null; // Store the "From" number (user's phone) } + // Arrow function for getTtsMessageForTool, so it can access `this` + getTtsMessageForTool = (toolName) => { + const name = this.userProfile?.profile?.firstName + ? this.userProfile.profile.firstName + : ""; // Get the user's name if available + + const nameIntroOptions = name + ? [ + `Sure ${name},`, + `Okay ${name},`, + `Alright ${name},`, + `Got it ${name},`, + `Certainly ${name},`, + ] + : ["Sure,", "Okay,", "Alright,", "Got it,", "Certainly,"]; + + const randomIntro = + nameIntroOptions[Math.floor(Math.random() * nameIntroOptions.length)]; + + let message; + + switch (toolName) { + case "listAvailableApartments": + message = `${randomIntro} let me check on the available apartments for you.`; + break; + case "checkExistingAppointments": + message = `${randomIntro} I'll look up your existing appointments.`; + break; + case "scheduleTour": + message = `${randomIntro} I'll go ahead and schedule that tour for you.`; + break; + case "checkAvailability": + message = `${randomIntro} let me verify the availability for the requested time.`; + break; + case "commonInquiries": + message = `${randomIntro} one moment.`; + break; + case "sendAppointmentConfirmationSms": + message = `${randomIntro} I'll send that SMS off to you shortly, give it a few minutes and you should see it come through.`; + break; + case "liveAgentHandoff": + message = `${randomIntro} that may be a challenging topic to discuss, so I'm going to get you over to a live agent so they can discuss this with you, hang tight.`; + break; + default: + message = `${randomIntro} give me a moment while I fetch the information.`; + break; + } + + // Log the message to the userContext in gptService + this.updateUserContext("assistant", message); + + return message; // Return the message for TTS + }; setUserProfile(userProfile) { this.userProfile = userProfile; @@ -104,6 +122,46 @@ class GptService extends EventEmitter { this.userContext.push({ role: role, content: text }); } + async summarizeConversation() { + const summaryPrompt = "Summarize the conversation so far in 2-3 sentences."; + + // // Log the full userContext before making the API call + // console.log( + // `[GptService] Full userContext: ${JSON.stringify( + // this.userContext, + // null, + // 2 + // )}` + // ); + + // // Validate and log each message in userContext + // this.userContext.forEach((message, index) => { + // if (typeof message.content !== "string") { + // console.error( + // `[GptService] Invalid content type at index ${index}: ${JSON.stringify( + // message + // )}` + // ); + // } else { + // console.log( + // `[GptService] Valid content at index ${index}: ${message.content}` + // ); + // } + // }); + + const summaryResponse = await this.openai.chat.completions.create({ + model: model, + messages: [ + ...this.userContext, + { role: "system", content: summaryPrompt }, + ], + stream: false, // Non-streaming + }); + + const summary = summaryResponse.choices[0]?.message?.content || ""; + return summary; + } + async completion( text, interactionCount, @@ -117,10 +175,6 @@ class GptService extends EventEmitter { this.updateUserContext(role, text); - let completeResponse = ""; - let detectedTool = null; - let toolCallDetected = false; // Boolean to track tool call detection - try { // Streaming is enabled const response = await this.openai.chat.completions.create({ @@ -130,125 +184,123 @@ class GptService extends EventEmitter { stream: true, // Always streaming }); - for await (const chunk of response) { - const content = chunk.choices[0]?.delta?.content || ""; - completeResponse += content; - - // Log each chunk as it comes in - this.log(`[GptService] Chunk received: ${JSON.stringify(chunk)}`); - - // Check if a tool call is detected - const toolCall = chunk.choices[0]?.delta?.tool_calls; - if ((toolCall && toolCall[0]) || toolCallDetected) { - toolCallDetected = true; // Set the boolean to true - detectedTool = toolCall[0]; - // Log the parsed toolCall - this.log( - `[GptService] Parsed tool call: ${JSON.stringify(toolCall)}` - ); - this.log( - `[GptService] Tool call DETAILS 1: ${JSON.stringify( - toolCall[0], - null, - 2 - )}` - ); + let toolCallId = null; // To store the ID of the tool call at index 0 + let toolCallFunctionName = ""; // To store the dynamically received function name + let argumentsAccumulator = ""; // To accumulate the 'arguments' data as chunks come in + let isToolCallActive = false; // To track when the tool call starts and finishes + let contentAccumulator = ""; // To accumulate the 'content' before tool_calls + let finalMessageObject = { + role: "assistant", + content: null, + tool_calls: [], + refusal: null, + }; // Final object to store content and tool call details - this.log( - `[GptService] Tool call detected: ${toolCall[0].function.name}` - ); - } + let lastContentChunk = ""; // To store the last content chunk received + let contentPending = false; // Flag to track if there's content pending to be emitted - // Handle regular streaming if no tool call is detected - if (!toolCallDetected) { - this.emit("gptreply", content, false, interactionCount); - } + for await (const chunk of response) { + const { choices } = chunk; + + // // Log each chunk as it comes in + // this.log(`[GptService] Chunk received: ${JSON.stringify(chunk)}`); + + // Check if tool_calls are present in this chunk (could be part of multiple chunks) + if (choices[0]?.delta?.tool_calls) { + const toolCall = choices[0].delta.tool_calls[0]; + + if (!isToolCallActive) { + // this.log(`[GptService] Tool Call is Active Logic`); + // Initialize tool call when detected + if (toolCall?.id) { + toolCallId = toolCall.id; + toolCallFunctionName = toolCall.function.name; // Capture dynamic function name + isToolCallActive = true; + + // // Log tool call detection + // this.log( + // `[GptService] Parsed tool call: ${JSON.stringify(toolCall)}` + // ); + // this.log( + // `[GptService] Tool call DETAILS 1: ${JSON.stringify( + // toolCall, + // null, + // 2 + // )}` + // ); + this.log( + `[GptService] Tool call detected: ${toolCall.function.name}` + ); - // Check if the current chunk is the last one in the stream - if (chunk.choices[0].finish_reason === "stop") { - this.log(`[GptService] In finish reason === STOP`); - if (!toolCallDetected) { - //only process here if the tool call wasn't detected - // No tool call, push the final response - this.userContext.push({ - role: "assistant", - content: completeResponse, - }); - this.emit("gptreply", completeResponse, true, interactionCount); - this.log( - `[GptService] Final GPT -> user context length: ${this.userContext.length}` - ); - break; // Exit the loop since the response is complete - } else { - this.log( - `[GptService] Tool call DETAILS2: ${JSON.stringify( - toolCall[0], - null, - 2 - )}` - ); + // Set the content before the tool call starts + finalMessageObject.content = contentAccumulator.trim() || null; + } + } - this.log( - `[GptService] Tool call detected 2: ${toolCall[0].function.name}` - ); - detectedTool = toolCall[0]; + // Accumulate arguments as they come in across chunks + if (toolCall?.function?.arguments) { + argumentsAccumulator += toolCall.function.arguments; } } - // If we detected a tool call, process it now - if (toolCallDetected) { - this.log(`[GptService] In Tool Call logic`); - this.log( - `[GptService] DetectedTool: ${JSON.stringify(detectedTool)}` - ); - const functionName = detectedTool.function.name; - // Check if arguments are not empty and valid JSON - let functionArgs; + + // Separate block to handle when finish_reason is 'tool_calls' + if (choices[0]?.finish_reason === "tool_calls") { + // this.log(`[GptService] Finish Reason is Tool Calls`); + + let parsedArguments; try { - functionArgs = detectedTool.function.arguments - ? JSON.parse(detectedTool.function.arguments) - : {}; // Default to empty object if no arguments + // Parse accumulated arguments + parsedArguments = JSON.parse(argumentsAccumulator); + + // Reset the accumulator for future tool calls + argumentsAccumulator = ""; } catch (error) { - this.log( - `[GptService] Error parsing function arguments: ${error.message}` - ); - functionArgs = {}; // Default to empty object if parsing fails + console.error("Error parsing arguments:", error); + parsedArguments = argumentsAccumulator; // Fallback in case of parsing failure } - const functionToCall = availableFunctions[functionName]; - // Inject phone numbers if it's the SMS function - if (functionName === "sendAppointmentConfirmationSms") { - const phoneNumbers = this.getPhoneNumbers(); - functionArgs = { ...functionArgs, ...phoneNumbers }; - } + // Finalize the tool_calls part of the message object + finalMessageObject.tool_calls.push({ + id: toolCallId, + type: "function", + function: { + name: toolCallFunctionName, + arguments: JSON.stringify(parsedArguments), // Ensure arguments are stringified + }, + }); + + // Now perform the tool logic as all tool_call data is ready + const functionToCall = availableFunctions[toolCallFunctionName]; this.log( - `[GptService] Calling function ${functionName} with arguments: ${JSON.stringify( - functionArgs + `[GptService] Calling function ${toolCallFunctionName} with arguments: ${JSON.stringify( + parsedArguments )}` ); - if (!dtmfTriggered) { - // Emit TTS message related to the tool call - const ttsMessage = getTtsMessageForTool( - functionName, - this.userProfile - ); - this.emit("gptreply", ttsMessage, true, interactionCount); // Emit the TTS message immediately - } + // if (!dtmfTriggered) { + // // Emit TTS message related to the tool call + // const ttsMessage = this.getTtsMessageForTool(toolCallFunctionName); + // this.emit("gptreply", ttsMessage, true, interactionCount); // Emit the TTS message immediately + // } - const functionResponse = await functionToCall(functionArgs); + // Inject phone numbers if it's the SMS function + if (toolCallFunctionName === "sendAppointmentConfirmationSms") { + const phoneNumbers = this.getPhoneNumbers(); + parsedArguments = { ...parsedArguments, ...phoneNumbers }; + } - let function_call_result_message; + const functionResponse = await functionToCall(parsedArguments); - function_call_result_message = { + let function_call_result_message = { role: "tool", content: JSON.stringify(functionResponse), - tool_call_id: detectedTool.id, + tool_call_id: toolCallId, }; // Check if specific tool calls require additional system messages const systemMessages = []; - if (functionName === "listAvailableApartments") { + if (toolCallFunctionName === "listAvailableApartments") { systemMessages.push({ role: "system", content: @@ -257,7 +309,10 @@ class GptService extends EventEmitter { } // Personalize system messages based on user profile during relevant tool calls - if (functionName === "checkAvailability" && this.userProfile) { + if ( + toolCallFunctionName === "checkAvailability" && + this.userProfile + ) { const { firstName, moveInDate } = this.userProfile.profile; systemMessages.push({ role: "system", @@ -265,13 +320,46 @@ class GptService extends EventEmitter { }); } + if ( + toolCallFunctionName === "scheduleTour" && + functionResponse.available + ) { + // Inject a system message to ask about SMS confirmation + systemMessages.push({ + role: "system", + content: + "If the user agrees to receive an SMS confirmation, immediately trigger the 'sendAppointmentConfirmationSms' tool with the appointment details and the UserProfile. Do not ask for their phone number or any other details from the user.", + }); + } + + // Check if the tool call is for the 'liveAgentHandoff' function + if (toolCallFunctionName === "liveAgentHandoff") { + setTimeout(async () => { + const conversationSummary = await this.summarizeConversation(); + + this.emit("endSession", { + reasonCode: "live-agent-handoff", + reason: functionResponse.reason, + conversationSummary: conversationSummary, + }); + + this.log( + `[GptService] Emitting endSession event with reason: ${functionResponse.reason}` + ); + }, 3000); // 3-second delay + + this.log( + `[GptService] Emitting endSession event with reason: ${functionResponse.reason}` + ); + } + // Prepare the chat completion call payload with the tool result const completion_payload = { model: model, messages: [ ...this.userContext, ...systemMessages, // Inject dynamic system messages when relevant - response.choices[0].message, // the tool_call message + finalMessageObject, // the tool_call message function_call_result_message, // The result of the tool call ], }; @@ -285,42 +373,106 @@ class GptService extends EventEmitter { } ); - let finalResponse = ""; + // Handle the final response stream (same logic as before) + let finalContentAccumulator = ""; for await (const chunk of finalResponseStream) { - const content = chunk.choices[0]?.delta?.content || ""; - finalResponse += content; - this.emit("gptreply", content, false, interactionCount); + const { choices } = chunk; + + // this.log( + // `[GptService] Final Chunk received: ${JSON.stringify(chunk)}` + // ); + + // Accumulate the content from each chunk + if (choices[0]?.delta?.content) { + if (contentPending && lastContentChunk) { + this.emit( + "gptreply", + lastContentChunk, + false, + interactionCount + ); + } + + lastContentChunk = choices[0].delta.content; + finalContentAccumulator += lastContentChunk; + contentPending = true; + } + + // Handle 'finish_reason' to detect the end of streaming + if (choices[0].finish_reason === "stop") { + // this.log(`[GptService] Final response STOP detected`); - if (chunk.choices[0].finish_reason === "stop") { + if (lastContentChunk) { + this.emit("gptreply", lastContentChunk, true, interactionCount); + } + + // Push the final accumulated content into userContext this.userContext.push({ role: "assistant", - content: finalResponse, + content: finalContentAccumulator.trim(), }); - if ( - functionName === "scheduleTour" && - functionResponse.available - ) { - // Inject a system message to ask about SMS confirmation - this.userContext.push({ - role: "system", - content: - "If the user agrees to receive an SMS confirmation, immediately trigger the 'sendAppointmentConfirmationSms' tool with the appointment details and the UserProfile. Do not ask for their phone number or any other details from the user.", - }); - } - this.emit("gptreply", content, true, interactionCount); this.log( `[GptService] Final GPT -> user context length: ${this.userContext.length}` ); - break; // Finish the loop + break; // Exit the loop once the final response is complete } } + + // Reset tool call state + toolCallId = null; + toolCallFunctionName = ""; + isToolCallActive = false; + argumentsAccumulator = ""; + } + + // Handle non-tool_call content chunks + if (choices[0]?.delta?.content) { + if (contentPending && lastContentChunk) { + this.emit("gptreply", lastContentChunk, false, interactionCount); + } + + lastContentChunk = choices[0].delta.content; + contentAccumulator += lastContentChunk; + contentPending = true; + } + + if (choices[0]?.delta?.refusal !== null) { + finalMessageObject.refusal = choices[0].delta.refusal; + } + + // Check if the current chunk is the last one in the stream + if (choices[0].finish_reason === "stop") { + // this.log(`[GptService] In finish reason === STOP`); + + if (lastContentChunk) { + this.emit("gptreply", lastContentChunk, true, interactionCount); + } + + this.userContext.push({ + role: "assistant", + content: contentAccumulator.trim(), + }); + + this.log( + `[GptService] Final GPT -> user context length: ${this.userContext.length}` + ); } } } catch (error) { - this.log( - `[GptService] Error during tool call processing: ${error.stack}` - ); + this.log(`[GptService] Error during completion: ${error.stack}`); + + // Friendly response for any error encountered + const friendlyMessage = + "I apologize, that request might have been a bit too complex. Could you try asking one thing at a time? I'd be happy to help step by step!"; + + // Emit the friendly message to the user + this.emit("gptreply", friendlyMessage, true, interactionCount); + + // Push the message into the assistant context + this.updateUserContext("assistant", friendlyMessage); + + return; // Stop further processing } } } From 1306a414efadb02b042d860cbbf5edaa7c9f2a7d Mon Sep 17 00:00:00 2001 From: Chris Feehan Date: Mon, 19 Aug 2024 16:38:36 -0500 Subject: [PATCH 09/26] Remove personalization.js from repository tracking --- data/personalization.js | 329 ---------------------------------------- 1 file changed, 329 deletions(-) delete mode 100644 data/personalization.js diff --git a/data/personalization.js b/data/personalization.js deleted file mode 100644 index 23c76127..00000000 --- a/data/personalization.js +++ /dev/null @@ -1,329 +0,0 @@ -const customerProfiles = { - "+17632291691": { - profile: { - firstName: "Chris", - lastName: "Feehan", - phoneNumber: "+17632291691", - email: "cfeehan@twilio.com", - preferredApartmentType: "studio", - budget: 1500, - moveInDate: "2024-09-15", - petOwner: false, - tourPreference: "self-guided", - }, - conversationHistory: [ - { - date: "2024-08-10", - summary: - "Chris inquired about available studio apartments and the pet policy. The assistant confirmed that cats and small dogs are allowed with a fee.", - }, - { - date: "2024-08-08", - summary: - "Chris asked about scheduling a self-guided tour for a one-bedroom apartment. The assistant offered available times, but Chris did not confirm.", - }, - { - date: "2024-08-07", - summary: - "Chris asked if the rent for the one-bedroom apartment could be negotiated. The assistant clarified that the listed prices are firm.", - }, - { - date: "2024-08-06", - summary: - "Chris asked if there is a daycare facility nearby. The assistant mentioned that there is no daycare service on-site and could not provide information about local options.", - }, - { - date: "2024-08-05", - summary: - "Chris asked about the utility fees for studio apartments. The assistant explained that water and trash are included, but electricity and internet are not.", - }, - { - date: "2024-08-04", - summary: - "Chris asked if there were any restrictions on hanging artwork or decorations in the apartment. The assistant did not have that information and advised Chris to check the lease agreement for decoration policies.", - }, - { - date: "2024-08-03", - summary: - "Chris asked if there were any promotions for signing a lease. The assistant explained that there were no current promotions for studio apartments.", - }, - { - date: "2024-08-02", - summary: - "Chris asked about the availability of two-bedroom apartments and mentioned a budget of $1,200. The assistant confirmed that two-bedroom apartments are outside the budget range.", - }, - { - date: "2024-08-01", - summary: - "Chris asked if the studio apartment included parking. The assistant confirmed that parking is available but comes with an additional fee.", - }, - { - date: "2024-07-31", - summary: - "Chris asked if Parkview Apartments offered a shuttle service to downtown. The assistant explained that Parkview does not provide any transportation services.", - }, - ], - }, - "+16503800236": { - profile: { - firstName: "Austin", - lastName: "Park", - phoneNumber: "+16503800236", - email: "apark@twilio.com", - preferredApartmentType: "two-bedroom", - budget: 2200, - moveInDate: "2024-10-01", - petOwner: true, - tourPreference: "in-person", - }, - conversationHistory: [ - { - date: "2024-08-12", - summary: - "Austin inquired about two-bedroom apartments and asked for the earliest move-in date. The assistant confirmed that the earliest move-in date is September 15, 2024.", - }, - { - date: "2024-08-10", - summary: - "Austin asked about the availability of parking spots for two-bedroom apartments. The assistant confirmed that each apartment comes with one reserved parking spot and additional parking is available for a fee.", - }, - { - date: "2024-08-08", - summary: - "Austin inquired about the pet policy, particularly if there are restrictions on dog breeds. The assistant confirmed that cats and small dogs are allowed with a fee, but large dogs may not be permitted.", - }, - { - date: "2024-08-07", - summary: - "Austin asked if utilities are included for the two-bedroom apartments. The assistant explained that water, trash, and Wi-Fi are included, but electricity and gas are the tenant's responsibility.", - }, - { - date: "2024-08-05", - summary: - "Austin asked about nearby parks for walking his dog. The assistant confirmed that there are a few parks within walking distance, but had no specific recommendations.", - }, - { - date: "2024-08-03", - summary: - "Austin asked if there are any current move-in specials for two-bedroom apartments. The assistant confirmed that there are no promotions available at this time.", - }, - { - date: "2024-08-02", - summary: - "Austin asked about the availability of two-bedroom apartments with hardwood floors. The assistant confirmed that all available two-bedroom apartments have hardwood floors in the living areas.", - }, - { - date: "2024-08-01", - summary: - "Austin inquired about the security deposit for the two-bedroom apartments. The assistant confirmed that the security deposit is $300.", - }, - { - date: "2024-07-30", - summary: - "Austin asked if there is a fitness center available on-site. The assistant confirmed that there is a small gym available for residents with no additional fee.", - }, - { - date: "2024-07-28", - summary: - "Austin asked if the apartment complex offers a concierge service. The assistant clarified that there is no concierge service available.", - }, - ], - }, - "+13035130469": { - profile: { - firstName: "Andy", - lastName: "O'Dower", - phoneNumber: "+13035130469", - email: "aodower@twilio.com@twilio.com", - preferredApartmentType: "two-bedroom", - budget: 1800, - moveInDate: "2024-09-25", - petOwner: false, - tourPreference: "in-person", - }, - conversationHistory: [ - { - date: "2024-08-11", - summary: - "Andy asked about available two-bedroom apartments with balcony access. The assistant confirmed availability and provided details about units with private balconies.", - }, - { - date: "2024-08-09", - summary: - "Andy inquired about the security features in the building, particularly around entry and exit points. The assistant confirmed that the building has 24/7 security and a controlled access system.", - }, - { - date: "2024-08-07", - summary: - "Andy asked about the move-in process and whether any assistance would be provided with moving services. The assistant clarified that while moving services are not provided, there are several recommended local movers.", - }, - { - date: "2024-08-06", - summary: - "Andy asked if there were any discounts or offers available for signing a lease within the next week. The assistant informed Andy that there were no current promotions, but to check back frequently to ask for specials or discounts as there may be new offers.", - }, - { - date: "2024-08-04", - summary: - "Andy inquired about utility costs for two-bedroom apartments and asked if internet service was included. The assistant confirmed that Water, trash, gas, and Wi-Fi included, but that Andy would pay for electricity.", - }, - { - date: "2024-08-03", - summary: - "Andy asked if the building has electric vehicle (EV) charging stations. The assistant could not confirm whether any EV stations were nearby.", - }, - { - date: "2024-08-02", - summary: - "Andy asked if the apartment had a washer and dryer in-unit or if it was shared. The assistant confirmed that all units have in-unit laundry facilities.", - }, - { - date: "2024-08-01", - summary: - "Andy asked about public transportation options near the apartment complex. The assistant provided details about nearby bus and train stations.", - }, - { - date: "2024-07-30", - summary: - "Andy inquired about the guest policy and whether guests could stay overnight. The assistant confirmed that guests are allowed to stay overnight, but extended stays would require approval.", - }, - { - date: "2024-07-28", - summary: - "Andy asked if any two-bedroom units have been recently renovated. The assistant provided details on newly renovated units and their availability.", - }, - ], - }, - "+15126507668": { - profile: { - firstName: "Sean", - lastName: "Bond", - phoneNumber: "+15126507668", - email: "sbond@twilio.com", - preferredApartmentType: "two-bedroom", - budget: 1800, - moveInDate: "2024-09-25", - petOwner: false, - tourPreference: "in-person", - }, - conversationHistory: [ - { - date: "2024-08-11", - summary: - "Sean asked about available two-bedroom apartments with balcony access. The assistant confirmed availability and provided details about units with private balconies.", - }, - { - date: "2024-08-09", - summary: - "Sean inquired about the security features in the building, particularly around entry and exit points. The assistant confirmed that the building has 24/7 security and a controlled access system.", - }, - { - date: "2024-08-07", - summary: - "Sean asked about the move-in process and whether any assistance would be provided with moving services. The assistant clarified that while moving services are not provided, there are several recommended local movers.", - }, - { - date: "2024-08-06", - summary: - "Sean asked if there were any discounts or offers available for signing a lease within the next week. The assistant informed Sean that there were no current promotions, but to check back frequently to ask for specials or discounts as there may be new offers.", - }, - { - date: "2024-08-04", - summary: - "Sean inquired about utility costs for two-bedroom apartments and asked if internet service was included. The assistant confirmed that Water, trash, gas, and Wi-Fi included, but that Sean would pay for electricity.", - }, - { - date: "2024-08-03", - summary: - "Sean asked if the building has electric vehicle (EV) charging stations. The assistant could not confirm whether any EV stations were nearby.", - }, - { - date: "2024-08-02", - summary: - "Sean asked if the apartment had a washer and dryer in-unit or if it was shared. The assistant confirmed that all units have in-unit laundry facilities.", - }, - { - date: "2024-08-01", - summary: - "Sean asked about public transportation options near the apartment complex. The assistant provided details about nearby bus and train stations.", - }, - { - date: "2024-07-30", - summary: - "Sean inquired about the guest policy and whether guests could stay overnight. The assistant confirmed that guests are allowed to stay overnight, but extended stays would require approval.", - }, - { - date: "2024-07-28", - summary: - "Sean asked if any two-bedroom units have been recently renovated. The assistant provided details on newly renovated units and their availability.", - }, - ], - }, - "+18323109635": { - profile: { - firstName: "Marian", - lastName: "Menschig", - phoneNumber: "+18323109635", - email: "mmenschig@twilio.com", - preferredApartmentType: "studio", - budget: 1500, - moveInDate: "2024-09-15", - petOwner: true, - tourPreference: "in-person", - }, - conversationHistory: [ - { - date: "2024-08-10", - summary: - "Marian inquired about available studio apartments and the pet policy. The assistant confirmed that cats and small dogs are allowed with a fee.", - }, - { - date: "2024-08-08", - summary: - "Marian asked about scheduling a self-guided tour for a one-bedroom apartment. The assistant offered available times, but Marian did not confirm.", - }, - { - date: "2024-08-07", - summary: - "Marian asked if the rent for the one-bedroom apartment could be negotiated. The assistant clarified that the listed prices are firm.", - }, - { - date: "2024-08-06", - summary: - "Marian asked if there is a daycare facility nearby. The assistant mentioned that there is no daycare service on-site and could not provide information about local options.", - }, - { - date: "2024-08-05", - summary: - "Marian asked about the utility fees for studio apartments. The assistant explained that water and trash are included, but electricity and internet are not.", - }, - { - date: "2024-08-04", - summary: - "Marian asked if there were any restrictions on hanging artwork or decorations in the apartment. The assistant did not have that information and advised Marian to check the lease agreement for decoration policies.", - }, - { - date: "2024-08-03", - summary: - "Marian asked if there were any promotions for signing a lease. The assistant explained that there were no current promotions for studio apartments.", - }, - { - date: "2024-08-02", - summary: - "Marian asked about the availability of two-bedroom apartments and mentioned a budget of $1,200. The assistant confirmed that two-bedroom apartments are outside the budget range.", - }, - { - date: "2024-08-01", - summary: - "Marian asked if the studio apartment included parking. The assistant confirmed that parking is available but comes with an additional fee.", - }, - { - date: "2024-07-31", - summary: - "Marian asked if Parkview Apartments offered a shuttle service to downtown. The assistant explained that Parkview does not provide any transportation services.", - }, - ], - }, -}; - -module.exports = customerProfiles; From 690eac1fd019b537034e522bd916c06a996eca3c Mon Sep 17 00:00:00 2001 From: Chris Feehan Date: Wed, 21 Aug 2024 11:23:54 -0500 Subject: [PATCH 10/26] Fixed Time Normalization, Improved SMS handling, Fixed Streaming with multiple tools --- app.js | 12 +- data/personalization.js | 589 ++++++++++++++++++++++++++ functions/available-functions.js | 32 +- prompts/prompt.js | 9 +- services/gpt-service-non-streaming.js | 11 +- services/gpt-service-streaming.js | 282 ++++++------ 6 files changed, 788 insertions(+), 147 deletions(-) create mode 100644 data/personalization.js diff --git a/app.js b/app.js index 30af9231..dfde10ae 100644 --- a/app.js +++ b/app.js @@ -5,7 +5,7 @@ const express = require("express"); const ExpressWs = require("express-ws"); //const { GptService } = require("./services/gpt-service-streaming"); -const { GptService } = require("./services/gpt-service-non-streaming"); +const { GptService } = require("./services/gpt-service-streaming"); const { TextService } = require("./services/text-service"); const { EndSessionService } = require("./services/end-session-service"); @@ -160,6 +160,12 @@ async function handleDtmfInput( app.post("/incoming", (req, res) => { try { + //WITH WELCOME PROMPT + // const response = ` + // + // + // + // `; const response = ` @@ -229,8 +235,8 @@ app.ws("/sockets", (ws) => { // Now generate a dynamic personalized greeting based on whether the user is new or returning const greetingText = userProfile - ? `Generate a personalized greeting for ${userProfile.profile.firstName}, a returning customer.` - : "Generate a warm greeting for a new user."; + ? `Generate a warm, personalized greeting for ${userProfile.profile.firstName}, a returning prospect. Keep it brief, and use informal/casual language so you sound like a friend, not a call center agent.` + : "Generate a warm greeting for a new potential prospect. Keep it brief, and use informal/casual language so you sound like a friend, not a call center agent."; // Call the LLM to generate the greeting dynamically, and it should be a another "system" prompt await gptService.completion(greetingText, interactionCount, "system"); diff --git a/data/personalization.js b/data/personalization.js new file mode 100644 index 00000000..bd9201c8 --- /dev/null +++ b/data/personalization.js @@ -0,0 +1,589 @@ +const customerProfiles = { + "+17632291691": { + profile: { + firstName: "Chris", + lastName: "Feehan", + phoneNumber: "+17632291691", + email: "cfeehan@twilio.com", + preferredApartmentType: "studio", + budget: 1500, + moveInDate: "2024-09-15", + petOwner: false, + tourPreference: "self-guided", + }, + conversationHistory: [ + { + date: "2024-08-10", + summary: + "Chris inquired about available studio apartments and the pet policy. The assistant confirmed that cats and small dogs are allowed with a fee.", + }, + { + date: "2024-08-08", + summary: + "Chris asked about scheduling a self-guided tour for a one-bedroom apartment. The assistant offered available times, but Chris did not confirm.", + }, + { + date: "2024-08-07", + summary: + "Chris asked if the rent for the one-bedroom apartment could be negotiated. The assistant clarified that the listed prices are firm.", + }, + { + date: "2024-08-06", + summary: + "Chris asked if there is a daycare facility nearby. The assistant mentioned that there is no daycare service on-site and could not provide information about local options.", + }, + { + date: "2024-08-05", + summary: + "Chris asked about the utility fees for studio apartments. The assistant explained that water and trash are included, but electricity and internet are not.", + }, + { + date: "2024-08-04", + summary: + "Chris asked if there were any restrictions on hanging artwork or decorations in the apartment. The assistant did not have that information and advised Chris to check the lease agreement for decoration policies.", + }, + { + date: "2024-08-03", + summary: + "Chris asked if there were any promotions for signing a lease. The assistant explained that there were no current promotions for studio apartments.", + }, + { + date: "2024-08-02", + summary: + "Chris asked about the availability of two-bedroom apartments and mentioned a budget of $1,200. The assistant confirmed that two-bedroom apartments are outside the budget range.", + }, + { + date: "2024-08-01", + summary: + "Chris asked if the studio apartment included parking. The assistant confirmed that parking is available but comes with an additional fee.", + }, + { + date: "2024-07-31", + summary: + "Chris asked if Parkview Apartments offered a shuttle service to downtown. The assistant explained that Parkview does not provide any transportation services.", + }, + ], + }, + "+16672074063": { + profile: { + firstName: "Kunal", + lastName: "Maiti", + phoneNumber: "+16672074063", + email: "kmaiti@twilio.com", + preferredApartmentType: "studio", + budget: 1500, + moveInDate: "2024-09-15", + petOwner: false, + tourPreference: "self-guided", + }, + conversationHistory: [ + { + date: "2024-08-10", + summary: + "Kunal inquired about available studio apartments and the pet policy. The assistant confirmed that cats and small dogs are allowed with a fee.", + }, + { + date: "2024-08-08", + summary: + "Kunal asked about scheduling a self-guided tour for a one-bedroom apartment. The assistant offered available times, but Kunal did not confirm.", + }, + { + date: "2024-08-07", + summary: + "Kunal asked if the rent for the one-bedroom apartment could be negotiated. The assistant clarified that the listed prices are firm.", + }, + { + date: "2024-08-06", + summary: + "Kunal asked if there is a daycare facility nearby. The assistant mentioned that there is no daycare service on-site and could not provide information about local options.", + }, + { + date: "2024-08-05", + summary: + "Kunal asked about the utility fees for studio apartments. The assistant explained that water and trash are included, but electricity and internet are not.", + }, + { + date: "2024-08-04", + summary: + "Kunal asked if there were any restrictions on hanging artwork or decorations in the apartment. The assistant did not have that information and advised Kunal to check the lease agreement for decoration policies.", + }, + { + date: "2024-08-03", + summary: + "Kunal asked if there were any promotions for signing a lease. The assistant explained that there were no current promotions for studio apartments.", + }, + { + date: "2024-08-02", + summary: + "Kunal asked about the availability of two-bedroom apartments and mentioned a budget of $1,200. The assistant confirmed that two-bedroom apartments are outside the budget range.", + }, + { + date: "2024-08-01", + summary: + "Kunal asked if the studio apartment included parking. The assistant confirmed that parking is available but comes with an additional fee.", + }, + { + date: "2024-07-31", + summary: + "Kunal asked if Parkview Apartments offered a shuttle service to downtown. The assistant explained that Parkview does not provide any transportation services.", + }, + ], + }, + "+12023095963": { + profile: { + firstName: "Alex", + lastName: "Millet", + phoneNumber: "+12023095963", + email: "amillet@twilio.com", + preferredApartmentType: "studio", + budget: 1500, + moveInDate: "2024-09-15", + petOwner: false, + tourPreference: "self-guided", + }, + conversationHistory: [ + { + date: "2024-08-10", + summary: + "Alex inquired about available studio apartments and the pet policy. The assistant confirmed that cats and small dogs are allowed with a fee.", + }, + { + date: "2024-08-08", + summary: + "Alex asked about scheduling a self-guided tour for a one-bedroom apartment. The assistant offered available times, but Alex did not confirm.", + }, + { + date: "2024-08-07", + summary: + "Alex asked if the rent for the one-bedroom apartment could be negotiated. The assistant clarified that the listed prices are firm.", + }, + { + date: "2024-08-06", + summary: + "Alex asked if there is a daycare facility nearby. The assistant mentioned that there is no daycare service on-site and could not provide information about local options.", + }, + { + date: "2024-08-05", + summary: + "Alex asked about the utility fees for studio apartments. The assistant explained that water and trash are included, but electricity and internet are not.", + }, + { + date: "2024-08-04", + summary: + "Alex asked if there were any restrictions on hanging artwork or decorations in the apartment. The assistant did not have that information and advised Alex to check the lease agreement for decoration policies.", + }, + { + date: "2024-08-03", + summary: + "Alex asked if there were any promotions for signing a lease. The assistant explained that there were no current promotions for studio apartments.", + }, + { + date: "2024-08-02", + summary: + "Alex asked about the availability of two-bedroom apartments and mentioned a budget of $1,200. The assistant confirmed that two-bedroom apartments are outside the budget range.", + }, + { + date: "2024-08-01", + summary: + "Alex asked if the studio apartment included parking. The assistant confirmed that parking is available but comes with an additional fee.", + }, + { + date: "2024-07-31", + summary: + "Alex asked if Parkview Apartments offered a shuttle service to downtown. The assistant explained that Parkview does not provide any transportation services.", + }, + ], + }, + "+14695251496": { + profile: { + firstName: "Alberto", + lastName: "Montilla", + phoneNumber: "+14695251496", + email: "amontilla@twilio.com", + preferredApartmentType: "studio", + budget: 1500, + moveInDate: "2024-09-15", + petOwner: false, + tourPreference: "self-guided", + }, + conversationHistory: [ + { + date: "2024-08-10", + summary: + "Alberto inquired about available studio apartments and the pet policy. The assistant confirmed that cats and small dogs are allowed with a fee.", + }, + { + date: "2024-08-08", + summary: + "Alberto asked about scheduling a self-guided tour for a one-bedroom apartment. The assistant offered available times, but Alberto did not confirm.", + }, + { + date: "2024-08-07", + summary: + "Alberto asked if the rent for the one-bedroom apartment could be negotiated. The assistant clarified that the listed prices are firm.", + }, + { + date: "2024-08-06", + summary: + "Alberto asked if there is a daycare facility nearby. The assistant mentioned that there is no daycare service on-site and could not provide information about local options.", + }, + { + date: "2024-08-05", + summary: + "Alberto asked about the utility fees for studio apartments. The assistant explained that water and trash are included, but electricity and internet are not.", + }, + { + date: "2024-08-04", + summary: + "Alberto asked if there were any restrictions on hanging artwork or decorations in the apartment. The assistant did not have that information and advised Alberto to check the lease agreement for decoration policies.", + }, + { + date: "2024-08-03", + summary: + "Alberto asked if there were any promotions for signing a lease. The assistant explained that there were no current promotions for studio apartments.", + }, + { + date: "2024-08-02", + summary: + "Alberto asked about the availability of two-bedroom apartments and mentioned a budget of $1,200. The assistant confirmed that two-bedroom apartments are outside the budget range.", + }, + { + date: "2024-08-01", + summary: + "Alberto asked if the studio apartment included parking. The assistant confirmed that parking is available but comes with an additional fee.", + }, + { + date: "2024-07-31", + summary: + "Alberto asked if Parkview Apartments offered a shuttle service to downtown. The assistant explained that Parkview does not provide any transportation services.", + }, + ], + }, + "+18477570040": { + profile: { + firstName: "Jeff", + lastName: "Eiden", + phoneNumber: "+18477570040", + email: "jeiden@twilio.com", + preferredApartmentType: "studio", + budget: 1500, + moveInDate: "2024-09-15", + petOwner: false, + tourPreference: "self-guided", + }, + conversationHistory: [ + { + date: "2024-08-10", + summary: + "Jeff inquired about available studio apartments and the pet policy. The assistant confirmed that cats and small dogs are allowed with a fee.", + }, + { + date: "2024-08-08", + summary: + "Jeff asked about scheduling a self-guided tour for a one-bedroom apartment. The assistant offered available times, but Jeff did not confirm.", + }, + { + date: "2024-08-07", + summary: + "Jeff asked if the rent for the one-bedroom apartment could be negotiated. The assistant clarified that the listed prices are firm.", + }, + { + date: "2024-08-06", + summary: + "Jeff asked if there is a daycare facility nearby. The assistant mentioned that there is no daycare service on-site and could not provide information about local options.", + }, + { + date: "2024-08-05", + summary: + "Jeff asked about the utility fees for studio apartments. The assistant explained that water and trash are included, but electricity and internet are not.", + }, + { + date: "2024-08-04", + summary: + "Jeff asked if there were any restrictions on hanging artwork or decorations in the apartment. The assistant did not have that information and advised Jeff to check the lease agreement for decoration policies.", + }, + { + date: "2024-08-03", + summary: + "Jeff asked if there were any promotions for signing a lease. The assistant explained that there were no current promotions for studio apartments.", + }, + { + date: "2024-08-02", + summary: + "Jeff asked about the availability of two-bedroom apartments and mentioned a budget of $1,200. The assistant confirmed that two-bedroom apartments are outside the budget range.", + }, + { + date: "2024-08-01", + summary: + "Jeff asked if the studio apartment included parking. The assistant confirmed that parking is available but comes with an additional fee.", + }, + { + date: "2024-07-31", + summary: + "Jeff asked if Parkview Apartments offered a shuttle service to downtown. The assistant explained that Parkview does not provide any transportation services.", + }, + ], + }, + "+16503800236": { + profile: { + firstName: "Austin", + lastName: "Park", + phoneNumber: "+16503800236", + email: "apark@twilio.com", + preferredApartmentType: "two-bedroom", + budget: 2200, + moveInDate: "2024-10-01", + petOwner: true, + tourPreference: "in-person", + }, + conversationHistory: [ + { + date: "2024-08-12", + summary: + "Austin inquired about two-bedroom apartments and asked for the earliest move-in date. The assistant confirmed that the earliest move-in date is September 15, 2024.", + }, + { + date: "2024-08-10", + summary: + "Austin asked about the availability of parking spots for two-bedroom apartments. The assistant confirmed that each apartment comes with one reserved parking spot and additional parking is available for a fee.", + }, + { + date: "2024-08-08", + summary: + "Austin inquired about the pet policy, particularly if there are restrictions on dog breeds. The assistant confirmed that cats and small dogs are allowed with a fee, but large dogs may not be permitted.", + }, + { + date: "2024-08-07", + summary: + "Austin asked if utilities are included for the two-bedroom apartments. The assistant explained that water, trash, and Wi-Fi are included, but electricity and gas are the tenant's responsibility.", + }, + { + date: "2024-08-05", + summary: + "Austin asked about nearby parks for walking his dog. The assistant confirmed that there are a few parks within walking distance, but had no specific recommendations.", + }, + { + date: "2024-08-03", + summary: + "Austin asked if there are any current move-in specials for two-bedroom apartments. The assistant confirmed that there are no promotions available at this time.", + }, + { + date: "2024-08-02", + summary: + "Austin asked about the availability of two-bedroom apartments with hardwood floors. The assistant confirmed that all available two-bedroom apartments have hardwood floors in the living areas.", + }, + { + date: "2024-08-01", + summary: + "Austin inquired about the security deposit for the two-bedroom apartments. The assistant confirmed that the security deposit is $300.", + }, + { + date: "2024-07-30", + summary: + "Austin asked if there is a fitness center available on-site. The assistant confirmed that there is a small gym available for residents with no additional fee.", + }, + { + date: "2024-07-28", + summary: + "Austin asked if the apartment complex offers a concierge service. The assistant clarified that there is no concierge service available.", + }, + ], + }, + "+13035130469": { + profile: { + firstName: "Andy", + lastName: "O'Dower", + phoneNumber: "+13035130469", + email: "aodower@twilio.com@twilio.com", + preferredApartmentType: "two-bedroom", + budget: 1800, + moveInDate: "2024-09-25", + petOwner: false, + tourPreference: "in-person", + }, + conversationHistory: [ + { + date: "2024-08-11", + summary: + "Andy asked about available two-bedroom apartments with balcony access. The assistant confirmed availability and provided details about units with private balconies.", + }, + { + date: "2024-08-09", + summary: + "Andy inquired about the security features in the building, particularly around entry and exit points. The assistant confirmed that the building has 24/7 security and a controlled access system.", + }, + { + date: "2024-08-07", + summary: + "Andy asked about the move-in process and whether any assistance would be provided with moving services. The assistant clarified that while moving services are not provided, there are several recommended local movers.", + }, + { + date: "2024-08-06", + summary: + "Andy asked if there were any discounts or offers available for signing a lease within the next week. The assistant informed Andy that there were no current promotions, but to check back frequently to ask for specials or discounts as there may be new offers.", + }, + { + date: "2024-08-04", + summary: + "Andy inquired about utility costs for two-bedroom apartments and asked if internet service was included. The assistant confirmed that Water, trash, gas, and Wi-Fi included, but that Andy would pay for electricity.", + }, + { + date: "2024-08-03", + summary: + "Andy asked if the building has electric vehicle (EV) charging stations. The assistant could not confirm whether any EV stations were nearby.", + }, + { + date: "2024-08-02", + summary: + "Andy asked if the apartment had a washer and dryer in-unit or if it was shared. The assistant confirmed that all units have in-unit laundry facilities.", + }, + { + date: "2024-08-01", + summary: + "Andy asked about public transportation options near the apartment complex. The assistant provided details about nearby bus and train stations.", + }, + { + date: "2024-07-30", + summary: + "Andy inquired about the guest policy and whether guests could stay overnight. The assistant confirmed that guests are allowed to stay overnight, but extended stays would require approval.", + }, + { + date: "2024-07-28", + summary: + "Andy asked if any two-bedroom units have been recently renovated. The assistant provided details on newly renovated units and their availability.", + }, + ], + }, + "+15126507668": { + profile: { + firstName: "Sean", + lastName: "Bond", + phoneNumber: "+15126507668", + email: "sbond@twilio.com", + preferredApartmentType: "two-bedroom", + budget: 1800, + moveInDate: "2024-09-25", + petOwner: false, + tourPreference: "in-person", + }, + conversationHistory: [ + { + date: "2024-08-11", + summary: + "Sean asked about available two-bedroom apartments with balcony access. The assistant confirmed availability and provided details about units with private balconies.", + }, + { + date: "2024-08-09", + summary: + "Sean inquired about the security features in the building, particularly around entry and exit points. The assistant confirmed that the building has 24/7 security and a controlled access system.", + }, + { + date: "2024-08-07", + summary: + "Sean asked about the move-in process and whether any assistance would be provided with moving services. The assistant clarified that while moving services are not provided, there are several recommended local movers.", + }, + { + date: "2024-08-06", + summary: + "Sean asked if there were any discounts or offers available for signing a lease within the next week. The assistant informed Sean that there were no current promotions, but to check back frequently to ask for specials or discounts as there may be new offers.", + }, + { + date: "2024-08-04", + summary: + "Sean inquired about utility costs for two-bedroom apartments and asked if internet service was included. The assistant confirmed that Water, trash, gas, and Wi-Fi included, but that Sean would pay for electricity.", + }, + { + date: "2024-08-03", + summary: + "Sean asked if the building has electric vehicle (EV) charging stations. The assistant could not confirm whether any EV stations were nearby.", + }, + { + date: "2024-08-02", + summary: + "Sean asked if the apartment had a washer and dryer in-unit or if it was shared. The assistant confirmed that all units have in-unit laundry facilities.", + }, + { + date: "2024-08-01", + summary: + "Sean asked about public transportation options near the apartment complex. The assistant provided details about nearby bus and train stations.", + }, + { + date: "2024-07-30", + summary: + "Sean inquired about the guest policy and whether guests could stay overnight. The assistant confirmed that guests are allowed to stay overnight, but extended stays would require approval.", + }, + { + date: "2024-07-28", + summary: + "Sean asked if any two-bedroom units have been recently renovated. The assistant provided details on newly renovated units and their availability.", + }, + ], + }, + "+18323109635": { + profile: { + firstName: "Marian", + lastName: "Menschig", + phoneNumber: "+18323109635", + email: "mmenschig@twilio.com", + preferredApartmentType: "studio", + budget: 1500, + moveInDate: "2024-09-15", + petOwner: true, + tourPreference: "in-person", + }, + conversationHistory: [ + { + date: "2024-08-10", + summary: + "Marian inquired about available studio apartments and the pet policy. The assistant confirmed that cats and small dogs are allowed with a fee.", + }, + { + date: "2024-08-08", + summary: + "Marian asked about scheduling a self-guided tour for a one-bedroom apartment. The assistant offered available times, but Marian did not confirm.", + }, + { + date: "2024-08-07", + summary: + "Marian asked if the rent for the one-bedroom apartment could be negotiated. The assistant clarified that the listed prices are firm.", + }, + { + date: "2024-08-06", + summary: + "Marian asked if there is a daycare facility nearby. The assistant mentioned that there is no daycare service on-site and could not provide information about local options.", + }, + { + date: "2024-08-05", + summary: + "Marian asked about the utility fees for studio apartments. The assistant explained that water and trash are included, but electricity and internet are not.", + }, + { + date: "2024-08-04", + summary: + "Marian asked if there were any restrictions on hanging artwork or decorations in the apartment. The assistant did not have that information and advised Marian to check the lease agreement for decoration policies.", + }, + { + date: "2024-08-03", + summary: + "Marian asked if there were any promotions for signing a lease. The assistant explained that there were no current promotions for studio apartments.", + }, + { + date: "2024-08-02", + summary: + "Marian asked about the availability of two-bedroom apartments and mentioned a budget of $1,200. The assistant confirmed that two-bedroom apartments are outside the budget range.", + }, + { + date: "2024-08-01", + summary: + "Marian asked if the studio apartment included parking. The assistant confirmed that parking is available but comes with an additional fee.", + }, + { + date: "2024-07-31", + summary: + "Marian asked if Parkview Apartments offered a shuttle service to downtown. The assistant explained that Parkview does not provide any transportation services.", + }, + ], + }, +}; + +module.exports = customerProfiles; diff --git a/functions/available-functions.js b/functions/available-functions.js index 15a46165..1ace9a18 100644 --- a/functions/available-functions.js +++ b/functions/available-functions.js @@ -1,17 +1,33 @@ const mockDatabase = require("../data/mock-database"); const twilio = require("twilio"); -// Utility function to normalize the time format +// Utility function to normalize various time formats to the database's 12-hour AM/PM format function normalizeTimeFormat(time) { - const timeParts = time.split(":"); - let hour = parseInt(timeParts[0], 10); - const minutes = timeParts[1].slice(0, 2); - const period = hour >= 12 ? "PM" : "AM"; + // Check if time is already in the desired AM/PM format + if (/^(0?[1-9]|1[0-2]):[0-5][0-9] ?(AM|PM)$/i.test(time.trim())) { + return time.toUpperCase().trim(); // Return as-is if it's already correct + } + + // Handle 24-hour format (e.g., "14:00") + let [hour, minute] = time.split(":"); + minute = minute.replace(/[^0-9]/g, ""); // Clean any non-numeric characters from minutes + hour = parseInt(hour, 10); + + let period = "AM"; // Default to AM + + // Convert 24-hour to 12-hour format + if (hour >= 12) { + period = "PM"; + if (hour > 12) hour -= 12; + } else if (hour === 0) { + hour = 12; // Midnight is 12:00 AM + } - if (hour > 12) hour -= 12; - if (hour === 0) hour = 12; + // Pad minutes to ensure it's always two digits + minute = minute.padStart(2, "0"); - return `${hour}:${minutes} ${period}`; + // Return time in the database's expected format + return `${hour}:${minute} ${period}`; } // Function to handle live agent handoff diff --git a/prompts/prompt.js b/prompts/prompt.js index 609cbc0a..b624ea2d 100644 --- a/prompts/prompt.js +++ b/prompts/prompt.js @@ -21,11 +21,11 @@ Order of Operations: - Always check availability before scheduling a tour. - Ensure all required information is collected before proceeding with a function call. -### Schedule Tour: - - This function can only be called after confirming availability. +### Schedule Tour: + - This function should only run as a single tool call, never with other tools + - This function can only be called after confirming availability, but it should NEVER be called when the user asks for or confirms they'd like an SMS Confirmation. - Required data includes date, time, tour type (in-person or self-guided), and apartment type. - If any required details are missing, prompt the user to provide them. - - Do not offer to send any type of SMS or email confirmation until after the tour has been booked. ### Check Availability: - This function requires date, tour type, and apartment type. @@ -55,8 +55,9 @@ Order of Operations: - If any of these situations arise, automatically trigger the liveAgentHandoff tool call. ### SMS Confirmations: + - SMS confirmations should NEVER be coupled with function calls to 'scheduleTour'. - Only offer to send an SMS confirmation if the user has successfully scheduled a tour, and the user agrees to receive one. - - If the user agrees, trigger the tool call 'sendAppointmentConfirmationSms' with the appointment details and the user's phone number. + - If the user agrees, trigger the tool call 'sendAppointmentConfirmationSms' with the appointment details and the user's phone number, but do not trigger another 'scheduleTour' function call. - Do not ask for the user's phone number if you've already been referencing them by name during the conversation. Assume the phone number is already available to the function. ## Important Notes diff --git a/services/gpt-service-non-streaming.js b/services/gpt-service-non-streaming.js index f76d29e6..dfa1a8cd 100644 --- a/services/gpt-service-non-streaming.js +++ b/services/gpt-service-non-streaming.js @@ -260,7 +260,7 @@ class GptService extends EventEmitter { systemMessages.push({ role: "system", content: - "If the user agrees to receive an SMS confirmation, immediately trigger the 'sendAppointmentConfirmationSms' tool with the appointment details and the UserProfile. Do not ask for their phone number or any other details from the user.", + "If the user agrees to receive an SMS confirmation, immediately trigger the 'sendAppointmentConfirmationSms' tool with the appointment details and the UserProfile. Do not ask for their phone number or any other details from the user. Do not call the 'scheduleTour' function again.", }); } @@ -314,6 +314,15 @@ class GptService extends EventEmitter { ], }; + // // Log the payload to the console + // console.log( + // `[GptService] Completion payload: ${JSON.stringify( + // completion_payload, + // null, + // 2 + // )}` + // ); + // Call the API again to get the final response after tool processing const finalResponse = await this.openai.chat.completions.create({ model: completion_payload.model, diff --git a/services/gpt-service-streaming.js b/services/gpt-service-streaming.js index 2318d994..e10ee877 100644 --- a/services/gpt-service-streaming.js +++ b/services/gpt-service-streaming.js @@ -21,7 +21,7 @@ class GptService extends EventEmitter { this.openai = new OpenAI(); this.userContext = [ { role: "system", content: prompt }, - // Only do this if you're going to use the WelcomePrompt in VoxRay config + // //Only do this if you're going to use the WelcomePrompt in VoxRay config // { // role: "assistant", // content: `${welcomePrompt}`, @@ -184,10 +184,8 @@ class GptService extends EventEmitter { stream: true, // Always streaming }); - let toolCallId = null; // To store the ID of the tool call at index 0 - let toolCallFunctionName = ""; // To store the dynamically received function name - let argumentsAccumulator = ""; // To accumulate the 'arguments' data as chunks come in - let isToolCallActive = false; // To track when the tool call starts and finishes + let toolCalls = {}; // Object to accumulate multiple tool calls by their ID + let functionCallResults = []; // Array to accumulate function call results let contentAccumulator = ""; // To accumulate the 'content' before tool_calls let finalMessageObject = { role: "assistant", @@ -198,159 +196,164 @@ class GptService extends EventEmitter { let lastContentChunk = ""; // To store the last content chunk received let contentPending = false; // Flag to track if there's content pending to be emitted + let currentToolCallId = null; // To store the ID of the active tool call for await (const chunk of response) { const { choices } = chunk; - // // Log each chunk as it comes in + // Log each chunk as it comes in // this.log(`[GptService] Chunk received: ${JSON.stringify(chunk)}`); // Check if tool_calls are present in this chunk (could be part of multiple chunks) if (choices[0]?.delta?.tool_calls) { const toolCall = choices[0].delta.tool_calls[0]; - if (!isToolCallActive) { - // this.log(`[GptService] Tool Call is Active Logic`); - // Initialize tool call when detected - if (toolCall?.id) { - toolCallId = toolCall.id; - toolCallFunctionName = toolCall.function.name; // Capture dynamic function name - isToolCallActive = true; + // Check if this is a new tool call (only when an ID is present) + if (toolCall.id && toolCall.id !== currentToolCallId) { + currentToolCallId = toolCall.id; + + // Initialize new tool call if not already in the map + if (!toolCalls[currentToolCallId]) { + //Log the last content to an assistant message (IS THIS AN OPENAI BUG??? For some reason, the finish_reason never is "STOP" and we miss the final punctuation (eg. "One Moment" should be "One Moment." where the period is the final content, but that period is being sent back after the function call completes)) + if (contentAccumulator.length > 0) { + this.userContext.push({ + role: "assistant", + content: contentAccumulator.trim(), + }); + + this.log( + `[GptService] Final GPT -> user context length: ${this.userContext.length}` + ); + } - // // Log tool call detection - // this.log( - // `[GptService] Parsed tool call: ${JSON.stringify(toolCall)}` - // ); - // this.log( - // `[GptService] Tool call DETAILS 1: ${JSON.stringify( - // toolCall, - // null, - // 2 - // )}` - // ); + toolCalls[currentToolCallId] = { + id: currentToolCallId, + functionName: toolCall.function.name, + arguments: "", // Initialize an empty string for accumulating arguments + }; + + // Log tool call detection this.log( - `[GptService] Tool call detected: ${toolCall.function.name}` + `[GptService] Detected new tool call: ${toolCall.function.name}` ); - - // Set the content before the tool call starts - finalMessageObject.content = contentAccumulator.trim() || null; + // Log tool call detection + // this.log( + // `[GptService] Log the choices: ${JSON.stringify(choices[0])}` + // ); } } - - // Accumulate arguments as they come in across chunks - if (toolCall?.function?.arguments) { - argumentsAccumulator += toolCall.function.arguments; - } } // Separate block to handle when finish_reason is 'tool_calls' if (choices[0]?.finish_reason === "tool_calls") { - // this.log(`[GptService] Finish Reason is Tool Calls`); - - let parsedArguments; - try { - // Parse accumulated arguments - parsedArguments = JSON.parse(argumentsAccumulator); - - // Reset the accumulator for future tool calls - argumentsAccumulator = ""; - } catch (error) { - console.error("Error parsing arguments:", error); - parsedArguments = argumentsAccumulator; // Fallback in case of parsing failure - } - - // Finalize the tool_calls part of the message object - finalMessageObject.tool_calls.push({ - id: toolCallId, - type: "function", - function: { - name: toolCallFunctionName, - arguments: JSON.stringify(parsedArguments), // Ensure arguments are stringified - }, - }); - - // Now perform the tool logic as all tool_call data is ready - const functionToCall = availableFunctions[toolCallFunctionName]; + this.log(`[GptService] All tool calls have been completed`); + const systemMessages = []; + // Process each tool call in the accumulated toolCalls object + for (const toolCallId in toolCalls) { + const toolCall = toolCalls[toolCallId]; + let parsedArguments; + try { + // Parse accumulated arguments for this tool call + parsedArguments = JSON.parse(toolCall.arguments); + } catch (error) { + console.error("Error parsing arguments:", error); + parsedArguments = toolCall.arguments; // Fallback in case of parsing failure + } - this.log( - `[GptService] Calling function ${toolCallFunctionName} with arguments: ${JSON.stringify( - parsedArguments - )}` - ); + // Finalize the tool call in the final message object + finalMessageObject.tool_calls.push({ + id: toolCall.id, + type: "function", + function: { + name: toolCall.functionName, + arguments: JSON.stringify(parsedArguments), // Ensure arguments are stringified + }, + }); - // if (!dtmfTriggered) { - // // Emit TTS message related to the tool call - // const ttsMessage = this.getTtsMessageForTool(toolCallFunctionName); - // this.emit("gptreply", ttsMessage, true, interactionCount); // Emit the TTS message immediately - // } + // if (!dtmfTriggered) { + // // Emit TTS message related to the tool call + // const ttsMessage = this.getTtsMessageForTool(toolCallFunctionName); + // this.emit("gptreply", ttsMessage, true, interactionCount); // Emit the TTS message immediately + // } - // Inject phone numbers if it's the SMS function - if (toolCallFunctionName === "sendAppointmentConfirmationSms") { - const phoneNumbers = this.getPhoneNumbers(); - parsedArguments = { ...parsedArguments, ...phoneNumbers }; - } + // Inject phone numbers if it's the SMS function + if (toolCall.functionName === "sendAppointmentConfirmationSms") { + const phoneNumbers = this.getPhoneNumbers(); + parsedArguments = { ...parsedArguments, ...phoneNumbers }; + } + // Now perform the tool logic as all tool_call data is ready + const functionToCall = availableFunctions[toolCall.functionName]; - const functionResponse = await functionToCall(parsedArguments); + this.log( + `[GptService] Calling function ${ + toolCall.functionName + } with arguments: ${JSON.stringify(parsedArguments)}` + ); - let function_call_result_message = { - role: "tool", - content: JSON.stringify(functionResponse), - tool_call_id: toolCallId, - }; + // Call the respective function + const functionResponse = await functionToCall(parsedArguments); - // Check if specific tool calls require additional system messages - const systemMessages = []; - if (toolCallFunctionName === "listAvailableApartments") { - systemMessages.push({ - role: "system", - content: - "Do not use asterisks (*) under any circumstances in this response. Summarize the available apartments in a readable format.", + // Construct the function call result message for this tool call + functionCallResults.push({ + role: "tool", + content: JSON.stringify(functionResponse), + tool_call_id: toolCall.id, }); - } - // Personalize system messages based on user profile during relevant tool calls - if ( - toolCallFunctionName === "checkAvailability" && - this.userProfile - ) { - const { firstName, moveInDate } = this.userProfile.profile; - systemMessages.push({ - role: "system", - content: `When checking availability for ${firstName}, remember that they are looking to move in on ${moveInDate}.`, - }); - } + // Check if specific tool calls require additional system messages - if ( - toolCallFunctionName === "scheduleTour" && - functionResponse.available - ) { - // Inject a system message to ask about SMS confirmation - systemMessages.push({ - role: "system", - content: - "If the user agrees to receive an SMS confirmation, immediately trigger the 'sendAppointmentConfirmationSms' tool with the appointment details and the UserProfile. Do not ask for their phone number or any other details from the user.", - }); - } + if (toolCall.functionName === "listAvailableApartments") { + systemMessages.push({ + role: "system", + content: + "Provide a summary of available apartments. Do not you symbols, and do not use markdown in your response.", + }); + } - // Check if the tool call is for the 'liveAgentHandoff' function - if (toolCallFunctionName === "liveAgentHandoff") { - setTimeout(async () => { - const conversationSummary = await this.summarizeConversation(); + // Personalize system messages based on user profile during relevant tool calls + if ( + toolCall.functionName === "checkAvailability" && + this.userProfile + ) { + const { firstName, moveInDate } = this.userProfile.profile; + systemMessages.push({ + role: "system", + content: `When checking availability for ${firstName}, remember that they are looking to move in on ${moveInDate}.`, + }); + } - this.emit("endSession", { - reasonCode: "live-agent-handoff", - reason: functionResponse.reason, - conversationSummary: conversationSummary, + if ( + toolCall.functionName === "scheduleTour" && + functionResponse.available + ) { + // Inject a system message to ask about SMS confirmation + systemMessages.push({ + role: "system", + content: + "If the user agrees to receive an SMS confirmation, immediately trigger the 'sendAppointmentConfirmationSms' tool with the appointment details and the UserProfile. Do not ask for their phone number or any other details from the user.", }); + } + + // Check if the tool call is for the 'liveAgentHandoff' function + if (toolCall.functionName === "liveAgentHandoff") { + setTimeout(async () => { + const conversationSummary = await this.summarizeConversation(); + + this.emit("endSession", { + reasonCode: "live-agent-handoff", + reason: functionResponse.reason, + conversationSummary: conversationSummary, + }); + + this.log( + `[GptService] Emitting endSession event with reason: ${functionResponse.reason}` + ); + }, 3000); // 3-second delay this.log( `[GptService] Emitting endSession event with reason: ${functionResponse.reason}` ); - }, 3000); // 3-second delay - - this.log( - `[GptService] Emitting endSession event with reason: ${functionResponse.reason}` - ); + } } // Prepare the chat completion call payload with the tool result @@ -360,10 +363,19 @@ class GptService extends EventEmitter { ...this.userContext, ...systemMessages, // Inject dynamic system messages when relevant finalMessageObject, // the tool_call message - function_call_result_message, // The result of the tool call + ...functionCallResults, // The result of the tool call ], }; + // Log the payload to the console + // console.log( + // `[GptService] Completion payload: ${JSON.stringify( + // completion_payload, + // null, + // 2 + // )}` + // ); + // Call the API again with streaming for final response const finalResponseStream = await this.openai.chat.completions.create( { @@ -418,12 +430,20 @@ class GptService extends EventEmitter { break; // Exit the loop once the final response is complete } } - - // Reset tool call state - toolCallId = null; - toolCallFunctionName = ""; - isToolCallActive = false; - argumentsAccumulator = ""; + // Reset tool call state after completion + toolCalls = {}; // Clear all stored tool calls + currentToolCallId = null; // Reset tool call ID + } else { + // If the Finish Reason isn't "tool_calls", then accumulate arguments for the current tool call + if (currentToolCallId && toolCalls[currentToolCallId]) { + if (choices[0]?.delta?.tool_calls[0]?.function?.arguments) { + toolCalls[currentToolCallId].arguments += + choices[0].delta.tool_calls[0].function.arguments; + this.log( + `[GptService] Accumulated arguments for tool call ${currentToolCallId}: ${toolCalls[currentToolCallId].arguments}` + ); + } + } } // Handle non-tool_call content chunks @@ -443,7 +463,7 @@ class GptService extends EventEmitter { // Check if the current chunk is the last one in the stream if (choices[0].finish_reason === "stop") { - // this.log(`[GptService] In finish reason === STOP`); + this.log(`[GptService] In finish reason === STOP`); if (lastContentChunk) { this.emit("gptreply", lastContentChunk, true, interactionCount); From c1dfe89a26b303fc836099e7228eadff2f518545 Mon Sep 17 00:00:00 2001 From: Chris Feehan Date: Wed, 21 Aug 2024 22:17:31 -0500 Subject: [PATCH 11/26] Remove personalization.js from repository tracking --- data/personalization.js | 589 ---------------------------------------- 1 file changed, 589 deletions(-) delete mode 100644 data/personalization.js diff --git a/data/personalization.js b/data/personalization.js deleted file mode 100644 index bd9201c8..00000000 --- a/data/personalization.js +++ /dev/null @@ -1,589 +0,0 @@ -const customerProfiles = { - "+17632291691": { - profile: { - firstName: "Chris", - lastName: "Feehan", - phoneNumber: "+17632291691", - email: "cfeehan@twilio.com", - preferredApartmentType: "studio", - budget: 1500, - moveInDate: "2024-09-15", - petOwner: false, - tourPreference: "self-guided", - }, - conversationHistory: [ - { - date: "2024-08-10", - summary: - "Chris inquired about available studio apartments and the pet policy. The assistant confirmed that cats and small dogs are allowed with a fee.", - }, - { - date: "2024-08-08", - summary: - "Chris asked about scheduling a self-guided tour for a one-bedroom apartment. The assistant offered available times, but Chris did not confirm.", - }, - { - date: "2024-08-07", - summary: - "Chris asked if the rent for the one-bedroom apartment could be negotiated. The assistant clarified that the listed prices are firm.", - }, - { - date: "2024-08-06", - summary: - "Chris asked if there is a daycare facility nearby. The assistant mentioned that there is no daycare service on-site and could not provide information about local options.", - }, - { - date: "2024-08-05", - summary: - "Chris asked about the utility fees for studio apartments. The assistant explained that water and trash are included, but electricity and internet are not.", - }, - { - date: "2024-08-04", - summary: - "Chris asked if there were any restrictions on hanging artwork or decorations in the apartment. The assistant did not have that information and advised Chris to check the lease agreement for decoration policies.", - }, - { - date: "2024-08-03", - summary: - "Chris asked if there were any promotions for signing a lease. The assistant explained that there were no current promotions for studio apartments.", - }, - { - date: "2024-08-02", - summary: - "Chris asked about the availability of two-bedroom apartments and mentioned a budget of $1,200. The assistant confirmed that two-bedroom apartments are outside the budget range.", - }, - { - date: "2024-08-01", - summary: - "Chris asked if the studio apartment included parking. The assistant confirmed that parking is available but comes with an additional fee.", - }, - { - date: "2024-07-31", - summary: - "Chris asked if Parkview Apartments offered a shuttle service to downtown. The assistant explained that Parkview does not provide any transportation services.", - }, - ], - }, - "+16672074063": { - profile: { - firstName: "Kunal", - lastName: "Maiti", - phoneNumber: "+16672074063", - email: "kmaiti@twilio.com", - preferredApartmentType: "studio", - budget: 1500, - moveInDate: "2024-09-15", - petOwner: false, - tourPreference: "self-guided", - }, - conversationHistory: [ - { - date: "2024-08-10", - summary: - "Kunal inquired about available studio apartments and the pet policy. The assistant confirmed that cats and small dogs are allowed with a fee.", - }, - { - date: "2024-08-08", - summary: - "Kunal asked about scheduling a self-guided tour for a one-bedroom apartment. The assistant offered available times, but Kunal did not confirm.", - }, - { - date: "2024-08-07", - summary: - "Kunal asked if the rent for the one-bedroom apartment could be negotiated. The assistant clarified that the listed prices are firm.", - }, - { - date: "2024-08-06", - summary: - "Kunal asked if there is a daycare facility nearby. The assistant mentioned that there is no daycare service on-site and could not provide information about local options.", - }, - { - date: "2024-08-05", - summary: - "Kunal asked about the utility fees for studio apartments. The assistant explained that water and trash are included, but electricity and internet are not.", - }, - { - date: "2024-08-04", - summary: - "Kunal asked if there were any restrictions on hanging artwork or decorations in the apartment. The assistant did not have that information and advised Kunal to check the lease agreement for decoration policies.", - }, - { - date: "2024-08-03", - summary: - "Kunal asked if there were any promotions for signing a lease. The assistant explained that there were no current promotions for studio apartments.", - }, - { - date: "2024-08-02", - summary: - "Kunal asked about the availability of two-bedroom apartments and mentioned a budget of $1,200. The assistant confirmed that two-bedroom apartments are outside the budget range.", - }, - { - date: "2024-08-01", - summary: - "Kunal asked if the studio apartment included parking. The assistant confirmed that parking is available but comes with an additional fee.", - }, - { - date: "2024-07-31", - summary: - "Kunal asked if Parkview Apartments offered a shuttle service to downtown. The assistant explained that Parkview does not provide any transportation services.", - }, - ], - }, - "+12023095963": { - profile: { - firstName: "Alex", - lastName: "Millet", - phoneNumber: "+12023095963", - email: "amillet@twilio.com", - preferredApartmentType: "studio", - budget: 1500, - moveInDate: "2024-09-15", - petOwner: false, - tourPreference: "self-guided", - }, - conversationHistory: [ - { - date: "2024-08-10", - summary: - "Alex inquired about available studio apartments and the pet policy. The assistant confirmed that cats and small dogs are allowed with a fee.", - }, - { - date: "2024-08-08", - summary: - "Alex asked about scheduling a self-guided tour for a one-bedroom apartment. The assistant offered available times, but Alex did not confirm.", - }, - { - date: "2024-08-07", - summary: - "Alex asked if the rent for the one-bedroom apartment could be negotiated. The assistant clarified that the listed prices are firm.", - }, - { - date: "2024-08-06", - summary: - "Alex asked if there is a daycare facility nearby. The assistant mentioned that there is no daycare service on-site and could not provide information about local options.", - }, - { - date: "2024-08-05", - summary: - "Alex asked about the utility fees for studio apartments. The assistant explained that water and trash are included, but electricity and internet are not.", - }, - { - date: "2024-08-04", - summary: - "Alex asked if there were any restrictions on hanging artwork or decorations in the apartment. The assistant did not have that information and advised Alex to check the lease agreement for decoration policies.", - }, - { - date: "2024-08-03", - summary: - "Alex asked if there were any promotions for signing a lease. The assistant explained that there were no current promotions for studio apartments.", - }, - { - date: "2024-08-02", - summary: - "Alex asked about the availability of two-bedroom apartments and mentioned a budget of $1,200. The assistant confirmed that two-bedroom apartments are outside the budget range.", - }, - { - date: "2024-08-01", - summary: - "Alex asked if the studio apartment included parking. The assistant confirmed that parking is available but comes with an additional fee.", - }, - { - date: "2024-07-31", - summary: - "Alex asked if Parkview Apartments offered a shuttle service to downtown. The assistant explained that Parkview does not provide any transportation services.", - }, - ], - }, - "+14695251496": { - profile: { - firstName: "Alberto", - lastName: "Montilla", - phoneNumber: "+14695251496", - email: "amontilla@twilio.com", - preferredApartmentType: "studio", - budget: 1500, - moveInDate: "2024-09-15", - petOwner: false, - tourPreference: "self-guided", - }, - conversationHistory: [ - { - date: "2024-08-10", - summary: - "Alberto inquired about available studio apartments and the pet policy. The assistant confirmed that cats and small dogs are allowed with a fee.", - }, - { - date: "2024-08-08", - summary: - "Alberto asked about scheduling a self-guided tour for a one-bedroom apartment. The assistant offered available times, but Alberto did not confirm.", - }, - { - date: "2024-08-07", - summary: - "Alberto asked if the rent for the one-bedroom apartment could be negotiated. The assistant clarified that the listed prices are firm.", - }, - { - date: "2024-08-06", - summary: - "Alberto asked if there is a daycare facility nearby. The assistant mentioned that there is no daycare service on-site and could not provide information about local options.", - }, - { - date: "2024-08-05", - summary: - "Alberto asked about the utility fees for studio apartments. The assistant explained that water and trash are included, but electricity and internet are not.", - }, - { - date: "2024-08-04", - summary: - "Alberto asked if there were any restrictions on hanging artwork or decorations in the apartment. The assistant did not have that information and advised Alberto to check the lease agreement for decoration policies.", - }, - { - date: "2024-08-03", - summary: - "Alberto asked if there were any promotions for signing a lease. The assistant explained that there were no current promotions for studio apartments.", - }, - { - date: "2024-08-02", - summary: - "Alberto asked about the availability of two-bedroom apartments and mentioned a budget of $1,200. The assistant confirmed that two-bedroom apartments are outside the budget range.", - }, - { - date: "2024-08-01", - summary: - "Alberto asked if the studio apartment included parking. The assistant confirmed that parking is available but comes with an additional fee.", - }, - { - date: "2024-07-31", - summary: - "Alberto asked if Parkview Apartments offered a shuttle service to downtown. The assistant explained that Parkview does not provide any transportation services.", - }, - ], - }, - "+18477570040": { - profile: { - firstName: "Jeff", - lastName: "Eiden", - phoneNumber: "+18477570040", - email: "jeiden@twilio.com", - preferredApartmentType: "studio", - budget: 1500, - moveInDate: "2024-09-15", - petOwner: false, - tourPreference: "self-guided", - }, - conversationHistory: [ - { - date: "2024-08-10", - summary: - "Jeff inquired about available studio apartments and the pet policy. The assistant confirmed that cats and small dogs are allowed with a fee.", - }, - { - date: "2024-08-08", - summary: - "Jeff asked about scheduling a self-guided tour for a one-bedroom apartment. The assistant offered available times, but Jeff did not confirm.", - }, - { - date: "2024-08-07", - summary: - "Jeff asked if the rent for the one-bedroom apartment could be negotiated. The assistant clarified that the listed prices are firm.", - }, - { - date: "2024-08-06", - summary: - "Jeff asked if there is a daycare facility nearby. The assistant mentioned that there is no daycare service on-site and could not provide information about local options.", - }, - { - date: "2024-08-05", - summary: - "Jeff asked about the utility fees for studio apartments. The assistant explained that water and trash are included, but electricity and internet are not.", - }, - { - date: "2024-08-04", - summary: - "Jeff asked if there were any restrictions on hanging artwork or decorations in the apartment. The assistant did not have that information and advised Jeff to check the lease agreement for decoration policies.", - }, - { - date: "2024-08-03", - summary: - "Jeff asked if there were any promotions for signing a lease. The assistant explained that there were no current promotions for studio apartments.", - }, - { - date: "2024-08-02", - summary: - "Jeff asked about the availability of two-bedroom apartments and mentioned a budget of $1,200. The assistant confirmed that two-bedroom apartments are outside the budget range.", - }, - { - date: "2024-08-01", - summary: - "Jeff asked if the studio apartment included parking. The assistant confirmed that parking is available but comes with an additional fee.", - }, - { - date: "2024-07-31", - summary: - "Jeff asked if Parkview Apartments offered a shuttle service to downtown. The assistant explained that Parkview does not provide any transportation services.", - }, - ], - }, - "+16503800236": { - profile: { - firstName: "Austin", - lastName: "Park", - phoneNumber: "+16503800236", - email: "apark@twilio.com", - preferredApartmentType: "two-bedroom", - budget: 2200, - moveInDate: "2024-10-01", - petOwner: true, - tourPreference: "in-person", - }, - conversationHistory: [ - { - date: "2024-08-12", - summary: - "Austin inquired about two-bedroom apartments and asked for the earliest move-in date. The assistant confirmed that the earliest move-in date is September 15, 2024.", - }, - { - date: "2024-08-10", - summary: - "Austin asked about the availability of parking spots for two-bedroom apartments. The assistant confirmed that each apartment comes with one reserved parking spot and additional parking is available for a fee.", - }, - { - date: "2024-08-08", - summary: - "Austin inquired about the pet policy, particularly if there are restrictions on dog breeds. The assistant confirmed that cats and small dogs are allowed with a fee, but large dogs may not be permitted.", - }, - { - date: "2024-08-07", - summary: - "Austin asked if utilities are included for the two-bedroom apartments. The assistant explained that water, trash, and Wi-Fi are included, but electricity and gas are the tenant's responsibility.", - }, - { - date: "2024-08-05", - summary: - "Austin asked about nearby parks for walking his dog. The assistant confirmed that there are a few parks within walking distance, but had no specific recommendations.", - }, - { - date: "2024-08-03", - summary: - "Austin asked if there are any current move-in specials for two-bedroom apartments. The assistant confirmed that there are no promotions available at this time.", - }, - { - date: "2024-08-02", - summary: - "Austin asked about the availability of two-bedroom apartments with hardwood floors. The assistant confirmed that all available two-bedroom apartments have hardwood floors in the living areas.", - }, - { - date: "2024-08-01", - summary: - "Austin inquired about the security deposit for the two-bedroom apartments. The assistant confirmed that the security deposit is $300.", - }, - { - date: "2024-07-30", - summary: - "Austin asked if there is a fitness center available on-site. The assistant confirmed that there is a small gym available for residents with no additional fee.", - }, - { - date: "2024-07-28", - summary: - "Austin asked if the apartment complex offers a concierge service. The assistant clarified that there is no concierge service available.", - }, - ], - }, - "+13035130469": { - profile: { - firstName: "Andy", - lastName: "O'Dower", - phoneNumber: "+13035130469", - email: "aodower@twilio.com@twilio.com", - preferredApartmentType: "two-bedroom", - budget: 1800, - moveInDate: "2024-09-25", - petOwner: false, - tourPreference: "in-person", - }, - conversationHistory: [ - { - date: "2024-08-11", - summary: - "Andy asked about available two-bedroom apartments with balcony access. The assistant confirmed availability and provided details about units with private balconies.", - }, - { - date: "2024-08-09", - summary: - "Andy inquired about the security features in the building, particularly around entry and exit points. The assistant confirmed that the building has 24/7 security and a controlled access system.", - }, - { - date: "2024-08-07", - summary: - "Andy asked about the move-in process and whether any assistance would be provided with moving services. The assistant clarified that while moving services are not provided, there are several recommended local movers.", - }, - { - date: "2024-08-06", - summary: - "Andy asked if there were any discounts or offers available for signing a lease within the next week. The assistant informed Andy that there were no current promotions, but to check back frequently to ask for specials or discounts as there may be new offers.", - }, - { - date: "2024-08-04", - summary: - "Andy inquired about utility costs for two-bedroom apartments and asked if internet service was included. The assistant confirmed that Water, trash, gas, and Wi-Fi included, but that Andy would pay for electricity.", - }, - { - date: "2024-08-03", - summary: - "Andy asked if the building has electric vehicle (EV) charging stations. The assistant could not confirm whether any EV stations were nearby.", - }, - { - date: "2024-08-02", - summary: - "Andy asked if the apartment had a washer and dryer in-unit or if it was shared. The assistant confirmed that all units have in-unit laundry facilities.", - }, - { - date: "2024-08-01", - summary: - "Andy asked about public transportation options near the apartment complex. The assistant provided details about nearby bus and train stations.", - }, - { - date: "2024-07-30", - summary: - "Andy inquired about the guest policy and whether guests could stay overnight. The assistant confirmed that guests are allowed to stay overnight, but extended stays would require approval.", - }, - { - date: "2024-07-28", - summary: - "Andy asked if any two-bedroom units have been recently renovated. The assistant provided details on newly renovated units and their availability.", - }, - ], - }, - "+15126507668": { - profile: { - firstName: "Sean", - lastName: "Bond", - phoneNumber: "+15126507668", - email: "sbond@twilio.com", - preferredApartmentType: "two-bedroom", - budget: 1800, - moveInDate: "2024-09-25", - petOwner: false, - tourPreference: "in-person", - }, - conversationHistory: [ - { - date: "2024-08-11", - summary: - "Sean asked about available two-bedroom apartments with balcony access. The assistant confirmed availability and provided details about units with private balconies.", - }, - { - date: "2024-08-09", - summary: - "Sean inquired about the security features in the building, particularly around entry and exit points. The assistant confirmed that the building has 24/7 security and a controlled access system.", - }, - { - date: "2024-08-07", - summary: - "Sean asked about the move-in process and whether any assistance would be provided with moving services. The assistant clarified that while moving services are not provided, there are several recommended local movers.", - }, - { - date: "2024-08-06", - summary: - "Sean asked if there were any discounts or offers available for signing a lease within the next week. The assistant informed Sean that there were no current promotions, but to check back frequently to ask for specials or discounts as there may be new offers.", - }, - { - date: "2024-08-04", - summary: - "Sean inquired about utility costs for two-bedroom apartments and asked if internet service was included. The assistant confirmed that Water, trash, gas, and Wi-Fi included, but that Sean would pay for electricity.", - }, - { - date: "2024-08-03", - summary: - "Sean asked if the building has electric vehicle (EV) charging stations. The assistant could not confirm whether any EV stations were nearby.", - }, - { - date: "2024-08-02", - summary: - "Sean asked if the apartment had a washer and dryer in-unit or if it was shared. The assistant confirmed that all units have in-unit laundry facilities.", - }, - { - date: "2024-08-01", - summary: - "Sean asked about public transportation options near the apartment complex. The assistant provided details about nearby bus and train stations.", - }, - { - date: "2024-07-30", - summary: - "Sean inquired about the guest policy and whether guests could stay overnight. The assistant confirmed that guests are allowed to stay overnight, but extended stays would require approval.", - }, - { - date: "2024-07-28", - summary: - "Sean asked if any two-bedroom units have been recently renovated. The assistant provided details on newly renovated units and their availability.", - }, - ], - }, - "+18323109635": { - profile: { - firstName: "Marian", - lastName: "Menschig", - phoneNumber: "+18323109635", - email: "mmenschig@twilio.com", - preferredApartmentType: "studio", - budget: 1500, - moveInDate: "2024-09-15", - petOwner: true, - tourPreference: "in-person", - }, - conversationHistory: [ - { - date: "2024-08-10", - summary: - "Marian inquired about available studio apartments and the pet policy. The assistant confirmed that cats and small dogs are allowed with a fee.", - }, - { - date: "2024-08-08", - summary: - "Marian asked about scheduling a self-guided tour for a one-bedroom apartment. The assistant offered available times, but Marian did not confirm.", - }, - { - date: "2024-08-07", - summary: - "Marian asked if the rent for the one-bedroom apartment could be negotiated. The assistant clarified that the listed prices are firm.", - }, - { - date: "2024-08-06", - summary: - "Marian asked if there is a daycare facility nearby. The assistant mentioned that there is no daycare service on-site and could not provide information about local options.", - }, - { - date: "2024-08-05", - summary: - "Marian asked about the utility fees for studio apartments. The assistant explained that water and trash are included, but electricity and internet are not.", - }, - { - date: "2024-08-04", - summary: - "Marian asked if there were any restrictions on hanging artwork or decorations in the apartment. The assistant did not have that information and advised Marian to check the lease agreement for decoration policies.", - }, - { - date: "2024-08-03", - summary: - "Marian asked if there were any promotions for signing a lease. The assistant explained that there were no current promotions for studio apartments.", - }, - { - date: "2024-08-02", - summary: - "Marian asked about the availability of two-bedroom apartments and mentioned a budget of $1,200. The assistant confirmed that two-bedroom apartments are outside the budget range.", - }, - { - date: "2024-08-01", - summary: - "Marian asked if the studio apartment included parking. The assistant confirmed that parking is available but comes with an additional fee.", - }, - { - date: "2024-07-31", - summary: - "Marian asked if Parkview Apartments offered a shuttle service to downtown. The assistant explained that Parkview does not provide any transportation services.", - }, - ], - }, -}; - -module.exports = customerProfiles; From 9edb8a27ae67242da17e184ba7488ccf4c58eaa1 Mon Sep 17 00:00:00 2001 From: Chris Feehan Date: Wed, 21 Aug 2024 22:23:13 -0500 Subject: [PATCH 12/26] Cleanse repo for Voxray-only code --- .gitignore | 4 + app.js | 2 +- functions/checkInventory.js | 14 -- functions/checkPrice.js | 13 -- functions/placeOrder.js | 17 -- functions/transferCall.js | 20 -- name_generator.js | 90 --------- public/index.html | 62 ------ public/quickstart.js | 310 ----------------------------- public/site.css | 124 ------------ public/twilio.min.js | 1 - scripts/inbound-call.js | 30 --- scripts/outbound-call.js | 23 --- services/gpt-service-mixed-mode.js | 250 ----------------------- services/recording-service.js | 23 --- services/token-generator.js | 72 ------- test/checkInventory.test.js | 13 -- test/checkPrice.test.js | 13 -- test/placeOrder.test.js | 8 - test/transferCall.test.js | 31 --- 20 files changed, 5 insertions(+), 1115 deletions(-) delete mode 100644 functions/checkInventory.js delete mode 100644 functions/checkPrice.js delete mode 100644 functions/placeOrder.js delete mode 100644 functions/transferCall.js delete mode 100644 name_generator.js delete mode 100644 public/index.html delete mode 100644 public/quickstart.js delete mode 100644 public/site.css delete mode 100644 public/twilio.min.js delete mode 100644 scripts/inbound-call.js delete mode 100644 scripts/outbound-call.js delete mode 100644 services/gpt-service-mixed-mode.js delete mode 100644 services/recording-service.js delete mode 100644 services/token-generator.js delete mode 100644 test/checkInventory.test.js delete mode 100644 test/checkPrice.test.js delete mode 100644 test/placeOrder.test.js delete mode 100644 test/transferCall.test.js diff --git a/.gitignore b/.gitignore index fdf76555..962f8473 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,10 @@ # Edit at https://www.toptal.com/developers/gitignore?templates=node ### Node ### + +#personalization +data/personalization.js + # Logs logs *.log diff --git a/app.js b/app.js index dfde10ae..4f5d121b 100644 --- a/app.js +++ b/app.js @@ -5,7 +5,7 @@ const express = require("express"); const ExpressWs = require("express-ws"); //const { GptService } = require("./services/gpt-service-streaming"); -const { GptService } = require("./services/gpt-service-streaming"); +const { GptService } = require("./services/gpt-service-non-streaming"); const { TextService } = require("./services/text-service"); const { EndSessionService } = require("./services/end-session-service"); diff --git a/functions/checkInventory.js b/functions/checkInventory.js deleted file mode 100644 index bb39161e..00000000 --- a/functions/checkInventory.js +++ /dev/null @@ -1,14 +0,0 @@ -async function checkInventory(functionArgs) { - const model = functionArgs.model; - console.log('GPT -> called checkInventory function'); - - if (model?.toLowerCase().includes('pro')) { - return JSON.stringify({ stock: 10 }); - } else if (model?.toLowerCase().includes('max')) { - return JSON.stringify({ stock: 0 }); - } else { - return JSON.stringify({ stock: 100 }); - } -} - -module.exports = checkInventory; \ No newline at end of file diff --git a/functions/checkPrice.js b/functions/checkPrice.js deleted file mode 100644 index ee2ce124..00000000 --- a/functions/checkPrice.js +++ /dev/null @@ -1,13 +0,0 @@ -async function checkPrice(functionArgs) { - let model = functionArgs.model; - console.log('GPT -> called checkPrice function'); - if (model?.toLowerCase().includes('pro')) { - return JSON.stringify({ price: 249 }); - } else if (model?.toLowerCase().includes('max')) { - return JSON.stringify({ price: 549 }); - } else { - return JSON.stringify({ price: 149 }); - } -} - -module.exports = checkPrice; \ No newline at end of file diff --git a/functions/placeOrder.js b/functions/placeOrder.js deleted file mode 100644 index 428325d6..00000000 --- a/functions/placeOrder.js +++ /dev/null @@ -1,17 +0,0 @@ -async function placeOrder(functionArgs) { - const {model, quantity} = functionArgs; - console.log('GPT -> called placeOrder function'); - - // generate a random order number that is 7 digits - const orderNum = Math.floor(Math.random() * (9999999 - 1000000 + 1) + 1000000); - - // check model and return the order number and price with 7.9% sales tax - if (model?.toLowerCase().includes('pro')) { - return JSON.stringify({ orderNumber: orderNum, price: Math.floor(quantity * 249 * 1.079)}); - } else if (model?.toLowerCase().includes('max')) { - return JSON.stringify({ orderNumber: orderNum, price: Math.floor(quantity * 549 * 1.079) }); - } - return JSON.stringify({ orderNumber: orderNum, price: Math.floor(quantity * 179 * 1.079) }); -} - -module.exports = placeOrder; \ No newline at end of file diff --git a/functions/transferCall.js b/functions/transferCall.js deleted file mode 100644 index 383f4c53..00000000 --- a/functions/transferCall.js +++ /dev/null @@ -1,20 +0,0 @@ -require('dotenv').config(); - -const transferCall = async function (call) { - - console.log('Transferring call', call.callSid); - const accountSid = process.env.TWILIO_ACCOUNT_SID; - const authToken = process.env.TWILIO_AUTH_TOKEN; - const client = require('twilio')(accountSid, authToken); - - return await client.calls(call.callSid) - .update({twiml: `${process.env.TRANSFER_NUMBER}`}) - .then(() => { - return 'The call was transferred successfully, say goodbye to the customer.'; - }) - .catch(() => { - return 'The call was not transferred successfully, advise customer to call back later.'; - }); -}; - -module.exports = transferCall; \ No newline at end of file diff --git a/name_generator.js b/name_generator.js deleted file mode 100644 index 30682f16..00000000 --- a/name_generator.js +++ /dev/null @@ -1,90 +0,0 @@ -const ADJECTIVES = [ - "Awesome", - "Bold", - "Creative", - "Dapper", - "Eccentric", - "Fiesty", - "Golden", - "Holy", - "Ignominious", - "Jolly", - "Kindly", - "Lucky", - "Mushy", - "Natural", - "Oaken", - "Precise", - "Quiet", - "Rowdy", - "Sunny", - "Tall", - "Unique", - "Vivid", - "Wonderful", - "Xtra", - "Yawning", - "Zesty", -]; - -const FIRST_NAMES = [ - "Anna", - "Bobby", - "Cameron", - "Danny", - "Emmett", - "Frida", - "Gracie", - "Hannah", - "Isaac", - "Jenova", - "Kendra", - "Lando", - "Mufasa", - "Nate", - "Owen", - "Penny", - "Quincy", - "Roddy", - "Samantha", - "Tammy", - "Ulysses", - "Victoria", - "Wendy", - "Xander", - "Yolanda", - "Zelda", -]; - -const LAST_NAMES = [ - "Anchorage", - "Berlin", - "Cucamonga", - "Davenport", - "Essex", - "Fresno", - "Gunsight", - "Hanover", - "Indianapolis", - "Jamestown", - "Kane", - "Liberty", - "Minneapolis", - "Nevis", - "Oakland", - "Portland", - "Quantico", - "Raleigh", - "SaintPaul", - "Tulsa", - "Utica", - "Vail", - "Warsaw", - "XiaoJin", - "Yale", - "Zimmerman", -]; - -const rand = (arr) => arr[Math.floor(Math.random() * arr.length)]; - -module.exports = () => rand(ADJECTIVES) + rand(FIRST_NAMES) + rand(LAST_NAMES); diff --git a/public/index.html b/public/index.html deleted file mode 100644 index 291e198d..00000000 --- a/public/index.html +++ /dev/null @@ -1,62 +0,0 @@ - - - - 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/public/quickstart.js b/public/quickstart.js deleted file mode 100644 index 2c0d7053..00000000 --- a/public/quickstart.js +++ /dev/null @@ -1,310 +0,0 @@ -$(function () { - 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 outgoingCallHangupButton = document.getElementById("button-hangup-outgoing"); - 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"); - - let device; - let token; - - // Event Listeners - - callButton.onclick = (e) => { - e.preventDefault(); - makeOutgoingCall(); - }; - getAudioDevicesButton.onclick = getAudioDevices; - speakerDevices.addEventListener("change", updateOutputDevice); - ringtoneDevices.addEventListener("change", updateRingtoneDevice); - - - // 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); - - // 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."); - } - } - - // SETUP STEP 3: - // Instantiate a new Twilio.Device - function intitializeDevice() { - logDiv.classList.remove("hide"); - log("Initializing device"); - device = new Twilio.Device(token, { - logLevel:1, - // Set Opus as our preferred codec. Opus generally performs better, requiring less bandwidth and - // providing better audio quality in restrained network conditions. - chunderw: "voice-js.ashburn.stage.twilio.com", - codecPreferences: ["opus", "pcmu"], - }); - - 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"); - }); - - 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 = { - // get the phone number to call from the DOM - To: phoneNumberInput.value, - }; - - if (device) { - log(`Attempting to call ${params.To} ...`); - - // 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.on("accept", updateUIAcceptedOutgoingCall); - call.on("disconnect", updateUIDisconnectedOutgoingCall); - call.on("cancel", updateUIDisconnectedOutgoingCall); - - outgoingCallHangupButton.onclick = () => { - log("Hanging up ..."); - call.disconnect(); - }; - - } else { - log("Unable to make call."); - } - } - - function updateUIAcceptedOutgoingCall(call) { - log("Call in progress ..."); - callButton.disabled = true; - outgoingCallHangupButton.classList.remove("hide"); - volumeIndicators.classList.remove("hide"); - bindVolumeIndicators(call); - } - - function updateUIDisconnectedOutgoingCall() { - log("Call disconnected."); - callButton.disabled = false; - outgoingCallHangupButton.classList.add("hide"); - volumeIndicators.classList.add("hide"); - } - - // HANDLE INCOMING CALL - - 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.on("cancel", handleDisconnectedIncomingCall); - call.on("disconnect", handleDisconnectedIncomingCall); - call.on("reject", 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(call) { - call.disconnect(); - log("Hanging up incoming call"); - resetIncomingCallUI(); - } - - // HANDLE CANCELLED INCOMING CALL - - function handleDisconnectedIncomingCall() { - 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}`; - - var input = document.getElementById("phone-number"); - input.value = clientName; - } - - function resetIncomingCallUI() { - incomingPhoneNumberEl.innerHTML = ""; - incomingCallAcceptButton.classList.remove("hide"); - incomingCallRejectButton.classList.remove("hide"); - incomingCallHangupButton.classList.add("hide"); - 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); - } - - 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(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.background = inputColor; - - 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.background = outputColor; - }); - } - - // 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; - } - }); - - var option = document.createElement("option"); - option.label = device.label; - option.setAttribute("data-id", id); - if (isActive) { - option.setAttribute("selected", "selected"); - } - selectEl.appendChild(option); - }); - } -}); diff --git a/public/site.css b/public/site.css deleted file mode 100644 index 4d9341f8..00000000 --- a/public/site.css +++ /dev/null @@ -1,124 +0,0 @@ -@import url(https://fonts.googleapis.com/css?family=Share+Tech+Mono); - -body, -p { - padding: 0; - margin: auto; - font-family: Arial, Helvetica, sans-serif; -} - -h1 { - text-align: center; -} - -h2 { - margin-top: 0; - border-bottom: 1px solid black; -} - -button { - margin-bottom: 10px; -} - -label { - text-align: left; - font-size: 1.25em; - color: #777776; - display: block; -} - -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; -} - -#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/public/twilio.min.js b/public/twilio.min.js deleted file mode 100644 index 73b15721..00000000 --- a/public/twilio.min.js +++ /dev/null @@ -1 +0,0 @@ -(function(root){var bundle=function(){function r(e,n,t){function o(i,f){if(!n[i]){if(!e[i]){var c="function"==typeof require&&require;if(!f&&c)return c(i,!0);if(u)return u(i,!0);var a=new Error("Cannot find module '"+i+"'");throw a.code="MODULE_NOT_FOUND",a}var p=n[i]={exports:{}};e[i][0].call(p.exports,function(r){var n=e[i][1][r];return o(n||r)},p,p.exports,r,e,n,t)}return n[i].exports}for(var u="function"==typeof require&&require,i=0;i0&&t[t.length-1])&&(op[0]===6||op[0]===2)){_=0;continue}if(op[0]===3&&(!t||op[1]>t[0]&&op[1]BACKOFF_CONFIG.maxDelay){_this._log.info("Exceeded max ICE retries");return _this._mediaHandler.onerror(MEDIA_DISCONNECT_ERROR)}try{_this._mediaReconnectBackoff.backoff()}catch(error){if(!(error.message&&error.message==="Backoff in progress.")){throw error}}}return}var pc=_this._mediaHandler.version.pc;var isIceDisconnected=pc&&pc.iceConnectionState==="disconnected";var hasLowBytesWarning=_this._monitor.hasActiveWarning("bytesSent","min")||_this._monitor.hasActiveWarning("bytesReceived","min");if(type===LowBytes&&isIceDisconnected||type===ConnectionDisconnected&&hasLowBytesWarning||isEndOfIceCycle){var mediaReconnectionError=new errors_1.MediaErrors.ConnectionError("Media connection failed.");_this._log.warn("ICE Connection disconnected.");_this._publisher.warn("connection","error",mediaReconnectionError,_this);_this._publisher.info("connection","reconnecting",null,_this);_this._mediaReconnectStartTime=Date.now();_this._status=Call.State.Reconnecting;_this._mediaStatus=Call.State.Reconnecting;_this._mediaReconnectBackoff.reset();_this._mediaReconnectBackoff.backoff();_this.emit("reconnecting",mediaReconnectionError)}};_this._onMediaReconnected=function(){if(_this._mediaStatus!==Call.State.Reconnecting){return}_this._log.info("ICE Connection reestablished.");_this._mediaStatus=Call.State.Open;if(_this._signalingStatus===Call.State.Open){_this._publisher.info("connection","reconnected",null,_this);_this.emit("reconnected");_this._status=Call.State.Open}};_this._onMessageReceived=function(payload){var callsid=payload.callsid,content=payload.content,contenttype=payload.contenttype,messagetype=payload.messagetype,voiceeventsid=payload.voiceeventsid;if(_this.parameters.CallSid!==callsid){_this._log.warn("Received a message from a different callsid: "+callsid);return}_this.emit("messageReceived",{content:content,contentType:contenttype,messageType:messagetype,voiceEventSid:voiceeventsid})};_this._onMessageSent=function(voiceEventSid){if(!_this._messages.has(voiceEventSid)){_this._log.warn("Received a messageSent with a voiceEventSid that doesn't exists: "+voiceEventSid);return}var message=_this._messages.get(voiceEventSid);_this._messages.delete(voiceEventSid);_this.emit("messageSent",message)};_this._onRinging=function(payload){_this._setCallSid(payload);if(_this._status!==Call.State.Connecting&&_this._status!==Call.State.Ringing){return}var hasEarlyMedia=!!payload.sdp;_this._status=Call.State.Ringing;_this._publisher.info("connection","outgoing-ringing",{hasEarlyMedia:hasEarlyMedia},_this);_this.emit("ringing",hasEarlyMedia)};_this._onRTCSample=function(sample){var callMetrics=__assign(__assign({},sample),{inputVolume:_this._latestInputVolume,outputVolume:_this._latestOutputVolume});_this._codec=callMetrics.codecName;_this._metricsSamples.push(callMetrics);if(_this._metricsSamples.length>=METRICS_BATCH_SIZE){_this._publishMetrics()}_this.emit("sample",sample)};_this._onSignalingError=function(payload){var callsid=payload.callsid,voiceeventsid=payload.voiceeventsid;if(_this.parameters.CallSid!==callsid){_this._log.warn("Received an error from a different callsid: "+callsid);return}if(voiceeventsid&&_this._messages.has(voiceeventsid)){_this._messages.delete(voiceeventsid);_this._log.warn("Received an error while sending a message.",payload)}};_this._onSignalingReconnected=function(){if(_this._signalingStatus!==Call.State.Reconnecting){return}_this._log.info("Signaling Connection reestablished.");_this._signalingStatus=Call.State.Open;if(_this._mediaStatus===Call.State.Open){_this._publisher.info("connection","reconnected",null,_this);_this.emit("reconnected");_this._status=Call.State.Open}};_this._onTransportClose=function(){_this._log.error("Received transportClose from pstream");_this.emit("transportClose");if(_this._signalingReconnectToken){_this._status=Call.State.Reconnecting;_this._signalingStatus=Call.State.Reconnecting;_this.emit("reconnecting",new errors_1.SignalingErrors.ConnectionDisconnected)}else{_this._status=Call.State.Closed;_this._signalingStatus=Call.State.Closed}};_this._reemitWarning=function(warningData,wasCleared){var groupPrefix=/^audio/.test(warningData.name)?"audio-level-":"network-quality-";var warningPrefix=WARNING_PREFIXES[warningData.threshold.name];var warningName;if(warningData.name in MULTIPLE_THRESHOLD_WARNING_NAMES){warningName=MULTIPLE_THRESHOLD_WARNING_NAMES[warningData.name][warningData.threshold.name]}else if(warningData.name in WARNING_NAMES){warningName=WARNING_NAMES[warningData.name]}var warning=warningPrefix+warningName;_this._emitWarning(groupPrefix,warning,warningData.threshold.value,warningData.values||warningData.value,wasCleared,warningData)};_this._reemitWarningCleared=function(warningData){_this._reemitWarning(warningData,true)};_this._isUnifiedPlanDefault=config.isUnifiedPlanDefault;_this._soundcache=config.soundcache;if(typeof config.onIgnore==="function"){_this._onIgnore=config.onIgnore}var message=options&&options.twimlParams||{};_this.customParameters=new Map(Object.entries(message).map(function(_a){var key=_a[0],val=_a[1];return[key,String(val)]}));Object.assign(_this._options,options);if(_this._options.callParameters){_this.parameters=_this._options.callParameters}if(_this._options.reconnectToken){_this._signalingReconnectToken=_this._options.reconnectToken}_this._voiceEventSidGenerator=_this._options.voiceEventSidGenerator||uuid_1.generateVoiceEventSid;_this._direction=_this.parameters.CallSid?Call.CallDirection.Incoming:Call.CallDirection.Outgoing;if(_this._direction===Call.CallDirection.Incoming&&_this.parameters){_this.callerInfo=_this.parameters.StirStatus?{isVerified:_this.parameters.StirStatus==="TN-Validation-Passed-A"}:null}else{_this.callerInfo=null}_this._mediaReconnectBackoff=Backoff.exponential(BACKOFF_CONFIG);_this._mediaReconnectBackoff.on("ready",function(){return _this._mediaHandler.iceRestart()});_this.outboundConnectionId=generateTempCallSid();var publisher=_this._publisher=config.publisher;if(_this._direction===Call.CallDirection.Incoming){publisher.info("connection","incoming",null,_this)}else{publisher.info("connection","outgoing",{preflight:_this._options.preflight},_this)}var monitor=_this._monitor=new(_this._options.StatsMonitor||statsMonitor_1.default);monitor.on("sample",_this._onRTCSample);monitor.disableWarnings();setTimeout(function(){return monitor.enableWarnings()},METRICS_DELAY);monitor.on("warning",function(data,wasCleared){if(data.name==="bytesSent"||data.name==="bytesReceived"){_this._onMediaFailure(Call.MediaFailure.LowBytes)}_this._reemitWarning(data,wasCleared)});monitor.on("warning-cleared",function(data){_this._reemitWarningCleared(data)});_this._mediaHandler=new _this._options.MediaHandler(config.audioHelper,config.pstream,config.getUserMedia,{codecPreferences:_this._options.codecPreferences,dscp:_this._options.dscp,forceAggressiveIceNomination:_this._options.forceAggressiveIceNomination,isUnifiedPlan:_this._isUnifiedPlanDefault,maxAverageBitrate:_this._options.maxAverageBitrate,preflight:_this._options.preflight});_this.on("volume",function(inputVolume,outputVolume){_this._inputVolumeStreak=_this._checkVolume(inputVolume,_this._inputVolumeStreak,_this._latestInputVolume,"input");_this._outputVolumeStreak=_this._checkVolume(outputVolume,_this._outputVolumeStreak,_this._latestOutputVolume,"output");_this._latestInputVolume=inputVolume;_this._latestOutputVolume=outputVolume});_this._mediaHandler.onvolume=function(inputVolume,outputVolume,internalInputVolume,internalOutputVolume){monitor.addVolumes(internalInputVolume/255*32767,internalOutputVolume/255*32767);_this.emit("volume",inputVolume,outputVolume)};_this._mediaHandler.ondtlstransportstatechange=function(state){var level=state==="failed"?"error":"debug";_this._publisher.post(level,"dtls-transport-state",state,null,_this)};_this._mediaHandler.onpcconnectionstatechange=function(state){var level="debug";var dtlsTransport=_this._mediaHandler.getRTCDtlsTransport();if(state==="failed"){level=dtlsTransport&&dtlsTransport.state==="failed"?"error":"warning"}_this._publisher.post(level,"pc-connection-state",state,null,_this)};_this._mediaHandler.onicecandidate=function(candidate){var payload=new icecandidate_1.IceCandidate(candidate).toPayload();_this._publisher.debug("ice-candidate","ice-candidate",payload,_this)};_this._mediaHandler.onselectedcandidatepairchange=function(pair){var localCandidatePayload=new icecandidate_1.IceCandidate(pair.local).toPayload();var remoteCandidatePayload=new icecandidate_1.IceCandidate(pair.remote,true).toPayload();_this._publisher.debug("ice-candidate","selected-ice-candidate-pair",{local_candidate:localCandidatePayload,remote_candidate:remoteCandidatePayload},_this)};_this._mediaHandler.oniceconnectionstatechange=function(state){var level=state==="failed"?"error":"debug";_this._publisher.post(level,"ice-connection-state",state,null,_this)};_this._mediaHandler.onicegatheringfailure=function(type){_this._publisher.warn("ice-gathering-state",type,null,_this);_this._onMediaFailure(Call.MediaFailure.IceGatheringFailed)};_this._mediaHandler.onicegatheringstatechange=function(state){_this._publisher.debug("ice-gathering-state",state,null,_this)};_this._mediaHandler.onsignalingstatechange=function(state){_this._publisher.debug("signaling-state",state,null,_this)};_this._mediaHandler.ondisconnected=function(msg){_this._log.info(msg);_this._publisher.warn("network-quality-warning-raised","ice-connectivity-lost",{message:msg},_this);_this.emit("warning","ice-connectivity-lost");_this._onMediaFailure(Call.MediaFailure.ConnectionDisconnected)};_this._mediaHandler.onfailed=function(msg){_this._onMediaFailure(Call.MediaFailure.ConnectionFailed)};_this._mediaHandler.onconnected=function(){if(_this._status===Call.State.Reconnecting){_this._onMediaReconnected()}};_this._mediaHandler.onreconnected=function(msg){_this._log.info(msg);_this._publisher.info("network-quality-warning-cleared","ice-connectivity-lost",{message:msg},_this);_this.emit("warning-cleared","ice-connectivity-lost");_this._onMediaReconnected()};_this._mediaHandler.onerror=function(e){if(e.disconnect===true){_this._disconnect(e.info&&e.info.message)}var error=e.info.twilioError||new errors_1.GeneralErrors.UnknownError(e.info.message);_this._log.error("Received an error from MediaStream:",e);_this.emit("error",error)};_this._mediaHandler.onopen=function(){if(_this._status===Call.State.Open||_this._status===Call.State.Reconnecting){return}else if(_this._status===Call.State.Ringing||_this._status===Call.State.Connecting){_this.mute(false);_this._mediaStatus=Call.State.Open;_this._maybeTransitionToOpen()}else{_this._mediaHandler.close()}};_this._mediaHandler.onclose=function(){_this._status=Call.State.Closed;if(_this._options.shouldPlayDisconnect&&_this._options.shouldPlayDisconnect()&&!_this._isCancelled){_this._soundcache.get(device_1.default.SoundName.Disconnect).play()}monitor.disable();_this._publishMetrics();if(!_this._isCancelled){_this.emit("disconnect",_this)}};_this._pstream=config.pstream;_this._pstream.on("ack",_this._onAck);_this._pstream.on("cancel",_this._onCancel);_this._pstream.on("error",_this._onSignalingError);_this._pstream.on("ringing",_this._onRinging);_this._pstream.on("transportClose",_this._onTransportClose);_this._pstream.on("connected",_this._onConnected);_this._pstream.on("message",_this._onMessageReceived);_this.on("error",function(error){_this._publisher.error("connection","error",{code:error.code,message:error.message},_this);if(_this._pstream&&_this._pstream.status==="disconnected"){_this._cleanupEventListeners()}});_this.on("disconnect",function(){_this._cleanupEventListeners()});return _this}Object.defineProperty(Call.prototype,"direction",{get:function(){return this._direction},enumerable:true,configurable:true});Object.defineProperty(Call.prototype,"codec",{get:function(){return this._codec},enumerable:true,configurable:true});Call.prototype._setInputTracksFromStream=function(stream){return this._mediaHandler.setInputTracksFromStream(stream)};Call.prototype._setSinkIds=function(sinkIds){return this._mediaHandler._setSinkIds(sinkIds)};Call.prototype.accept=function(options){var _this=this;if(this._status!==Call.State.Pending){return}options=options||{};var rtcConfiguration=options.rtcConfiguration||this._options.rtcConfiguration;var rtcConstraints=options.rtcConstraints||this._options.rtcConstraints||{};var audioConstraints=rtcConstraints.audio||{audio:true};this._status=Call.State.Connecting;var connect=function(){if(_this._status!==Call.State.Connecting){_this._cleanupEventListeners();_this._mediaHandler.close();return}var onAnswer=function(pc,reconnectToken){var eventName=_this._direction===Call.CallDirection.Incoming?"accepted-by-local":"accepted-by-remote";_this._publisher.info("connection",eventName,null,_this);if(typeof reconnectToken==="string"){_this._signalingReconnectToken=reconnectToken}var _a=getPreferredCodecInfo(_this._mediaHandler.version.getSDP()),codecName=_a.codecName,codecParams=_a.codecParams;_this._publisher.info("settings","codec",{codec_params:codecParams,selected_codec:codecName},_this);_this._monitor.enable(pc)};var sinkIds=typeof _this._options.getSinkIds==="function"&&_this._options.getSinkIds();if(Array.isArray(sinkIds)){_this._mediaHandler._setSinkIds(sinkIds).catch(function(){})}_this._pstream.addListener("hangup",_this._onHangup);if(_this._direction===Call.CallDirection.Incoming){_this._isAnswered=true;_this._pstream.on("answer",_this._onAnswer.bind(_this));_this._mediaHandler.answerIncomingCall(_this.parameters.CallSid,_this._options.offerSdp,rtcConstraints,rtcConfiguration,onAnswer)}else{var params=Array.from(_this.customParameters.entries()).map(function(pair){return encodeURIComponent(pair[0])+"="+encodeURIComponent(pair[1])}).join("&");_this._pstream.on("answer",_this._onAnswer.bind(_this));_this._mediaHandler.makeOutgoingCall(_this._pstream.token,params,_this.outboundConnectionId,rtcConstraints,rtcConfiguration,onAnswer)}};if(this._options.beforeAccept){this._options.beforeAccept(this)}var inputStream=typeof this._options.getInputStream==="function"&&this._options.getInputStream();var promise=inputStream?this._mediaHandler.setInputTracksFromStream(inputStream):this._mediaHandler.openWithConstraints(audioConstraints);promise.then(function(){_this._publisher.info("get-user-media","succeeded",{data:{audioConstraints:audioConstraints}},_this);connect()},function(error){var twilioError;if(error.code===31208||["PermissionDeniedError","NotAllowedError"].indexOf(error.name)!==-1){twilioError=new errors_1.UserMediaErrors.PermissionDeniedError;_this._publisher.error("get-user-media","denied",{data:{audioConstraints:audioConstraints,error:error}},_this)}else{twilioError=new errors_1.UserMediaErrors.AcquisitionFailedError;_this._publisher.error("get-user-media","failed",{data:{audioConstraints:audioConstraints,error:error}},_this)}_this._disconnect();_this.emit("error",twilioError)})};Call.prototype.disconnect=function(){this._disconnect()};Call.prototype.getLocalStream=function(){return this._mediaHandler&&this._mediaHandler.stream};Call.prototype.getRemoteStream=function(){return this._mediaHandler&&this._mediaHandler._remoteStream};Call.prototype.ignore=function(){if(this._status!==Call.State.Pending){return}this._status=Call.State.Closed;this._mediaHandler.ignore(this.parameters.CallSid);this._publisher.info("connection","ignored-by-local",null,this);if(this._onIgnore){this._onIgnore()}};Call.prototype.isMuted=function(){return this._mediaHandler.isMuted};Call.prototype.mute=function(shouldMute){if(shouldMute===void 0){shouldMute=true}var wasMuted=this._mediaHandler.isMuted;this._mediaHandler.mute(shouldMute);var isMuted=this._mediaHandler.isMuted;if(wasMuted!==isMuted){this._publisher.info("connection",isMuted?"muted":"unmuted",null,this);this.emit("mute",isMuted,this)}};Call.prototype.postFeedback=function(score,issue){if(typeof score==="undefined"||score===null){return this._postFeedbackDeclined()}if(!Object.values(Call.FeedbackScore).includes(score)){throw new errors_1.InvalidArgumentError("Feedback score must be one of: "+Object.values(Call.FeedbackScore))}if(typeof issue!=="undefined"&&issue!==null&&!Object.values(Call.FeedbackIssue).includes(issue)){throw new errors_1.InvalidArgumentError("Feedback issue must be one of: "+Object.values(Call.FeedbackIssue))}return this._publisher.info("feedback","received",{issue_name:issue,quality_score:score},this,true)};Call.prototype.reject=function(){if(this._status!==Call.State.Pending){return}this._pstream.reject(this.parameters.CallSid);this._status=Call.State.Closed;this.emit("reject");this._mediaHandler.reject(this.parameters.CallSid);this._publisher.info("connection","rejected-by-local",null,this)};Call.prototype.sendDigits=function(digits){if(digits.match(/[^0-9*#w]/)){throw new errors_1.InvalidArgumentError("Illegal character passed into sendDigits")}var sequence=[];digits.split("").forEach(function(digit){var dtmf=digit!=="w"?"dtmf"+digit:"";if(dtmf==="dtmf*"){dtmf="dtmfs"}if(dtmf==="dtmf#"){dtmf="dtmfh"}sequence.push(dtmf)});(function playNextDigit(soundCache,dialtonePlayer){var digit=sequence.shift();if(digit){if(dialtonePlayer){dialtonePlayer.play(digit)}else{soundCache.get(digit).play()}}if(sequence.length){setTimeout(playNextDigit.bind(null,soundCache),200)}})(this._soundcache,this._options.dialtonePlayer);var dtmfSender=this._mediaHandler.getOrCreateDTMFSender();function insertDTMF(dtmfs){if(!dtmfs.length){return}var dtmf=dtmfs.shift();if(dtmf&&dtmf.length){dtmfSender.insertDTMF(dtmf,DTMF_TONE_DURATION,DTMF_INTER_TONE_GAP)}setTimeout(insertDTMF.bind(null,dtmfs),DTMF_PAUSE_DURATION)}if(dtmfSender){if(!("canInsertDTMF"in dtmfSender)||dtmfSender.canInsertDTMF){this._log.info("Sending digits using RTCDTMFSender");insertDTMF(digits.split("w"));return}this._log.info("RTCDTMFSender cannot insert DTMF")}this._log.info("Sending digits over PStream");if(this._pstream!==null&&this._pstream.status!=="disconnected"){this._pstream.dtmf(this.parameters.CallSid,digits)}else{var error=new errors_1.GeneralErrors.ConnectionError("Could not send DTMF: Signaling channel is disconnected");this.emit("error",error)}};Call.prototype.sendMessage=function(message){var content=message.content,contentType=message.contentType,messageType=message.messageType;if(typeof content==="undefined"||content===null){throw new errors_1.InvalidArgumentError("`content` is empty")}if(typeof messageType!=="string"){throw new errors_1.InvalidArgumentError("`messageType` must be an enumeration value of `Call.MessageType` or "+"a string.")}if(messageType.length===0){throw new errors_1.InvalidArgumentError("`messageType` must be a non-empty string.")}if(this._pstream===null){throw new errors_1.InvalidStateError("Could not send CallMessage; Signaling channel is disconnected")}var callSid=this.parameters.CallSid;if(typeof this.parameters.CallSid==="undefined"){throw new errors_1.InvalidStateError("Could not send CallMessage; Call has no CallSid")}var voiceEventSid=this._voiceEventSidGenerator();this._messages.set(voiceEventSid,{content:content,contentType:contentType,messageType:messageType,voiceEventSid:voiceEventSid});this._pstream.sendMessage(callSid,content,contentType,messageType,voiceEventSid);return voiceEventSid};Call.prototype.status=function(){return this._status};Call.prototype._checkVolume=function(currentVolume,currentStreak,lastValue,direction){var wasWarningRaised=currentStreak>=10;var newStreak=0;if(lastValue===currentVolume){newStreak=currentStreak}if(newStreak>=10){this._emitWarning("audio-level-","constant-audio-"+direction+"-level",10,newStreak,false)}else if(wasWarningRaised){this._emitWarning("audio-level-","constant-audio-"+direction+"-level",10,newStreak,true)}return newStreak};Call.prototype._cleanupEventListeners=function(){var _this=this;var cleanup=function(){if(!_this._pstream){return}_this._pstream.removeListener("ack",_this._onAck);_this._pstream.removeListener("answer",_this._onAnswer);_this._pstream.removeListener("cancel",_this._onCancel);_this._pstream.removeListener("error",_this._onSignalingError);_this._pstream.removeListener("hangup",_this._onHangup);_this._pstream.removeListener("ringing",_this._onRinging);_this._pstream.removeListener("transportClose",_this._onTransportClose);_this._pstream.removeListener("connected",_this._onConnected);_this._pstream.removeListener("message",_this._onMessageReceived)};cleanup();setTimeout(cleanup,0)};Call.prototype._createMetricPayload=function(){var payload={call_sid:this.parameters.CallSid,dscp:!!this._options.dscp,sdk_version:C.RELEASE_VERSION,selected_region:this._options.selectedRegion};if(this._options.gateway){payload.gateway=this._options.gateway}if(this._options.region){payload.region=this._options.region}payload.direction=this._direction;return payload};Call.prototype._disconnect=function(message,wasRemote){message=typeof message==="string"?message:null;if(this._status!==Call.State.Open&&this._status!==Call.State.Connecting&&this._status!==Call.State.Reconnecting&&this._status!==Call.State.Ringing){return}this._log.info("Disconnecting...");if(this._pstream!==null&&this._pstream.status!=="disconnected"&&this._shouldSendHangup){var callsid=this.parameters.CallSid||this.outboundConnectionId;if(callsid){this._pstream.hangup(callsid,message)}}this._cleanupEventListeners();this._mediaHandler.close();if(!wasRemote){this._publisher.info("connection","disconnected-by-local",null,this)}};Call.prototype._maybeTransitionToOpen=function(){var wasConnected=this._wasConnected;if(this._isAnswered){this._onSignalingReconnected();this._signalingStatus=Call.State.Open;if(this._mediaHandler&&this._mediaHandler.status==="open"){this._status=Call.State.Open;if(!this._wasConnected){this._wasConnected=true;this.emit("accept",this)}}}};Call.prototype._postFeedbackDeclined=function(){return this._publisher.info("feedback","received-none",null,this,true)};Call.prototype._publishMetrics=function(){var _this=this;if(this._metricsSamples.length===0){return}this._publisher.postMetrics("quality-metrics-samples","metrics-sample",this._metricsSamples.splice(0),this._createMetricPayload(),this).catch(function(e){_this._log.warn("Unable to post metrics to Insights. Received error:",e)})};Call.prototype._setCallSid=function(payload){var callSid=payload.callsid;if(!callSid){return}this.parameters.CallSid=callSid;this._mediaHandler.callSid=callSid};Call.toString=function(){return"[Twilio.Call class]"};return Call}(events_1.EventEmitter);(function(Call){var State;(function(State){State["Closed"]="closed";State["Connecting"]="connecting";State["Open"]="open";State["Pending"]="pending";State["Reconnecting"]="reconnecting";State["Ringing"]="ringing"})(State=Call.State||(Call.State={}));var FeedbackIssue;(function(FeedbackIssue){FeedbackIssue["AudioLatency"]="audio-latency";FeedbackIssue["ChoppyAudio"]="choppy-audio";FeedbackIssue["DroppedCall"]="dropped-call";FeedbackIssue["Echo"]="echo";FeedbackIssue["NoisyCall"]="noisy-call";FeedbackIssue["OneWayAudio"]="one-way-audio"})(FeedbackIssue=Call.FeedbackIssue||(Call.FeedbackIssue={}));var FeedbackScore;(function(FeedbackScore){FeedbackScore[FeedbackScore["One"]=1]="One";FeedbackScore[FeedbackScore["Two"]=2]="Two";FeedbackScore[FeedbackScore["Three"]=3]="Three";FeedbackScore[FeedbackScore["Four"]=4]="Four";FeedbackScore[FeedbackScore["Five"]=5]="Five"})(FeedbackScore=Call.FeedbackScore||(Call.FeedbackScore={}));var CallDirection;(function(CallDirection){CallDirection["Incoming"]="INCOMING";CallDirection["Outgoing"]="OUTGOING"})(CallDirection=Call.CallDirection||(Call.CallDirection={}));var Codec;(function(Codec){Codec["Opus"]="opus";Codec["PCMU"]="pcmu"})(Codec=Call.Codec||(Call.Codec={}));var IceGatheringFailureReason;(function(IceGatheringFailureReason){IceGatheringFailureReason["None"]="none";IceGatheringFailureReason["Timeout"]="timeout"})(IceGatheringFailureReason=Call.IceGatheringFailureReason||(Call.IceGatheringFailureReason={}));var MediaFailure;(function(MediaFailure){MediaFailure["ConnectionDisconnected"]="ConnectionDisconnected";MediaFailure["ConnectionFailed"]="ConnectionFailed";MediaFailure["IceGatheringFailed"]="IceGatheringFailed";MediaFailure["LowBytes"]="LowBytes"})(MediaFailure=Call.MediaFailure||(Call.MediaFailure={}));var MessageType;(function(MessageType){MessageType["UserDefinedMessage"]="user-defined-message"})(MessageType=Call.MessageType||(Call.MessageType={}))})(Call||(Call={}));function generateTempCallSid(){return"TJSxxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g,function(c){var r=Math.random()*16|0;var v=c==="x"?r:r&3|8;return v.toString(16)})}exports.default=Call},{"./constants":7,"./device":9,"./errors":12,"./log":15,"./rtc":23,"./rtc/icecandidate":22,"./rtc/sdp":28,"./statsMonitor":34,"./util":35,"./uuid":36,backoff:45,events:53}],7:[function(require,module,exports){var PACKAGE_NAME="@twilio/voice-sdk";var RELEASE_VERSION="2.2.0";var SOUNDS_BASE_URL="https://sdk.twilio.com/js/client/sounds/releases/1.0.0";module.exports.COWBELL_AUDIO_URL=SOUNDS_BASE_URL+"/cowbell.mp3?cache="+RELEASE_VERSION;module.exports.ECHO_TEST_DURATION=2e4;module.exports.PACKAGE_NAME=PACKAGE_NAME;module.exports.RELEASE_VERSION=RELEASE_VERSION;module.exports.SOUNDS_BASE_URL=SOUNDS_BASE_URL;module.exports.USED_ERRORS=["AuthorizationErrors.AccessTokenExpired","AuthorizationErrors.AccessTokenInvalid","AuthorizationErrors.AuthenticationFailed","AuthorizationErrors.PayloadSizeExceededError","AuthorizationErrors.RateExceededError","ClientErrors.BadRequest","GeneralErrors.CallCancelledError","GeneralErrors.ConnectionError","GeneralErrors.TransportError","GeneralErrors.UnknownError","MalformedRequestErrors.MalformedRequestError","MediaErrors.ClientLocalDescFailed","MediaErrors.ClientRemoteDescFailed","MediaErrors.ConnectionError","SignalingErrors.ConnectionDisconnected","SignalingErrors.ConnectionError","UserMediaErrors.PermissionDeniedError","UserMediaErrors.AcquisitionFailedError"]},{}],8:[function(require,module,exports){"use strict";Object.defineProperty(exports,"__esModule",{value:true});var Deferred=function(){function Deferred(){var _this=this;this._promise=new Promise(function(resolve,reject){_this._resolve=resolve;_this._reject=reject})}Object.defineProperty(Deferred.prototype,"promise",{get:function(){return this._promise},enumerable:true,configurable:true});Deferred.prototype.reject=function(reason){this._reject(reason)};Deferred.prototype.resolve=function(value){this._resolve(value)};return Deferred}();exports.default=Deferred},{}],9:[function(require,module,exports){"use strict";var __extends=this&&this.__extends||function(){var extendStatics=function(d,b){extendStatics=Object.setPrototypeOf||{__proto__:[]}instanceof Array&&function(d,b){d.__proto__=b}||function(d,b){for(var p in b)if(b.hasOwnProperty(p))d[p]=b[p]};return extendStatics(d,b)};return function(d,b){extendStatics(d,b);function __(){this.constructor=d}d.prototype=b===null?Object.create(b):(__.prototype=b.prototype,new __)}}();var __assign=this&&this.__assign||function(){__assign=Object.assign||function(t){for(var s,i=1,n=arguments.length;i0&&t[t.length-1])&&(op[0]===6||op[0]===2)){_=0;continue}if(op[0]===3&&(!t||op[1]>t[0]&&op[1]0){var preferredURI=preferredURIs[0];_this._preferredURI=regions_1.createSignalingEndpointURL(preferredURI)}else{_this._log.info("Could not parse a preferred URI from the stream#connected event.")}if(_this._shouldReRegister){_this.register()}};_this._onSignalingError=function(payload){if(typeof payload!=="object"){return}var originalError=payload.error,callsid=payload.callsid;if(typeof originalError!=="object"){return}var call=typeof callsid==="string"&&_this._findCall(callsid)||undefined;var code=originalError.code,customMessage=originalError.message;var twilioError=originalError.twilioError;if(typeof code==="number"){if(code===31201){twilioError=new errors_1.AuthorizationErrors.AuthenticationFailed(originalError)}else if(code===31204){twilioError=new errors_1.AuthorizationErrors.AccessTokenInvalid(originalError)}else if(code===31205){_this._stopRegistrationTimer();twilioError=new errors_1.AuthorizationErrors.AccessTokenExpired(originalError)}else if(errors_1.hasErrorByCode(code)){twilioError=new(errors_1.getErrorByCode(code))(originalError)}}if(!twilioError){_this._log.error("Unknown signaling error: ",originalError);twilioError=new errors_1.GeneralErrors.UnknownError(customMessage,originalError)}_this._log.info("Received error: ",twilioError);_this.emit(Device.EventName.Error,twilioError,call)};_this._onSignalingInvite=function(payload){return __awaiter(_this,void 0,void 0,function(){var wasBusy,callParameters,customParameters,call,play;var _this=this;return __generator(this,function(_a){switch(_a.label){case 0:wasBusy=!!this._activeCall;if(wasBusy&&!this._options.allowIncomingWhileBusy){this._log.info("Device busy; ignoring incoming invite");return[2]}if(!payload.callsid||!payload.sdp){this.emit(Device.EventName.Error,new errors_1.ClientErrors.BadRequest("Malformed invite from gateway"));return[2]}callParameters=payload.parameters||{};callParameters.CallSid=callParameters.CallSid||payload.callsid;customParameters=Object.assign({},util_1.queryToJson(callParameters.Params));return[4,this._makeCall(customParameters,{callParameters:callParameters,offerSdp:payload.sdp,reconnectToken:payload.reconnect,voiceEventSidGenerator:this._options.voiceEventSidGenerator})];case 1:call=_a.sent();this._calls.push(call);call.once("accept",function(){_this._soundcache.get(Device.SoundName.Incoming).stop();_this._publishNetworkChange()});play=this._enabledSounds.incoming&&!wasBusy?function(){return _this._soundcache.get(Device.SoundName.Incoming).play()}:function(){return Promise.resolve()};this._showIncomingCall(call,play);return[2]}})})};_this._onSignalingOffline=function(){_this._log.info("Stream is offline");_this._edge=null;_this._region=null;_this._shouldReRegister=_this.state!==Device.State.Unregistered;_this._setState(Device.State.Unregistered)};_this._onSignalingReady=function(){_this._log.info("Stream is ready");_this._setState(Device.State.Registered)};_this._publishNetworkChange=function(){if(!_this._activeCall){return}if(_this._networkInformation){_this._publisher.info("network-information","network-change",{connection_type:_this._networkInformation.type,downlink:_this._networkInformation.downlink,downlinkMax:_this._networkInformation.downlinkMax,effective_type:_this._networkInformation.effectiveType,rtt:_this._networkInformation.rtt},_this._activeCall)}};_this._updateInputStream=function(inputStream){var call=_this._activeCall;if(call&&!inputStream){return Promise.reject(new errors_1.InvalidStateError("Cannot unset input device while a call is in progress."))}_this._callInputStream=inputStream;return call?call._setInputTracksFromStream(inputStream):Promise.resolve()};_this._updateSinkIds=function(type,sinkIds){var promise=type==="ringtone"?_this._updateRingtoneSinkIds(sinkIds):_this._updateSpeakerSinkIds(sinkIds);return promise.then(function(){_this._publisher.info("audio",type+"-devices-set",{audio_device_ids:sinkIds},_this._activeCall)},function(error){_this._publisher.error("audio",type+"-devices-set-failed",{audio_device_ids:sinkIds,message:error.message},_this._activeCall);throw error})};_this.updateToken(token);if(util_1.isLegacyEdge()){throw new errors_1.NotSupportedError("Microsoft Edge Legacy (https://support.microsoft.com/en-us/help/4533505/what-is-microsoft-edge-legacy) "+"is deprecated and will not be able to connect to Twilio to make or receive calls after September 1st, 2020. "+"Please see this documentation for a list of supported browsers "+"https://www.twilio.com/docs/voice/client/javascript#supported-browsers")}if(!Device.isSupported&&options.ignoreBrowserSupport){if(window&&window.location&&window.location.protocol==="http:"){throw new errors_1.NotSupportedError("twilio.js wasn't able to find WebRTC browser support. This is most likely because this page is served over http rather than https, which does not support WebRTC in many browsers. Please load this page over https and try again.")}throw new errors_1.NotSupportedError("twilio.js 1.3+ SDKs require WebRTC browser support. For more information, see . If you have any questions about this announcement, please contact Twilio Support at .")}if(window){var root=window;var browser=root.msBrowser||root.browser||root.chrome;_this._isBrowserExtension=!!browser&&!!browser.runtime&&!!browser.runtime.id||!!root.safari&&!!root.safari.extension}if(_this._isBrowserExtension){_this._log.info("Running as browser extension.")}if(navigator){var n=navigator;_this._networkInformation=n.connection||n.mozConnection||n.webkitConnection}if(_this._networkInformation&&typeof _this._networkInformation.addEventListener==="function"){_this._networkInformation.addEventListener("change",_this._publishNetworkChange)}Device._getOrCreateAudioContext();if(Device._audioContext){if(!Device._dialtonePlayer){Device._dialtonePlayer=new dialtonePlayer_1.default(Device._audioContext)}}if(typeof Device._isUnifiedPlanDefault==="undefined"){Device._isUnifiedPlanDefault=typeof window!=="undefined"&&typeof RTCPeerConnection!=="undefined"&&typeof RTCRtpTransceiver!=="undefined"?util_1.isUnifiedPlanDefault(window,window.navigator,RTCPeerConnection,RTCRtpTransceiver):false}_this._boundDestroy=_this.destroy.bind(_this);_this._boundConfirmClose=_this._confirmClose.bind(_this);if(typeof window!=="undefined"&&window.addEventListener){window.addEventListener("unload",_this._boundDestroy);window.addEventListener("pagehide",_this._boundDestroy)}_this.updateOptions(options);return _this}Object.defineProperty(Device,"audioContext",{get:function(){return Device._audioContext},enumerable:true,configurable:true});Object.defineProperty(Device,"extension",{get:function(){var a=typeof document!=="undefined"?document.createElement("audio"):{canPlayType:false};var canPlayMp3;try{canPlayMp3=a.canPlayType&&!!a.canPlayType("audio/mpeg").replace(/no/,"")}catch(e){canPlayMp3=false}var canPlayVorbis;try{canPlayVorbis=a.canPlayType&&!!a.canPlayType("audio/ogg;codecs='vorbis'").replace(/no/,"")}catch(e){canPlayVorbis=false}return canPlayVorbis&&!canPlayMp3?"ogg":"mp3"},enumerable:true,configurable:true});Object.defineProperty(Device,"isSupported",{get:function(){return rtc.enabled()},enumerable:true,configurable:true});Object.defineProperty(Device,"packageName",{get:function(){return C.PACKAGE_NAME},enumerable:true,configurable:true});Device.runPreflight=function(token,options){return new preflight_1.PreflightTest(token,__assign({audioContext:Device._getOrCreateAudioContext()},options))};Device.toString=function(){return"[Twilio.Device class]"};Object.defineProperty(Device,"version",{get:function(){return C.RELEASE_VERSION},enumerable:true,configurable:true});Device._getOrCreateAudioContext=function(){if(!Device._audioContext){if(typeof AudioContext!=="undefined"){Device._audioContext=new AudioContext}else if(typeof webkitAudioContext!=="undefined"){Device._audioContext=new webkitAudioContext}}return Device._audioContext};Object.defineProperty(Device.prototype,"audio",{get:function(){return this._audio},enumerable:true,configurable:true});Device.prototype.connect=function(options){if(options===void 0){options={}}return __awaiter(this,void 0,void 0,function(){var activeCall,_a;return __generator(this,function(_b){switch(_b.label){case 0:this._throwIfDestroyed();if(this._activeCall){throw new errors_1.InvalidStateError("A Call is already active")}_a=this;return[4,this._makeCall(options.params||{},{rtcConfiguration:options.rtcConfiguration,voiceEventSidGenerator:this._options.voiceEventSidGenerator})];case 1:activeCall=_a._activeCall=_b.sent();this._calls.splice(0).forEach(function(call){return call.ignore()});this._soundcache.get(Device.SoundName.Incoming).stop();activeCall.accept({rtcConstraints:options.rtcConstraints});this._publishNetworkChange();return[2,activeCall]}})})};Object.defineProperty(Device.prototype,"calls",{get:function(){return this._calls},enumerable:true,configurable:true});Device.prototype.destroy=function(){this.disconnectAll();this._stopRegistrationTimer();if(this._audio){this._audio._unbind()}this._destroyStream();this._destroyPublisher();this._destroyAudioHelper();if(this._networkInformation&&typeof this._networkInformation.removeEventListener==="function"){this._networkInformation.removeEventListener("change",this._publishNetworkChange)}if(typeof window!=="undefined"&&window.removeEventListener){window.removeEventListener("beforeunload",this._boundConfirmClose);window.removeEventListener("unload",this._boundDestroy);window.removeEventListener("pagehide",this._boundDestroy)}this._setState(Device.State.Destroyed);events_1.EventEmitter.prototype.removeAllListeners.call(this)};Device.prototype.disconnectAll=function(){var calls=this._calls.splice(0);calls.forEach(function(call){return call.disconnect()});if(this._activeCall){this._activeCall.disconnect()}};Object.defineProperty(Device.prototype,"edge",{get:function(){return this._edge},enumerable:true,configurable:true});Object.defineProperty(Device.prototype,"home",{get:function(){return this._home},enumerable:true,configurable:true});Object.defineProperty(Device.prototype,"identity",{get:function(){return this._identity},enumerable:true,configurable:true});Object.defineProperty(Device.prototype,"isBusy",{get:function(){return!!this._activeCall},enumerable:true,configurable:true});Device.prototype.register=function(){return __awaiter(this,void 0,void 0,function(){var stream,streamReadyPromise;var _this=this;return __generator(this,function(_a){switch(_a.label){case 0:if(this.state!==Device.State.Unregistered){throw new errors_1.InvalidStateError('Attempt to register when device is in state "'+this.state+'". '+('Must be "'+Device.State.Unregistered+'".'))}this._setState(Device.State.Registering);return[4,this._streamConnectedPromise||this._setupStream()];case 1:stream=_a.sent();streamReadyPromise=new Promise(function(resolve){_this.once(Device.State.Registered,resolve)});return[4,this._sendPresence(true)];case 2:_a.sent();return[4,streamReadyPromise];case 3:_a.sent();return[2]}})})};Object.defineProperty(Device.prototype,"state",{get:function(){return this._state},enumerable:true,configurable:true});Object.defineProperty(Device.prototype,"token",{get:function(){return this._token},enumerable:true,configurable:true});Device.prototype.toString=function(){return"[Twilio.Device instance]"};Device.prototype.unregister=function(){return __awaiter(this,void 0,void 0,function(){var stream,streamOfflinePromise;return __generator(this,function(_a){switch(_a.label){case 0:if(this.state!==Device.State.Registered){throw new errors_1.InvalidStateError('Attempt to unregister when device is in state "'+this.state+'". '+('Must be "'+Device.State.Registered+'".'))}this._shouldReRegister=false;return[4,this._streamConnectedPromise];case 1:stream=_a.sent();streamOfflinePromise=new Promise(function(resolve){stream.on("offline",resolve)});return[4,this._sendPresence(false)];case 2:_a.sent();return[4,streamOfflinePromise];case 3:_a.sent();return[2]}})})};Device.prototype.updateOptions=function(options){if(options===void 0){options={}}if(this.state===Device.State.Destroyed){throw new errors_1.InvalidStateError('Attempt to "updateOptions" when device is in state "'+this.state+'".')}this._options=__assign(__assign(__assign({},this._defaultOptions),this._options),options);var originalChunderURIs=new Set(this._chunderURIs);var chunderw=typeof this._options.chunderw==="string"?[this._options.chunderw]:Array.isArray(this._options.chunderw)&&this._options.chunderw;var newChunderURIs=this._chunderURIs=(chunderw||regions_1.getChunderURIs(this._options.edge,undefined,this._log.warn.bind(this._log))).map(regions_1.createSignalingEndpointURL);var hasChunderURIsChanged=originalChunderURIs.size!==newChunderURIs.length;if(!hasChunderURIsChanged){for(var _i=0,newChunderURIs_1=newChunderURIs;_i=0;i--){if(call===this._calls[i]){this._calls.splice(i,1)}}};Device.prototype._sendPresence=function(presence){return __awaiter(this,void 0,void 0,function(){var stream;return __generator(this,function(_a){switch(_a.label){case 0:return[4,this._streamConnectedPromise];case 1:stream=_a.sent();if(!stream){return[2]}stream.register({audio:presence});if(presence){this._startRegistrationTimer()}else{this._stopRegistrationTimer()}return[2]}})})};Device.prototype._setState=function(state){if(state===this.state){return}this._state=state;this.emit(this._stateEventMapping[state])};Device.prototype._setupAudioHelper=function(){var _this=this;if(this._audio){this._log.info("Found existing audio helper; destroying...");this._destroyAudioHelper()}this._audio=new(this._options.AudioHelper||audiohelper_1.default)(this._updateSinkIds,this._updateInputStream,getUserMedia,{audioContext:Device.audioContext,enabledSounds:this._enabledSounds});this._audio.on("deviceChange",function(lostActiveDevices){var activeCall=_this._activeCall;var deviceIds=lostActiveDevices.map(function(device){return device.deviceId});_this._publisher.info("audio","device-change",{lost_active_device_ids:deviceIds},activeCall);if(activeCall){activeCall["_mediaHandler"]._onInputDevicesChanged()}})};Device.prototype._setupPublisher=function(){var _this=this;if(this._publisher){this._log.info("Found existing publisher; destroying...");this._destroyPublisher()}var publisherOptions={defaultPayload:this._createDefaultPayload,log:this._log,metadata:{app_name:this._options.appName,app_version:this._options.appVersion}};if(this._options.eventgw){publisherOptions.host=this._options.eventgw}if(this._home){publisherOptions.host=regions_1.createEventGatewayURI(this._home)}this._publisher=new(this._options.Publisher||Publisher)(PUBLISHER_PRODUCT_NAME,this.token,publisherOptions);if(this._options.publishEvents===false){this._publisher.disable()}else{this._publisher.on("error",function(error){_this._log.warn("Cannot connect to insights.",error)})}return this._publisher};Device.prototype._setupStream=function(){var _this=this;if(this._stream){this._log.info("Found existing stream; destroying...");this._destroyStream()}this._log.info("Setting up VSP");this._stream=new(this._options.PStream||PStream)(this.token,this._chunderURIs,{backoffMaxMs:this._options.backoffMaxMs,maxPreferredDurationMs:this._options.maxCallSignalingTimeoutMs});this._stream.addListener("close",this._onSignalingClose);this._stream.addListener("connected",this._onSignalingConnected);this._stream.addListener("error",this._onSignalingError);this._stream.addListener("invite",this._onSignalingInvite);this._stream.addListener("offline",this._onSignalingOffline);this._stream.addListener("ready",this._onSignalingReady);return this._streamConnectedPromise=new Promise(function(resolve){return _this._stream.once("connected",function(){resolve(_this._stream)})})};Device.prototype._showIncomingCall=function(call,play){var _this=this;var timeout;return Promise.race([play(),new Promise(function(resolve,reject){timeout=setTimeout(function(){var msg="Playing incoming ringtone took too long; it might not play. Continuing execution...";reject(new Error(msg))},RINGTONE_PLAY_TIMEOUT)})]).catch(function(reason){_this._log.info(reason.message)}).then(function(){clearTimeout(timeout);_this.emit(Device.EventName.Incoming,call)})};Device.prototype._startRegistrationTimer=function(){var _this=this;this._stopRegistrationTimer();this._regTimer=setTimeout(function(){_this._sendPresence(true)},REGISTRATION_INTERVAL)};Device.prototype._stopRegistrationTimer=function(){if(this._regTimer){clearTimeout(this._regTimer)}};Device.prototype._throwIfDestroyed=function(){if(this.state===Device.State.Destroyed){throw new errors_1.InvalidStateError("Device has been destroyed.")}};Device.prototype._updateRingtoneSinkIds=function(sinkIds){return Promise.resolve(this._soundcache.get(Device.SoundName.Incoming).setSinkIds(sinkIds))};Device.prototype._updateSpeakerSinkIds=function(sinkIds){Array.from(this._soundcache.entries()).filter(function(entry){return entry[0]!==Device.SoundName.Incoming}).forEach(function(entry){return entry[1].setSinkIds(sinkIds)});this._callSinkIds=sinkIds;var call=this._activeCall;return call?call._setSinkIds(sinkIds):Promise.resolve()};Device._defaultSounds={disconnect:{filename:"disconnect",maxDuration:3e3},dtmf0:{filename:"dtmf-0",maxDuration:1e3},dtmf1:{filename:"dtmf-1",maxDuration:1e3},dtmf2:{filename:"dtmf-2",maxDuration:1e3},dtmf3:{filename:"dtmf-3",maxDuration:1e3},dtmf4:{filename:"dtmf-4",maxDuration:1e3},dtmf5:{filename:"dtmf-5",maxDuration:1e3},dtmf6:{filename:"dtmf-6",maxDuration:1e3},dtmf7:{filename:"dtmf-7",maxDuration:1e3},dtmf8:{filename:"dtmf-8",maxDuration:1e3},dtmf9:{filename:"dtmf-9",maxDuration:1e3},dtmfh:{filename:"dtmf-hash",maxDuration:1e3},dtmfs:{filename:"dtmf-star",maxDuration:1e3},incoming:{filename:"incoming",shouldLoop:true},outgoing:{filename:"outgoing",maxDuration:3e3}};return Device}(events_1.EventEmitter);(function(Device){var EventName;(function(EventName){EventName["Error"]="error";EventName["Incoming"]="incoming";EventName["Destroyed"]="destroyed";EventName["Unregistered"]="unregistered";EventName["Registering"]="registering";EventName["Registered"]="registered";EventName["TokenWillExpire"]="tokenWillExpire"})(EventName=Device.EventName||(Device.EventName={}));var State;(function(State){State["Destroyed"]="destroyed";State["Unregistered"]="unregistered";State["Registering"]="registering";State["Registered"]="registered"})(State=Device.State||(Device.State={}));var SoundName;(function(SoundName){SoundName["Incoming"]="incoming";SoundName["Outgoing"]="outgoing";SoundName["Disconnect"]="disconnect";SoundName["Dtmf0"]="dtmf0";SoundName["Dtmf1"]="dtmf1";SoundName["Dtmf2"]="dtmf2";SoundName["Dtmf3"]="dtmf3";SoundName["Dtmf4"]="dtmf4";SoundName["Dtmf5"]="dtmf5";SoundName["Dtmf6"]="dtmf6";SoundName["Dtmf7"]="dtmf7";SoundName["Dtmf8"]="dtmf8";SoundName["Dtmf9"]="dtmf9";SoundName["DtmfS"]="dtmfs";SoundName["DtmfH"]="dtmfh"})(SoundName=Device.SoundName||(Device.SoundName={}))})(Device||(Device={}));exports.default=Device},{"./audiohelper":5,"./call":6,"./constants":7,"./dialtonePlayer":10,"./errors":12,"./eventpublisher":14,"./log":15,"./preflight/preflight":17,"./pstream":18,"./regions":19,"./rtc":23,"./rtc/getusermedia":21,"./sound":33,"./util":35,"./uuid":36,events:53,loglevel:55}],10:[function(require,module,exports){"use strict";Object.defineProperty(exports,"__esModule",{value:true});var errors_1=require("./errors");var bandFrequencies={dtmf0:[1360,960],dtmf1:[1230,720],dtmf2:[1360,720],dtmf3:[1480,720],dtmf4:[1230,790],dtmf5:[1360,790],dtmf6:[1480,790],dtmf7:[1230,870],dtmf8:[1360,870],dtmf9:[1480,870],dtmfh:[1480,960],dtmfs:[1230,960]};var DialtonePlayer=function(){function DialtonePlayer(_context){var _this=this;this._context=_context;this._gainNodes=[];this._gainNodes=[this._context.createGain(),this._context.createGain()];this._gainNodes.forEach(function(gainNode){gainNode.connect(_this._context.destination);gainNode.gain.value=.1;_this._gainNodes.push(gainNode)})}DialtonePlayer.prototype.cleanup=function(){this._gainNodes.forEach(function(gainNode){gainNode.disconnect()})};DialtonePlayer.prototype.play=function(sound){var _this=this;var frequencies=bandFrequencies[sound];if(!frequencies){throw new errors_1.InvalidArgumentError("Invalid DTMF sound name")}var oscillators=[this._context.createOscillator(),this._context.createOscillator()];oscillators.forEach(function(oscillator,i){oscillator.type="sine";oscillator.frequency.value=frequencies[i];oscillator.connect(_this._gainNodes[i]);oscillator.start();oscillator.stop(_this._context.currentTime+.1);oscillator.addEventListener("ended",function(){return oscillator.disconnect()})})};return DialtonePlayer}();exports.default=DialtonePlayer},{"./errors":12}],11:[function(require,module,exports){"use strict";var __extends=this&&this.__extends||function(){var extendStatics=function(d,b){extendStatics=Object.setPrototypeOf||{__proto__:[]}instanceof Array&&function(d,b){d.__proto__=b}||function(d,b){for(var p in b)if(b.hasOwnProperty(p))d[p]=b[p]};return extendStatics(d,b)};return function(d,b){extendStatics(d,b);function __(){this.constructor=d}d.prototype=b===null?Object.create(b):(__.prototype=b.prototype,new __)}}();Object.defineProperty(exports,"__esModule",{value:true});var twilioError_1=require("./twilioError");exports.TwilioError=twilioError_1.default;var AuthorizationErrors;(function(AuthorizationErrors){var AccessTokenInvalid=function(_super){__extends(AccessTokenInvalid,_super);function AccessTokenInvalid(messageOrError,error){var _this=_super.call(this,messageOrError,error)||this;_this.causes=[];_this.code=20101;_this.description="Invalid access token";_this.explanation="Twilio was unable to validate your Access Token";_this.name="AccessTokenInvalid";_this.solutions=[];Object.setPrototypeOf(_this,AuthorizationErrors.AccessTokenInvalid.prototype);var message=typeof messageOrError==="string"?messageOrError:_this.explanation;var originalError=typeof messageOrError==="object"?messageOrError:error;_this.message=_this.name+" ("+_this.code+"): "+message;_this.originalError=originalError;return _this}return AccessTokenInvalid}(twilioError_1.default);AuthorizationErrors.AccessTokenInvalid=AccessTokenInvalid;var AccessTokenExpired=function(_super){__extends(AccessTokenExpired,_super);function AccessTokenExpired(messageOrError,error){var _this=_super.call(this,messageOrError,error)||this;_this.causes=[];_this.code=20104;_this.description="Access token expired or expiration date invalid";_this.explanation="The Access Token provided to the Twilio API has expired, the expiration time specified in the token was invalid, or the expiration time specified was too far in the future";_this.name="AccessTokenExpired";_this.solutions=[];Object.setPrototypeOf(_this,AuthorizationErrors.AccessTokenExpired.prototype);var message=typeof messageOrError==="string"?messageOrError:_this.explanation;var originalError=typeof messageOrError==="object"?messageOrError:error;_this.message=_this.name+" ("+_this.code+"): "+message;_this.originalError=originalError;return _this}return AccessTokenExpired}(twilioError_1.default);AuthorizationErrors.AccessTokenExpired=AccessTokenExpired;var AuthenticationFailed=function(_super){__extends(AuthenticationFailed,_super);function AuthenticationFailed(messageOrError,error){var _this=_super.call(this,messageOrError,error)||this;_this.causes=[];_this.code=20151;_this.description="Authentication Failed";_this.explanation="The Authentication with the provided JWT failed";_this.name="AuthenticationFailed";_this.solutions=[];Object.setPrototypeOf(_this,AuthorizationErrors.AuthenticationFailed.prototype);var message=typeof messageOrError==="string"?messageOrError:_this.explanation;var originalError=typeof messageOrError==="object"?messageOrError:error;_this.message=_this.name+" ("+_this.code+"): "+message;_this.originalError=originalError;return _this}return AuthenticationFailed}(twilioError_1.default);AuthorizationErrors.AuthenticationFailed=AuthenticationFailed})(AuthorizationErrors=exports.AuthorizationErrors||(exports.AuthorizationErrors={}));var ClientErrors;(function(ClientErrors){var BadRequest=function(_super){__extends(BadRequest,_super);function BadRequest(messageOrError,error){var _this=_super.call(this,messageOrError,error)||this;_this.causes=[];_this.code=31400;_this.description="Bad Request (HTTP/SIP)";_this.explanation="The request could not be understood due to malformed syntax.";_this.name="BadRequest";_this.solutions=[];Object.setPrototypeOf(_this,ClientErrors.BadRequest.prototype);var message=typeof messageOrError==="string"?messageOrError:_this.explanation;var originalError=typeof messageOrError==="object"?messageOrError:error;_this.message=_this.name+" ("+_this.code+"): "+message;_this.originalError=originalError;return _this}return BadRequest}(twilioError_1.default);ClientErrors.BadRequest=BadRequest})(ClientErrors=exports.ClientErrors||(exports.ClientErrors={}));var GeneralErrors;(function(GeneralErrors){var UnknownError=function(_super){__extends(UnknownError,_super);function UnknownError(messageOrError,error){var _this=_super.call(this,messageOrError,error)||this;_this.causes=[];_this.code=31e3;_this.description="Unknown Error";_this.explanation="An unknown error has occurred. See error details for more information.";_this.name="UnknownError";_this.solutions=[];Object.setPrototypeOf(_this,GeneralErrors.UnknownError.prototype);var message=typeof messageOrError==="string"?messageOrError:_this.explanation;var originalError=typeof messageOrError==="object"?messageOrError:error;_this.message=_this.name+" ("+_this.code+"): "+message;_this.originalError=originalError;return _this}return UnknownError}(twilioError_1.default);GeneralErrors.UnknownError=UnknownError;var ConnectionError=function(_super){__extends(ConnectionError,_super);function ConnectionError(messageOrError,error){var _this=_super.call(this,messageOrError,error)||this;_this.causes=[];_this.code=31005;_this.description="Connection error";_this.explanation="A connection error occurred during the call";_this.name="ConnectionError";_this.solutions=[];Object.setPrototypeOf(_this,GeneralErrors.ConnectionError.prototype);var message=typeof messageOrError==="string"?messageOrError:_this.explanation;var originalError=typeof messageOrError==="object"?messageOrError:error;_this.message=_this.name+" ("+_this.code+"): "+message;_this.originalError=originalError;return _this}return ConnectionError}(twilioError_1.default);GeneralErrors.ConnectionError=ConnectionError;var CallCancelledError=function(_super){__extends(CallCancelledError,_super);function CallCancelledError(messageOrError,error){var _this=_super.call(this,messageOrError,error)||this;_this.causes=["The incoming call was cancelled because it was not answered in time or it was accepted/rejected by another application instance registered with the same identity."];_this.code=31008;_this.description="Call cancelled";_this.explanation="Unable to answer because the call has ended";_this.name="CallCancelledError";_this.solutions=[];Object.setPrototypeOf(_this,GeneralErrors.CallCancelledError.prototype);var message=typeof messageOrError==="string"?messageOrError:_this.explanation;var originalError=typeof messageOrError==="object"?messageOrError:error;_this.message=_this.name+" ("+_this.code+"): "+message;_this.originalError=originalError;return _this}return CallCancelledError}(twilioError_1.default);GeneralErrors.CallCancelledError=CallCancelledError;var TransportError=function(_super){__extends(TransportError,_super);function TransportError(messageOrError,error){var _this=_super.call(this,messageOrError,error)||this;_this.causes=[];_this.code=31009;_this.description="Transport error";_this.explanation="No transport available to send or receive messages";_this.name="TransportError";_this.solutions=[];Object.setPrototypeOf(_this,GeneralErrors.TransportError.prototype);var message=typeof messageOrError==="string"?messageOrError:_this.explanation;var originalError=typeof messageOrError==="object"?messageOrError:error;_this.message=_this.name+" ("+_this.code+"): "+message;_this.originalError=originalError;return _this}return TransportError}(twilioError_1.default);GeneralErrors.TransportError=TransportError})(GeneralErrors=exports.GeneralErrors||(exports.GeneralErrors={}));var MalformedRequestErrors;(function(MalformedRequestErrors){var MalformedRequestError=function(_super){__extends(MalformedRequestError,_super);function MalformedRequestError(messageOrError,error){var _this=_super.call(this,messageOrError,error)||this;_this.causes=["Invalid content or MessageType passed to sendMessage method."];_this.code=31100;_this.description="The request had malformed syntax.";_this.explanation="The request could not be understood due to malformed syntax.";_this.name="MalformedRequestError";_this.solutions=["Ensure content and MessageType passed to sendMessage method are valid."];Object.setPrototypeOf(_this,MalformedRequestErrors.MalformedRequestError.prototype);var message=typeof messageOrError==="string"?messageOrError:_this.explanation;var originalError=typeof messageOrError==="object"?messageOrError:error;_this.message=_this.name+" ("+_this.code+"): "+message;_this.originalError=originalError;return _this}return MalformedRequestError}(twilioError_1.default);MalformedRequestErrors.MalformedRequestError=MalformedRequestError})(MalformedRequestErrors=exports.MalformedRequestErrors||(exports.MalformedRequestErrors={}));(function(AuthorizationErrors){var RateExceededError=function(_super){__extends(RateExceededError,_super);function RateExceededError(messageOrError,error){var _this=_super.call(this,messageOrError,error)||this;_this.causes=["Rate limit exceeded."];_this.code=31206;_this.description="Rate exceeded authorized limit.";_this.explanation="The request performed exceeds the authorized limit.";_this.name="RateExceededError";_this.solutions=["Ensure message send rate does not exceed authorized limits."];Object.setPrototypeOf(_this,AuthorizationErrors.RateExceededError.prototype);var message=typeof messageOrError==="string"?messageOrError:_this.explanation;var originalError=typeof messageOrError==="object"?messageOrError:error;_this.message=_this.name+" ("+_this.code+"): "+message;_this.originalError=originalError;return _this}return RateExceededError}(twilioError_1.default);AuthorizationErrors.RateExceededError=RateExceededError;var PayloadSizeExceededError=function(_super){__extends(PayloadSizeExceededError,_super);function PayloadSizeExceededError(messageOrError,error){var _this=_super.call(this,messageOrError,error)||this;_this.causes=["The payload size of Call Message Event exceeds the authorized limit."];_this.code=31209;_this.description="Call Message Event Payload size exceeded authorized limit.";_this.explanation="The request performed to send a Call Message Event exceeds the payload size authorized limit";_this.name="PayloadSizeExceededError";_this.solutions=["Reduce payload size of Call Message Event to be within the authorized limit and try again."];Object.setPrototypeOf(_this,AuthorizationErrors.PayloadSizeExceededError.prototype);var message=typeof messageOrError==="string"?messageOrError:_this.explanation;var originalError=typeof messageOrError==="object"?messageOrError:error;_this.message=_this.name+" ("+_this.code+"): "+message;_this.originalError=originalError;return _this}return PayloadSizeExceededError}(twilioError_1.default);AuthorizationErrors.PayloadSizeExceededError=PayloadSizeExceededError})(AuthorizationErrors=exports.AuthorizationErrors||(exports.AuthorizationErrors={}));var UserMediaErrors;(function(UserMediaErrors){var PermissionDeniedError=function(_super){__extends(PermissionDeniedError,_super);function PermissionDeniedError(messageOrError,error){var _this=_super.call(this,messageOrError,error)||this;_this.causes=["The user denied the getUserMedia request.","The browser denied the getUserMedia request."];_this.code=31401;_this.description="UserMedia Permission Denied Error";_this.explanation="The browser or end-user denied permissions to user media. Therefore we were unable to acquire input audio.";_this.name="PermissionDeniedError";_this.solutions=["The user should accept the request next time prompted. If the browser saved the deny, the user should change that permission in their browser.","The user should to verify that the browser has permission to access the microphone at this address."];Object.setPrototypeOf(_this,UserMediaErrors.PermissionDeniedError.prototype);var message=typeof messageOrError==="string"?messageOrError:_this.explanation;var originalError=typeof messageOrError==="object"?messageOrError:error;_this.message=_this.name+" ("+_this.code+"): "+message;_this.originalError=originalError;return _this}return PermissionDeniedError}(twilioError_1.default);UserMediaErrors.PermissionDeniedError=PermissionDeniedError;var AcquisitionFailedError=function(_super){__extends(AcquisitionFailedError,_super);function AcquisitionFailedError(messageOrError,error){var _this=_super.call(this,messageOrError,error)||this;_this.causes=["NotFoundError - The deviceID specified was not found.","The getUserMedia constraints were overconstrained and no devices matched."];_this.code=31402;_this.description="UserMedia Acquisition Failed Error";_this.explanation="The browser and end-user allowed permissions, however getting the media failed. Usually this is due to bad constraints, but can sometimes fail due to browser, OS or hardware issues.";_this.name="AcquisitionFailedError";_this.solutions=["Ensure the deviceID being specified exists.","Try acquiring media with fewer constraints."];Object.setPrototypeOf(_this,UserMediaErrors.AcquisitionFailedError.prototype);var message=typeof messageOrError==="string"?messageOrError:_this.explanation;var originalError=typeof messageOrError==="object"?messageOrError:error;_this.message=_this.name+" ("+_this.code+"): "+message;_this.originalError=originalError;return _this}return AcquisitionFailedError}(twilioError_1.default);UserMediaErrors.AcquisitionFailedError=AcquisitionFailedError})(UserMediaErrors=exports.UserMediaErrors||(exports.UserMediaErrors={}));var SignalingErrors;(function(SignalingErrors){var ConnectionError=function(_super){__extends(ConnectionError,_super);function ConnectionError(messageOrError,error){var _this=_super.call(this,messageOrError,error)||this;_this.causes=[];_this.code=53e3;_this.description="Signaling connection error";_this.explanation="Raised whenever a signaling connection error occurs that is not covered by a more specific error code.";_this.name="ConnectionError";_this.solutions=[];Object.setPrototypeOf(_this,SignalingErrors.ConnectionError.prototype);var message=typeof messageOrError==="string"?messageOrError:_this.explanation;var originalError=typeof messageOrError==="object"?messageOrError:error;_this.message=_this.name+" ("+_this.code+"): "+message;_this.originalError=originalError;return _this}return ConnectionError}(twilioError_1.default);SignalingErrors.ConnectionError=ConnectionError;var ConnectionDisconnected=function(_super){__extends(ConnectionDisconnected,_super);function ConnectionDisconnected(messageOrError,error){var _this=_super.call(this,messageOrError,error)||this;_this.causes=["The device running your application lost its Internet connection."];_this.code=53001;_this.description="Signaling connection disconnected";_this.explanation="Raised whenever the signaling connection is unexpectedly disconnected.";_this.name="ConnectionDisconnected";_this.solutions=["Ensure the device running your application has access to a stable Internet connection."];Object.setPrototypeOf(_this,SignalingErrors.ConnectionDisconnected.prototype);var message=typeof messageOrError==="string"?messageOrError:_this.explanation;var originalError=typeof messageOrError==="object"?messageOrError:error;_this.message=_this.name+" ("+_this.code+"): "+message;_this.originalError=originalError;return _this}return ConnectionDisconnected}(twilioError_1.default);SignalingErrors.ConnectionDisconnected=ConnectionDisconnected})(SignalingErrors=exports.SignalingErrors||(exports.SignalingErrors={}));var MediaErrors;(function(MediaErrors){var ClientLocalDescFailed=function(_super){__extends(ClientLocalDescFailed,_super);function ClientLocalDescFailed(messageOrError,error){var _this=_super.call(this,messageOrError,error)||this;_this.causes=["The Client may not be using a supported WebRTC implementation.","The Client may not have the necessary resources to create or apply a new media description."];_this.code=53400;_this.description="Client is unable to create or apply a local media description";_this.explanation="Raised whenever a Client is unable to create or apply a local media description.";_this.name="ClientLocalDescFailed";_this.solutions=["If you are experiencing this error using the JavaScript SDK, ensure you are running it with a supported WebRTC implementation."];Object.setPrototypeOf(_this,MediaErrors.ClientLocalDescFailed.prototype);var message=typeof messageOrError==="string"?messageOrError:_this.explanation;var originalError=typeof messageOrError==="object"?messageOrError:error;_this.message=_this.name+" ("+_this.code+"): "+message;_this.originalError=originalError;return _this}return ClientLocalDescFailed}(twilioError_1.default);MediaErrors.ClientLocalDescFailed=ClientLocalDescFailed;var ClientRemoteDescFailed=function(_super){__extends(ClientRemoteDescFailed,_super);function ClientRemoteDescFailed(messageOrError,error){var _this=_super.call(this,messageOrError,error)||this;_this.causes=["The Client may not be using a supported WebRTC implementation.","The Client may be connecting peer-to-peer with another Participant that is not using a supported WebRTC implementation.","The Client may not have the necessary resources to apply a new media description."];_this.code=53402;_this.description="Client is unable to apply a remote media description";_this.explanation="Raised whenever the Client receives a remote media description but is unable to apply it.";_this.name="ClientRemoteDescFailed";_this.solutions=["If you are experiencing this error using the JavaScript SDK, ensure you are running it with a supported WebRTC implementation."];Object.setPrototypeOf(_this,MediaErrors.ClientRemoteDescFailed.prototype);var message=typeof messageOrError==="string"?messageOrError:_this.explanation;var originalError=typeof messageOrError==="object"?messageOrError:error;_this.message=_this.name+" ("+_this.code+"): "+message;_this.originalError=originalError;return _this}return ClientRemoteDescFailed}(twilioError_1.default);MediaErrors.ClientRemoteDescFailed=ClientRemoteDescFailed;var ConnectionError=function(_super){__extends(ConnectionError,_super);function ConnectionError(messageOrError,error){var _this=_super.call(this,messageOrError,error)||this;_this.causes=["The Client was unable to establish a media connection.","A media connection which was active failed liveliness checks."];_this.code=53405;_this.description="Media connection failed";_this.explanation="Raised by the Client or Server whenever a media connection fails.";_this.name="ConnectionError";_this.solutions=["If the problem persists, try connecting to another region.","Check your Client's network connectivity.","If you've provided custom ICE Servers then ensure that the URLs and credentials are valid."];Object.setPrototypeOf(_this,MediaErrors.ConnectionError.prototype);var message=typeof messageOrError==="string"?messageOrError:_this.explanation;var originalError=typeof messageOrError==="object"?messageOrError:error;_this.message=_this.name+" ("+_this.code+"): "+message;_this.originalError=originalError;return _this}return ConnectionError}(twilioError_1.default);MediaErrors.ConnectionError=ConnectionError})(MediaErrors=exports.MediaErrors||(exports.MediaErrors={}));exports.errorsByCode=new Map([[20101,AuthorizationErrors.AccessTokenInvalid],[20104,AuthorizationErrors.AccessTokenExpired],[20151,AuthorizationErrors.AuthenticationFailed],[31400,ClientErrors.BadRequest],[31e3,GeneralErrors.UnknownError],[31005,GeneralErrors.ConnectionError],[31008,GeneralErrors.CallCancelledError],[31009,GeneralErrors.TransportError],[31100,MalformedRequestErrors.MalformedRequestError],[31206,AuthorizationErrors.RateExceededError],[31209,AuthorizationErrors.PayloadSizeExceededError],[31401,UserMediaErrors.PermissionDeniedError],[31402,UserMediaErrors.AcquisitionFailedError],[53e3,SignalingErrors.ConnectionError],[53001,SignalingErrors.ConnectionDisconnected],[53400,MediaErrors.ClientLocalDescFailed],[53402,MediaErrors.ClientRemoteDescFailed],[53405,MediaErrors.ConnectionError]]);Object.freeze(exports.errorsByCode)},{"./twilioError":13}],12:[function(require,module,exports){"use strict";var __extends=this&&this.__extends||function(){var extendStatics=function(d,b){extendStatics=Object.setPrototypeOf||{__proto__:[]}instanceof Array&&function(d,b){d.__proto__=b}||function(d,b){for(var p in b)if(b.hasOwnProperty(p))d[p]=b[p]};return extendStatics(d,b)};return function(d,b){extendStatics(d,b);function __(){this.constructor=d}d.prototype=b===null?Object.create(b):(__.prototype=b.prototype,new __)}}();Object.defineProperty(exports,"__esModule",{value:true});var generated_1=require("./generated");exports.AuthorizationErrors=generated_1.AuthorizationErrors;exports.ClientErrors=generated_1.ClientErrors;exports.GeneralErrors=generated_1.GeneralErrors;exports.MediaErrors=generated_1.MediaErrors;exports.SignalingErrors=generated_1.SignalingErrors;exports.TwilioError=generated_1.TwilioError;exports.UserMediaErrors=generated_1.UserMediaErrors;var InvalidArgumentError=function(_super){__extends(InvalidArgumentError,_super);function InvalidArgumentError(message){var _this=_super.call(this,message)||this;_this.name="InvalidArgumentError";return _this}return InvalidArgumentError}(Error);exports.InvalidArgumentError=InvalidArgumentError;var InvalidStateError=function(_super){__extends(InvalidStateError,_super);function InvalidStateError(message){var _this=_super.call(this,message)||this;_this.name="InvalidStateError";return _this}return InvalidStateError}(Error);exports.InvalidStateError=InvalidStateError;var NotSupportedError=function(_super){__extends(NotSupportedError,_super);function NotSupportedError(message){var _this=_super.call(this,message)||this;_this.name="NotSupportedError";return _this}return NotSupportedError}(Error);exports.NotSupportedError=NotSupportedError;function getErrorByCode(code){var error=generated_1.errorsByCode.get(code);if(!error){throw new InvalidArgumentError("Error code "+code+" not found")}return error}exports.getErrorByCode=getErrorByCode;function hasErrorByCode(code){return generated_1.errorsByCode.has(code)}exports.hasErrorByCode=hasErrorByCode},{"./generated":11}],13:[function(require,module,exports){"use strict";var __extends=this&&this.__extends||function(){var extendStatics=function(d,b){extendStatics=Object.setPrototypeOf||{__proto__:[]}instanceof Array&&function(d,b){d.__proto__=b}||function(d,b){for(var p in b)if(b.hasOwnProperty(p))d[p]=b[p]};return extendStatics(d,b)};return function(d,b){extendStatics(d,b);function __(){this.constructor=d}d.prototype=b===null?Object.create(b):(__.prototype=b.prototype,new __)}}();Object.defineProperty(exports,"__esModule",{value:true});var TwilioError=function(_super){__extends(TwilioError,_super);function TwilioError(messageOrError,error){var _this=_super.call(this)||this;Object.setPrototypeOf(_this,TwilioError.prototype);var message=typeof messageOrError==="string"?messageOrError:_this.explanation;var originalError=typeof messageOrError==="object"?messageOrError:error;_this.message=_this.name+" ("+_this.code+"): "+message;_this.originalError=originalError;return _this}return TwilioError}(Error);exports.default=TwilioError},{}],14:[function(require,module,exports){"use strict";function _classCallCheck(instance,Constructor){if(!(instance instanceof Constructor)){throw new TypeError("Cannot call a class as a function")}}function _possibleConstructorReturn(self,call){if(!self){throw new ReferenceError("this hasn't been initialised - super() hasn't been called")}return call&&(typeof call==="object"||typeof call==="function")?call:self}function _inherits(subClass,superClass){if(typeof superClass!=="function"&&superClass!==null){throw new TypeError("Super expression must either be null or a function, not "+typeof superClass)}subClass.prototype=Object.create(superClass&&superClass.prototype,{constructor:{value:subClass,enumerable:false,writable:true,configurable:true}});if(superClass)Object.setPrototypeOf?Object.setPrototypeOf(subClass,superClass):subClass.__proto__=superClass}var EventEmitter=require("events").EventEmitter;var request=require("./request");var EventPublisher=function(_EventEmitter){_inherits(EventPublisher,_EventEmitter);function EventPublisher(productName,token,options){_classCallCheck(this,EventPublisher);var _this=_possibleConstructorReturn(this,(EventPublisher.__proto__||Object.getPrototypeOf(EventPublisher)).call(this));if(!(_this instanceof EventPublisher)){var _ret;return _ret=new EventPublisher(productName,token,options),_possibleConstructorReturn(_this,_ret)}options=Object.assign({defaultPayload:function defaultPayload(){return{}}},options);var defaultPayload=options.defaultPayload;if(typeof defaultPayload!=="function"){defaultPayload=function defaultPayload(){return Object.assign({},options.defaultPayload)}}var isEnabled=true;var metadata=Object.assign({app_name:undefined,app_version:undefined},options.metadata);Object.defineProperties(_this,{_defaultPayload:{value:defaultPayload},_isEnabled:{get:function get(){return isEnabled},set:function set(_isEnabled){isEnabled=_isEnabled}},_host:{value:options.host,writable:true},_log:{value:options.log},_request:{value:options.request||request,writable:true},_token:{value:token,writable:true},isEnabled:{enumerable:true,get:function get(){return isEnabled}},metadata:{enumerable:true,get:function get(){return metadata}},productName:{enumerable:true,value:productName},token:{enumerable:true,get:function get(){return this._token}}});return _this}return EventPublisher}(EventEmitter);EventPublisher.prototype._post=function _post(endpointName,level,group,name,payload,connection,force){var _this2=this;if(!this.isEnabled&&!force||!this._host){return Promise.resolve()}if(!connection||(!connection.parameters||!connection.parameters.CallSid)&&!connection.outboundConnectionId){return Promise.resolve()}var event={publisher:this.productName,group:group,name:name,timestamp:(new Date).toISOString(),level:level.toUpperCase(),payload_type:"application/json",private:false,payload:payload&&payload.forEach?payload.slice(0):Object.assign(this._defaultPayload(connection),payload)};if(this.metadata){event.publisher_metadata=this.metadata}var requestParams={url:"https://"+this._host+"/v4/"+endpointName,body:event,headers:{"Content-Type":"application/json","X-Twilio-Token":this.token}};return new Promise(function(resolve,reject){_this2._request.post(requestParams,function(err){if(err){_this2.emit("error",err);reject(err)}else{resolve()}})}).catch(function(e){_this2._log.warn("Unable to post "+group+" "+name+" event to Insights. Received error: "+e)})};EventPublisher.prototype.post=function post(level,group,name,payload,connection,force){return this._post("EndpointEvents",level,group,name,payload,connection,force)};EventPublisher.prototype.debug=function debug(group,name,payload,connection){return this.post("debug",group,name,payload,connection)};EventPublisher.prototype.info=function info(group,name,payload,connection){return this.post("info",group,name,payload,connection)};EventPublisher.prototype.warn=function warn(group,name,payload,connection){return this.post("warning",group,name,payload,connection)};EventPublisher.prototype.error=function error(group,name,payload,connection){return this.post("error",group,name,payload,connection)};EventPublisher.prototype.postMetrics=function postMetrics(group,name,metrics,customFields,connection){var _this3=this;return new Promise(function(resolve){var samples=metrics.map(formatMetric).map(function(sample){return Object.assign(sample,customFields)});resolve(_this3._post("EndpointMetrics","info",group,name,samples,connection))})};EventPublisher.prototype.setHost=function setHost(host){this._host=host};EventPublisher.prototype.setToken=function setToken(token){this._token=token};EventPublisher.prototype.enable=function enable(){this._isEnabled=true};EventPublisher.prototype.disable=function disable(){this._isEnabled=false};function formatMetric(sample){return{timestamp:new Date(sample.timestamp).toISOString(),total_packets_received:sample.totals.packetsReceived,total_packets_lost:sample.totals.packetsLost,total_packets_sent:sample.totals.packetsSent,total_bytes_received:sample.totals.bytesReceived,total_bytes_sent:sample.totals.bytesSent,packets_received:sample.packetsReceived,packets_lost:sample.packetsLost,packets_lost_fraction:sample.packetsLostFraction&&Math.round(sample.packetsLostFraction*100)/100,bytes_received:sample.bytesReceived,bytes_sent:sample.bytesSent,audio_codec:sample.codecName,audio_level_in:sample.audioInputLevel,audio_level_out:sample.audioOutputLevel,call_volume_input:sample.inputVolume,call_volume_output:sample.outputVolume,jitter:sample.jitter,rtt:sample.rtt,mos:sample.mos&&Math.round(sample.mos*100)/100}}module.exports=EventPublisher},{"./request":20,events:53}],15:[function(require,module,exports){"use strict";Object.defineProperty(exports,"__esModule",{value:true});var LogLevelModule=require("loglevel");var constants_1=require("./constants");var Log=function(){function Log(options){this._log=(options&&options.LogLevelModule?options.LogLevelModule:LogLevelModule).getLogger(constants_1.PACKAGE_NAME)}Log.getInstance=function(){if(!Log.instance){Log.instance=new Log}return Log.instance};Log.prototype.debug=function(){var _a;var args=[];for(var _i=0;_i0&&t[t.length-1])&&(op[0]===6||op[0]===2)){_=0;continue}if(op[0]===3&&(!t||op[1]>t[0]&&op[1]4.2){return PreflightTest.CallQuality.Excellent}else if(mos>=4.1&&mos<=4.2){return PreflightTest.CallQuality.Great}else if(mos>=3.7&&mos<=4){return PreflightTest.CallQuality.Good}else if(mos>=3.1&&mos<=3.6){return PreflightTest.CallQuality.Fair}else{return PreflightTest.CallQuality.Degraded}};PreflightTest.prototype._getReport=function(){var stats=this._getRTCStats();var testTiming={start:this._startTime};if(this._endTime){testTiming.end=this._endTime;testTiming.duration=this._endTime-this._startTime}var report={callSid:this._callSid,edge:this._edge,iceCandidateStats:this._rtcIceCandidateStatsReport.iceCandidateStats,networkTiming:this._networkTiming,samples:this._samples,selectedEdge:this._options.edge,stats:stats,testTiming:testTiming,totals:this._getRTCSampleTotals(),warnings:this._warnings};var selectedIceCandidatePairStats=this._rtcIceCandidateStatsReport.selectedIceCandidatePairStats;if(selectedIceCandidatePairStats){report.selectedIceCandidatePairStats=selectedIceCandidatePairStats;report.isTurnRequired=selectedIceCandidatePairStats.localCandidate.candidateType==="relay"||selectedIceCandidatePairStats.remoteCandidate.candidateType==="relay"}if(stats){report.callQuality=this._getCallQuality(stats.mos.average)}return report};PreflightTest.prototype._getRTCSampleTotals=function(){if(!this._latestSample){return}return __assign({},this._latestSample.totals)};PreflightTest.prototype._getRTCStats=function(){var firstMosSampleIdx=this._samples.findIndex(function(sample){return typeof sample.mos==="number"&&sample.mos>0});var samples=firstMosSampleIdx>=0?this._samples.slice(firstMosSampleIdx):[];if(!samples||!samples.length){return}return["jitter","mos","rtt"].reduce(function(statObj,stat){var _a;var values=samples.map(function(s){return s[stat]});return __assign(__assign({},statObj),(_a={},_a[stat]={average:Number((values.reduce(function(total,value){return total+value})/values.length).toPrecision(5)),max:Math.max.apply(Math,values),min:Math.min.apply(Math,values)},_a))},{})};PreflightTest.prototype._getStreamFromFile=function(){var audioContext=this._options.audioContext;if(!audioContext){throw new errors_1.NotSupportedError("Cannot fake input audio stream: AudioContext is not supported by this browser.")}var audioEl=new Audio(COWBELL_AUDIO_URL);audioEl.addEventListener("canplaythrough",function(){return audioEl.play()});if(typeof audioEl.setAttribute==="function"){audioEl.setAttribute("crossorigin","anonymous")}var src=audioContext.createMediaElementSource(audioEl);var dest=audioContext.createMediaStreamDestination();src.connect(dest);return dest.stream};PreflightTest.prototype._initDevice=function(token,options){var _this=this;try{this._device=new(options.deviceFactory||device_1.default)(token,{codecPreferences:options.codecPreferences,edge:options.edge,fileInputStream:options.fileInputStream,logLevel:options.logLevel,preflight:true});this._device.once(device_1.default.EventName.Registered,function(){_this._onDeviceRegistered()});this._device.once(device_1.default.EventName.Error,function(error){_this._onDeviceError(error)});this._device.register()}catch(error){setTimeout(function(){_this._onFailed(error)});return}this._signalingTimeoutTimer=setTimeout(function(){_this._onDeviceError(new errors_1.SignalingErrors.ConnectionError("WebSocket Connection Timeout"))},options.signalingTimeoutMs)};PreflightTest.prototype._onDeviceError=function(error){this._device.destroy();this._onFailed(error)};PreflightTest.prototype._onDeviceRegistered=function(){return __awaiter(this,void 0,void 0,function(){var _a,audio,publisher;var _this=this;return __generator(this,function(_b){switch(_b.label){case 0:clearTimeout(this._echoTimer);clearTimeout(this._signalingTimeoutTimer);_a=this;return[4,this._device.connect({rtcConfiguration:this._options.rtcConfiguration})];case 1:_a._call=_b.sent();this._networkTiming.signaling={start:Date.now()};this._setupCallHandlers(this._call);this._edge=this._device.edge||undefined;if(this._options.fakeMicInput){this._echoTimer=setTimeout(function(){return _this._device.disconnectAll()},ECHO_TEST_DURATION);audio=this._device.audio;if(audio){audio.disconnect(false);audio.outgoing(false)}}this._call.once("disconnect",function(){_this._device.once(device_1.default.EventName.Unregistered,function(){return _this._onUnregistered()});_this._device.destroy()});publisher=this._call["_publisher"];publisher.on("error",function(){if(!_this._hasInsightsErrored){_this._emitWarning("insights-connection-error","Received an error when attempting to connect to Insights gateway")}_this._hasInsightsErrored=true});return[2]}})})};PreflightTest.prototype._onFailed=function(error){clearTimeout(this._echoTimer);clearTimeout(this._signalingTimeoutTimer);this._releaseHandlers();this._endTime=Date.now();this._status=PreflightTest.Status.Failed;this.emit(PreflightTest.Events.Failed,error)};PreflightTest.prototype._onUnregistered=function(){var _this=this;setTimeout(function(){if(_this._status===PreflightTest.Status.Failed){return}clearTimeout(_this._echoTimer);clearTimeout(_this._signalingTimeoutTimer);_this._releaseHandlers();_this._endTime=Date.now();_this._status=PreflightTest.Status.Completed;_this._report=_this._getReport();_this.emit(PreflightTest.Events.Completed,_this._report)},10)};PreflightTest.prototype._releaseHandlers=function(){[this._device,this._call].forEach(function(emitter){if(emitter){emitter.eventNames().forEach(function(name){return emitter.removeAllListeners(name)})}})};PreflightTest.prototype._setupCallHandlers=function(call){var _this=this;if(this._options.fakeMicInput){call.once("volume",function(){call["_mediaHandler"].outputs.forEach(function(output){return output.audio.muted=true})})}call.on("warning",function(name,data){_this._emitWarning(name,"Received an RTCWarning. See .rtcWarning for the RTCWarning",data)});call.once("accept",function(){_this._callSid=call["_mediaHandler"].callSid;_this._status=PreflightTest.Status.Connected;_this.emit(PreflightTest.Events.Connected)});call.on("sample",function(sample){return __awaiter(_this,void 0,void 0,function(){var _a;return __generator(this,function(_b){switch(_b.label){case 0:if(!!this._latestSample)return[3,2];_a=this;return[4,(this._options.getRTCIceCandidateStatsReport||stats_1.getRTCIceCandidateStatsReport)(call["_mediaHandler"].version.pc)];case 1:_a._rtcIceCandidateStatsReport=_b.sent();_b.label=2;case 2:this._latestSample=sample;this._samples.push(sample);this.emit(PreflightTest.Events.Sample,sample);return[2]}})})});[{reportLabel:"peerConnection",type:"pcconnection"},{reportLabel:"ice",type:"iceconnection"},{reportLabel:"dtls",type:"dtlstransport"},{reportLabel:"signaling",type:"signaling"}].forEach(function(_a){var type=_a.type,reportLabel=_a.reportLabel;var handlerName="on"+type+"statechange";var originalHandler=call["_mediaHandler"][handlerName];call["_mediaHandler"][handlerName]=function(state){var timing=_this._networkTiming[reportLabel]=_this._networkTiming[reportLabel]||{start:0};if(state==="connecting"||state==="checking"){timing.start=Date.now()}else if((state==="connected"||state==="stable")&&!timing.duration){timing.end=Date.now();timing.duration=timing.end-timing.start}originalHandler(state)}})};Object.defineProperty(PreflightTest.prototype,"callSid",{get:function(){return this._callSid},enumerable:true,configurable:true});Object.defineProperty(PreflightTest.prototype,"endTime",{get:function(){return this._endTime},enumerable:true,configurable:true});Object.defineProperty(PreflightTest.prototype,"latestSample",{get:function(){return this._latestSample},enumerable:true,configurable:true});Object.defineProperty(PreflightTest.prototype,"report",{get:function(){return this._report},enumerable:true,configurable:true});Object.defineProperty(PreflightTest.prototype,"startTime",{get:function(){return this._startTime},enumerable:true,configurable:true});Object.defineProperty(PreflightTest.prototype,"status",{get:function(){return this._status},enumerable:true,configurable:true});return PreflightTest}(events_1.EventEmitter);exports.PreflightTest=PreflightTest;(function(PreflightTest){var CallQuality;(function(CallQuality){CallQuality["Excellent"]="excellent";CallQuality["Great"]="great";CallQuality["Good"]="good";CallQuality["Fair"]="fair";CallQuality["Degraded"]="degraded"})(CallQuality=PreflightTest.CallQuality||(PreflightTest.CallQuality={}));var Events;(function(Events){Events["Completed"]="completed";Events["Connected"]="connected";Events["Failed"]="failed";Events["Sample"]="sample";Events["Warning"]="warning"})(Events=PreflightTest.Events||(PreflightTest.Events={}));var Status;(function(Status){Status["Connecting"]="connecting";Status["Connected"]="connected";Status["Completed"]="completed";Status["Failed"]="failed"})(Status=PreflightTest.Status||(PreflightTest.Status={}))})(PreflightTest=exports.PreflightTest||(exports.PreflightTest={}));exports.PreflightTest=PreflightTest},{"../call":6,"../constants":7,"../device":9,"../errors":12,"../rtc/stats":29,events:53}],18:[function(require,module,exports){"use strict";function _toConsumableArray(arr){if(Array.isArray(arr)){for(var i=0,arr2=Array(arr.length);i2&&arguments[2]!==undefined?arguments[2]:"application/json";var messagetype=arguments[3];var voiceeventsid=arguments[4];var payload={callsid:callsid,content:content,contenttype:contenttype,messagetype:messagetype,voiceeventsid:voiceeventsid};this._publish("message",payload,true)};PStream.prototype.register=function(mediaCapabilities){var regPayload={media:mediaCapabilities};this._publish("register",regPayload,true)};PStream.prototype.invite=function(sdp,callsid,preflight,params){var payload={callsid:callsid,sdp:sdp,preflight:!!preflight,twilio:params?{params:params}:{}};this._publish("invite",payload,true)};PStream.prototype.reconnect=function(sdp,callsid,reconnect,params){var payload={callsid:callsid,sdp:sdp,reconnect:reconnect,preflight:false,twilio:params?{params:params}:{}};this._publish("invite",payload,true)};PStream.prototype.answer=function(sdp,callsid){this._publish("answer",{sdp:sdp,callsid:callsid},true)};PStream.prototype.dtmf=function(callsid,digits){this._publish("dtmf",{callsid:callsid,dtmf:digits},true)};PStream.prototype.hangup=function(callsid,message){var payload=message?{callsid:callsid,message:message}:{callsid:callsid};this._publish("hangup",payload,true)};PStream.prototype.reject=function(callsid){this._publish("reject",{callsid:callsid},true)};PStream.prototype.reinvite=function(sdp,callsid){this._publish("reinvite",{sdp:sdp,callsid:callsid},false)};PStream.prototype._destroy=function(){this.transport.removeListener("close",this._handleTransportClose);this.transport.removeListener("error",this._handleTransportError);this.transport.removeListener("message",this._handleTransportMessage);this.transport.removeListener("open",this._handleTransportOpen);this.transport.close();this.emit("offline",this)};PStream.prototype.destroy=function(){this._log.info("PStream.destroy() called...");this._destroy();return this};PStream.prototype.updatePreferredURI=function(uri){this._preferredUri=uri;this.transport.updatePreferredURI(uri)};PStream.prototype.updateURIs=function(uris){this._uris=uris;this.transport.updateURIs(this._uris)};PStream.prototype.publish=function(type,payload){return this._publish(type,payload,true)};PStream.prototype._publish=function(type,payload,shouldRetry){var msg=JSON.stringify({type:type,version:PSTREAM_VERSION,payload:payload});var isSent=!!this.transport.send(msg);if(!isSent){this.emit("error",{error:{code:31009,message:"No transport available to send or receive messages",twilioError:new GeneralErrors.TransportError}});if(shouldRetry){this._messageQueue.push([type,payload,true])}}};function getBrowserInfo(){var nav=typeof navigator!=="undefined"?navigator:{};var info={p:"browser",v:C.RELEASE_VERSION,browser:{userAgent:nav.userAgent||"unknown",platform:nav.platform||"unknown"},plugin:"rtc"};return info}module.exports=PStream},{"./constants":7,"./errors":12,"./log":15,"./wstransport":37,events:53}],19:[function(require,module,exports){"use strict";var _a,_b,_c,_d;Object.defineProperty(exports,"__esModule",{value:true});var errors_1=require("./errors");var DeprecatedRegion;(function(DeprecatedRegion){DeprecatedRegion["Au"]="au";DeprecatedRegion["Br"]="br";DeprecatedRegion["Ie"]="ie";DeprecatedRegion["Jp"]="jp";DeprecatedRegion["Sg"]="sg";DeprecatedRegion["UsOr"]="us-or";DeprecatedRegion["UsVa"]="us-va"})(DeprecatedRegion=exports.DeprecatedRegion||(exports.DeprecatedRegion={}));var Edge;(function(Edge){Edge["Sydney"]="sydney";Edge["SaoPaulo"]="sao-paulo";Edge["Dublin"]="dublin";Edge["Frankfurt"]="frankfurt";Edge["Tokyo"]="tokyo";Edge["Singapore"]="singapore";Edge["Ashburn"]="ashburn";Edge["Umatilla"]="umatilla";Edge["Roaming"]="roaming";Edge["AshburnIx"]="ashburn-ix";Edge["SanJoseIx"]="san-jose-ix";Edge["LondonIx"]="london-ix";Edge["FrankfurtIx"]="frankfurt-ix";Edge["SingaporeIx"]="singapore-ix";Edge["SydneyIx"]="sydney-ix";Edge["TokyoIx"]="tokyo-ix"})(Edge=exports.Edge||(exports.Edge={}));var Region;(function(Region){Region["Au1"]="au1";Region["Au1Ix"]="au1-ix";Region["Br1"]="br1";Region["De1"]="de1";Region["De1Ix"]="de1-ix";Region["Gll"]="gll";Region["Ie1"]="ie1";Region["Ie1Ix"]="ie1-ix";Region["Ie1Tnx"]="ie1-tnx";Region["Jp1"]="jp1";Region["Jp1Ix"]="jp1-ix";Region["Sg1"]="sg1";Region["Sg1Ix"]="sg1-ix";Region["Sg1Tnx"]="sg1-tnx";Region["Us1"]="us1";Region["Us1Ix"]="us1-ix";Region["Us1Tnx"]="us1-tnx";Region["Us2"]="us2";Region["Us2Ix"]="us2-ix";Region["Us2Tnx"]="us2-tnx"})(Region=exports.Region||(exports.Region={}));exports.deprecatedRegions=(_a={},_a[DeprecatedRegion.Au]=Region.Au1,_a[DeprecatedRegion.Br]=Region.Br1,_a[DeprecatedRegion.Ie]=Region.Ie1,_a[DeprecatedRegion.Jp]=Region.Jp1,_a[DeprecatedRegion.Sg]=Region.Sg1,_a[DeprecatedRegion.UsOr]=Region.Us1,_a[DeprecatedRegion.UsVa]=Region.Us1,_a);exports.regionShortcodes={ASIAPAC_SINGAPORE:Region.Sg1,ASIAPAC_SYDNEY:Region.Au1,ASIAPAC_TOKYO:Region.Jp1,EU_FRANKFURT:Region.De1,EU_IRELAND:Region.Ie1,SOUTH_AMERICA_SAO_PAULO:Region.Br1,US_EAST_VIRGINIA:Region.Us1,US_WEST_OREGON:Region.Us2};var regionURIs=(_b={},_b[Region.Au1]="chunderw-vpc-gll-au1.twilio.com",_b[Region.Au1Ix]="chunderw-vpc-gll-au1-ix.twilio.com",_b[Region.Br1]="chunderw-vpc-gll-br1.twilio.com",_b[Region.De1]="chunderw-vpc-gll-de1.twilio.com",_b[Region.De1Ix]="chunderw-vpc-gll-de1-ix.twilio.com",_b[Region.Gll]="chunderw-vpc-gll.twilio.com",_b[Region.Ie1]="chunderw-vpc-gll-ie1.twilio.com",_b[Region.Ie1Ix]="chunderw-vpc-gll-ie1-ix.twilio.com",_b[Region.Ie1Tnx]="chunderw-vpc-gll-ie1-tnx.twilio.com",_b[Region.Jp1]="chunderw-vpc-gll-jp1.twilio.com",_b[Region.Jp1Ix]="chunderw-vpc-gll-jp1-ix.twilio.com",_b[Region.Sg1]="chunderw-vpc-gll-sg1.twilio.com",_b[Region.Sg1Ix]="chunderw-vpc-gll-sg1-ix.twilio.com",_b[Region.Sg1Tnx]="chunderw-vpc-gll-sg1-tnx.twilio.com",_b[Region.Us1]="chunderw-vpc-gll-us1.twilio.com",_b[Region.Us1Ix]="chunderw-vpc-gll-us1-ix.twilio.com",_b[Region.Us1Tnx]="chunderw-vpc-gll-us1-tnx.twilio.com",_b[Region.Us2]="chunderw-vpc-gll-us2.twilio.com",_b[Region.Us2Ix]="chunderw-vpc-gll-us2-ix.twilio.com",_b[Region.Us2Tnx]="chunderw-vpc-gll-us2-tnx.twilio.com",_b);exports.edgeToRegion=(_c={},_c[Edge.Sydney]=Region.Au1,_c[Edge.SaoPaulo]=Region.Br1,_c[Edge.Dublin]=Region.Ie1,_c[Edge.Frankfurt]=Region.De1,_c[Edge.Tokyo]=Region.Jp1,_c[Edge.Singapore]=Region.Sg1,_c[Edge.Ashburn]=Region.Us1,_c[Edge.Umatilla]=Region.Us2,_c[Edge.Roaming]=Region.Gll,_c[Edge.AshburnIx]=Region.Us1Ix,_c[Edge.SanJoseIx]=Region.Us2Ix,_c[Edge.LondonIx]=Region.Ie1Ix,_c[Edge.FrankfurtIx]=Region.De1Ix,_c[Edge.SingaporeIx]=Region.Sg1Ix,_c[Edge.SydneyIx]=Region.Au1Ix,_c[Edge.TokyoIx]=Region.Jp1Ix,_c);exports.regionToEdge=(_d={},_d[Region.Au1]=Edge.Sydney,_d[Region.Br1]=Edge.SaoPaulo,_d[Region.Ie1]=Edge.Dublin,_d[Region.De1]=Edge.Frankfurt,_d[Region.Jp1]=Edge.Tokyo,_d[Region.Sg1]=Edge.Singapore,_d[Region.Us1]=Edge.Ashburn,_d[Region.Us2]=Edge.Umatilla,_d[Region.Gll]=Edge.Roaming,_d[Region.Us1Ix]=Edge.AshburnIx,_d[Region.Us2Ix]=Edge.SanJoseIx,_d[Region.Ie1Ix]=Edge.LondonIx,_d[Region.De1Ix]=Edge.FrankfurtIx,_d[Region.Sg1Ix]=Edge.SingaporeIx,_d[Region.Au1Ix]=Edge.SydneyIx,_d[Region.Jp1Ix]=Edge.TokyoIx,_d[Region.Us1Tnx]=Edge.AshburnIx,_d[Region.Us2Tnx]=Edge.AshburnIx,_d[Region.Ie1Tnx]=Edge.LondonIx,_d[Region.Sg1Tnx]=Edge.SingaporeIx,_d);exports.defaultRegion="gll";exports.defaultEdge=Edge.Roaming;exports.defaultChunderRegionURI="chunderw-vpc-gll.twilio.com";var defaultEventGatewayURI="eventgw.twilio.com";function createChunderRegionURI(region){return region===exports.defaultRegion?exports.defaultChunderRegionURI:"chunderw-vpc-gll-"+region+".twilio.com"}function createChunderEdgeURI(edge){return"voice-js."+edge+".twilio.com"}function createEventGatewayURI(region){return region?"eventgw."+region+".twilio.com":defaultEventGatewayURI}exports.createEventGatewayURI=createEventGatewayURI;function createSignalingEndpointURL(uri){return"wss://"+uri+"/signal"}exports.createSignalingEndpointURL=createSignalingEndpointURL;function getChunderURIs(edge,region,onDeprecated){if(!!region&&typeof region!=="string"){throw new errors_1.InvalidArgumentError("If `region` is provided, it must be of type `string`.")}if(!!edge&&typeof edge!=="string"&&!Array.isArray(edge)){throw new errors_1.InvalidArgumentError("If `edge` is provided, it must be of type `string` or an array of strings.")}var deprecatedMessages=[];var uris;if(region&&edge){throw new errors_1.InvalidArgumentError("You cannot specify `region` when `edge` is specified in"+"`Twilio.Device.Options`.")}else if(region){var chunderRegion=region;deprecatedMessages.push("Regions are deprecated in favor of edges. Please see this page for "+"documentation: https://www.twilio.com/docs/voice/client/edges.");var isDeprecatedRegion=Object.values(DeprecatedRegion).includes(chunderRegion);if(isDeprecatedRegion){chunderRegion=exports.deprecatedRegions[chunderRegion]}var isKnownRegion=Object.values(Region).includes(chunderRegion);if(isKnownRegion){var preferredEdge=exports.regionToEdge[chunderRegion];deprecatedMessages.push('Region "'+chunderRegion+'" is deprecated, please use `edge` '+('"'+preferredEdge+'".'))}uris=[createChunderRegionURI(chunderRegion)]}else if(edge){var edgeValues_1=Object.values(Edge);var edgeParams=Array.isArray(edge)?edge:[edge];uris=edgeParams.map(function(param){return edgeValues_1.includes(param)?createChunderRegionURI(exports.edgeToRegion[param]):createChunderEdgeURI(param)})}else{uris=[exports.defaultChunderRegionURI]}if(onDeprecated&&deprecatedMessages.length){setTimeout(function(){return onDeprecated(deprecatedMessages.join("\n"))})}return uris}exports.getChunderURIs=getChunderURIs;function getRegionShortcode(region){return exports.regionShortcodes[region]||null}exports.getRegionShortcode=getRegionShortcode},{"./errors":12}],20:[function(require,module,exports){"use strict";var XHR=require("xmlhttprequest").XMLHttpRequest;function request(method,params,callback){var options={};options.XMLHttpRequest=options.XMLHttpRequest||XHR;var xhr=new options.XMLHttpRequest;xhr.open(method,params.url,true);xhr.onreadystatechange=function onreadystatechange(){if(xhr.readyState!==4){return}if(200<=xhr.status&&xhr.status<300){callback(null,xhr.responseText);return}callback(new Error(xhr.responseText))};for(var headerName in params.headers){xhr.setRequestHeader(headerName,params.headers[headerName])}xhr.send(JSON.stringify(params.body))}var Request=request;Request.get=function get(params,callback){return new this("GET",params,callback)};Request.post=function post(params,callback){return new this("POST",params,callback)};module.exports=Request},{xmlhttprequest:2}],21:[function(require,module,exports){"use strict";var _typeof=typeof Symbol==="function"&&typeof Symbol.iterator==="symbol"?function(obj){return typeof obj}:function(obj){return obj&&typeof Symbol==="function"&&obj.constructor===Symbol&&obj!==Symbol.prototype?"symbol":typeof obj};var NotSupportedError=require("../errors").NotSupportedError;var util=require("../util");function getUserMedia(constraints,options){options=options||{};options.util=options.util||util;options.navigator=options.navigator||(typeof navigator!=="undefined"?navigator:null);return new Promise(function(resolve,reject){if(!options.navigator){throw new NotSupportedError("getUserMedia is not supported")}switch("function"){case _typeof(options.navigator.mediaDevices&&options.navigator.mediaDevices.getUserMedia):return resolve(options.navigator.mediaDevices.getUserMedia(constraints));case _typeof(options.navigator.webkitGetUserMedia):return options.navigator.webkitGetUserMedia(constraints,resolve,reject);case _typeof(options.navigator.mozGetUserMedia):return options.navigator.mozGetUserMedia(constraints,resolve,reject);case _typeof(options.navigator.getUserMedia):return options.navigator.getUserMedia(constraints,resolve,reject);default:throw new NotSupportedError("getUserMedia is not supported")}}).catch(function(e){throw options.util.isFirefox()&&e.name==="NotReadableError"?new NotSupportedError("Firefox does not currently support opening multiple audio input tracks"+"simultaneously, even across different tabs.\n"+"Related Bugzilla thread: https://bugzilla.mozilla.org/show_bug.cgi?id=1299324"):e})}module.exports=getUserMedia},{"../errors":12,"../util":35}],22:[function(require,module,exports){"use strict";Object.defineProperty(exports,"__esModule",{value:true});var IceCandidate=function(){function IceCandidate(iceCandidate,isRemote){if(isRemote===void 0){isRemote=false}this.deleted=false;var cost;var parts=iceCandidate.candidate.split("network-cost ");if(parts[1]){cost=parseInt(parts[1],10)}this.candidateType=iceCandidate.type;this.ip=iceCandidate.ip||iceCandidate.address;this.isRemote=isRemote;this.networkCost=cost;this.port=iceCandidate.port;this.priority=iceCandidate.priority;this.protocol=iceCandidate.protocol;this.relatedAddress=iceCandidate.relatedAddress;this.relatedPort=iceCandidate.relatedPort;this.tcpType=iceCandidate.tcpType;this.transportId=iceCandidate.sdpMid}IceCandidate.prototype.toPayload=function(){return{candidate_type:this.candidateType,deleted:this.deleted,ip:this.ip,is_remote:this.isRemote,"network-cost":this.networkCost,port:this.port,priority:this.priority,protocol:this.protocol,related_address:this.relatedAddress,related_port:this.relatedPort,tcp_type:this.tcpType,transport_id:this.transportId}};return IceCandidate}();exports.IceCandidate=IceCandidate},{}],23:[function(require,module,exports){"use strict";var PeerConnection=require("./peerconnection");var _require=require("./rtcpc"),test=_require.test;function enabled(){return test()}function getMediaEngine(){return typeof RTCIceGatherer!=="undefined"?"ORTC":"WebRTC"}module.exports={enabled:enabled,getMediaEngine:getMediaEngine,PeerConnection:PeerConnection}},{"./peerconnection":26,"./rtcpc":27}],24:[function(require,module,exports){var OLD_MAX_VOLUME=32767;var NativeRTCStatsReport=typeof window!=="undefined"?window.RTCStatsReport:undefined;function MockRTCStatsReport(statsMap){if(!(this instanceof MockRTCStatsReport)){return new MockRTCStatsReport(statsMap)}var self=this;Object.defineProperties(this,{size:{enumerable:true,get:function(){return self._map.size}},_map:{value:statsMap}});this[Symbol.iterator]=statsMap[Symbol.iterator]}if(NativeRTCStatsReport){MockRTCStatsReport.prototype=Object.create(NativeRTCStatsReport.prototype);MockRTCStatsReport.prototype.constructor=MockRTCStatsReport}["entries","forEach","get","has","keys","values"].forEach(function(key){MockRTCStatsReport.prototype[key]=function(){var _a;var args=[];for(var _i=0;_i=0}exports.isNonNegativeNumber=isNonNegativeNumber;exports.default={calculate:calculate,isNonNegativeNumber:isNonNegativeNumber}},{}],26:[function(require,module,exports){"use strict";var _require=require("../errors"),InvalidArgumentError=_require.InvalidArgumentError,MediaErrors=_require.MediaErrors,NotSupportedError=_require.NotSupportedError,SignalingErrors=_require.SignalingErrors;var Log=require("../log").default;var util=require("../util");var RTCPC=require("./rtcpc");var _require2=require("./sdp"),setIceAggressiveNomination=_require2.setIceAggressiveNomination;var ICE_GATHERING_TIMEOUT=15e3;var ICE_GATHERING_FAIL_NONE="none";var ICE_GATHERING_FAIL_TIMEOUT="timeout";var INITIAL_ICE_CONNECTION_STATE="new";var VOLUME_INTERVAL_MS=50;function PeerConnection(audioHelper,pstream,getUserMedia,options){if(!audioHelper||!pstream||!getUserMedia){throw new InvalidArgumentError("Audiohelper, pstream and getUserMedia are required arguments")}if(!(this instanceof PeerConnection)){return new PeerConnection(audioHelper,pstream,getUserMedia,options)}function noop(){}this.onopen=noop;this.onerror=noop;this.onclose=noop;this.ondisconnected=noop;this.onfailed=noop;this.onconnected=noop;this.onreconnected=noop;this.onsignalingstatechange=noop;this.ondtlstransportstatechange=noop;this.onicegatheringfailure=noop;this.onicegatheringstatechange=noop;this.oniceconnectionstatechange=noop;this.onpcconnectionstatechange=noop;this.onicecandidate=noop;this.onselectedcandidatepairchange=noop;this.onvolume=noop;this.version=null;this.pstream=pstream;this.stream=null;this.sinkIds=new Set(["default"]);this.outputs=new Map;this.status="connecting";this.callSid=null;this.isMuted=false;this.getUserMedia=getUserMedia;var AudioContext=typeof window!=="undefined"&&(window.AudioContext||window.webkitAudioContext);this._isSinkSupported=!!AudioContext&&typeof HTMLAudioElement!=="undefined"&&HTMLAudioElement.prototype.setSinkId;this._audioContext=AudioContext&&audioHelper._audioContext;this._hasIceCandidates=false;this._hasIceGatheringFailures=false;this._iceGatheringTimeoutId=null;this._masterAudio=null;this._masterAudioDeviceId=null;this._mediaStreamSource=null;this._dtmfSender=null;this._dtmfSenderUnsupported=false;this._callEvents=[];this._nextTimeToPublish=Date.now();this._onAnswerOrRinging=noop;this._onHangup=noop;this._remoteStream=null;this._shouldManageStream=true;this._iceState=INITIAL_ICE_CONNECTION_STATE;this._isUnifiedPlan=options.isUnifiedPlan;this.options=options=options||{};this.navigator=options.navigator||(typeof navigator!=="undefined"?navigator:null);this.util=options.util||util;this.codecPreferences=options.codecPreferences;this._log=Log.getInstance();return this}PeerConnection.prototype.uri=function(){return this._uri};PeerConnection.prototype.openWithConstraints=function(constraints){return this.getUserMedia({audio:constraints}).then(this._setInputTracksFromStream.bind(this,false))};PeerConnection.prototype.setInputTracksFromStream=function(stream){var self=this;return this._setInputTracksFromStream(true,stream).then(function(){self._shouldManageStream=false})};PeerConnection.prototype._createAnalyser=function(audioContext,options){options=Object.assign({fftSize:32,smoothingTimeConstant:.3},options);var analyser=audioContext.createAnalyser();for(var field in options){analyser[field]=options[field]}return analyser};PeerConnection.prototype._setVolumeHandler=function(handler){this.onvolume=handler};PeerConnection.prototype._startPollingVolume=function(){if(!this._audioContext||!this.stream||!this._remoteStream){return}var audioContext=this._audioContext;var inputAnalyser=this._inputAnalyser=this._createAnalyser(audioContext);var inputBufferLength=inputAnalyser.frequencyBinCount;var inputDataArray=new Uint8Array(inputBufferLength);this._inputAnalyser2=this._createAnalyser(audioContext,{minDecibels:-127,maxDecibels:0,smoothingTimeConstant:0});var outputAnalyser=this._outputAnalyser=this._createAnalyser(audioContext);var outputBufferLength=outputAnalyser.frequencyBinCount;var outputDataArray=new Uint8Array(outputBufferLength);this._outputAnalyser2=this._createAnalyser(audioContext,{minDecibels:-127,maxDecibels:0,smoothingTimeConstant:0});this._updateInputStreamSource(this.stream);this._updateOutputStreamSource(this._remoteStream);var self=this;setTimeout(function emitVolume(){if(!self._audioContext){return}else if(self.status==="closed"){self._inputAnalyser.disconnect();self._outputAnalyser.disconnect();self._inputAnalyser2.disconnect();self._outputAnalyser2.disconnect();return}self._inputAnalyser.getByteFrequencyData(inputDataArray);var inputVolume=self.util.average(inputDataArray);self._inputAnalyser2.getByteFrequencyData(inputDataArray);var inputVolume2=self.util.average(inputDataArray);self._outputAnalyser.getByteFrequencyData(outputDataArray);var outputVolume=self.util.average(outputDataArray);self._outputAnalyser2.getByteFrequencyData(outputDataArray);var outputVolume2=self.util.average(outputDataArray);self.onvolume(inputVolume/255,outputVolume/255,inputVolume2,outputVolume2);setTimeout(emitVolume,VOLUME_INTERVAL_MS)},VOLUME_INTERVAL_MS)};PeerConnection.prototype._stopStream=function _stopStream(stream){if(!this._shouldManageStream){return}if(typeof MediaStreamTrack.prototype.stop==="function"){var audioTracks=typeof stream.getAudioTracks==="function"?stream.getAudioTracks():stream.audioTracks;audioTracks.forEach(function(track){track.stop()})}else{stream.stop()}};PeerConnection.prototype._updateInputStreamSource=function(stream){if(this._inputStreamSource){this._inputStreamSource.disconnect()}this._inputStreamSource=this._audioContext.createMediaStreamSource(stream);this._inputStreamSource.connect(this._inputAnalyser);this._inputStreamSource.connect(this._inputAnalyser2)};PeerConnection.prototype._updateOutputStreamSource=function(stream){if(this._outputStreamSource){this._outputStreamSource.disconnect()}this._outputStreamSource=this._audioContext.createMediaStreamSource(stream);this._outputStreamSource.connect(this._outputAnalyser);this._outputStreamSource.connect(this._outputAnalyser2)};PeerConnection.prototype._setInputTracksFromStream=function(shouldClone,newStream){return this._isUnifiedPlan?this._setInputTracksForUnifiedPlan(shouldClone,newStream):this._setInputTracksForPlanB(shouldClone,newStream)};PeerConnection.prototype._setInputTracksForPlanB=function(shouldClone,newStream){var _this=this;if(!newStream){return Promise.reject(new InvalidArgumentError("Can not set input stream to null while in a call"))}if(!newStream.getAudioTracks().length){return Promise.reject(new InvalidArgumentError("Supplied input stream has no audio tracks"))}var localStream=this.stream;if(!localStream){this.stream=shouldClone?cloneStream(newStream):newStream}else{this._stopStream(localStream);removeStream(this.version.pc,localStream);localStream.getAudioTracks().forEach(localStream.removeTrack,localStream);newStream.getAudioTracks().forEach(localStream.addTrack,localStream);addStream(this.version.pc,newStream);this._updateInputStreamSource(this.stream)}this.mute(this.isMuted);if(!this.version){return Promise.resolve(this.stream)}return new Promise(function(resolve,reject){_this.version.createOffer(_this.options.maxAverageBitrate,_this.codecPreferences,{audio:true},function(){_this.version.processAnswer(_this.codecPreferences,_this._answerSdp,function(){resolve(_this.stream)},reject)},reject)})};PeerConnection.prototype._setInputTracksForUnifiedPlan=function(shouldClone,newStream){var _this2=this;if(!newStream){return Promise.reject(new InvalidArgumentError("Can not set input stream to null while in a call"))}if(!newStream.getAudioTracks().length){return Promise.reject(new InvalidArgumentError("Supplied input stream has no audio tracks"))}var localStream=this.stream;var getStreamPromise=function getStreamPromise(){_this2.mute(_this2.isMuted);return Promise.resolve(_this2.stream)};if(!localStream){this.stream=shouldClone?cloneStream(newStream):newStream}else{if(this._shouldManageStream){this._stopStream(localStream)}if(!this._sender){this._sender=this.version.pc.getSenders()[0]}return this._sender.replaceTrack(newStream.getAudioTracks()[0]).then(function(){_this2._updateInputStreamSource(newStream);return getStreamPromise()})}return getStreamPromise()};PeerConnection.prototype._onInputDevicesChanged=function(){if(!this.stream){return}var activeInputWasLost=this.stream.getAudioTracks().every(function(track){return track.readyState==="ended"});if(activeInputWasLost&&this._shouldManageStream){this.openWithConstraints(true)}};PeerConnection.prototype._onIceGatheringFailure=function(type){this._hasIceGatheringFailures=true;this.onicegatheringfailure(type)};PeerConnection.prototype._onMediaConnectionStateChange=function(newState){var previousState=this._iceState;if(previousState===newState||newState!=="connected"&&newState!=="disconnected"&&newState!=="failed"){return}this._iceState=newState;var message=void 0;switch(newState){case"connected":if(previousState==="disconnected"||previousState==="failed"){message="ICE liveliness check succeeded. Connection with Twilio restored";this._log.info(message);this.onreconnected(message)}else{message="Media connection established.";this._log.info(message);this.onconnected(message)}this._stopIceGatheringTimeout();this._hasIceGatheringFailures=false;break;case"disconnected":message="ICE liveliness check failed. May be having trouble connecting to Twilio";this._log.info(message);this.ondisconnected(message);break;case"failed":message="Connection with Twilio was interrupted.";this._log.info(message);this.onfailed(message);break}};PeerConnection.prototype._setSinkIds=function(sinkIds){if(!this._isSinkSupported){return Promise.reject(new NotSupportedError("Audio output selection is not supported by this browser"))}this.sinkIds=new Set(sinkIds.forEach?sinkIds:[sinkIds]);return this.version?this._updateAudioOutputs():Promise.resolve()};PeerConnection.prototype._startIceGatheringTimeout=function startIceGatheringTimeout(){var _this3=this;this._stopIceGatheringTimeout();this._iceGatheringTimeoutId=setTimeout(function(){_this3._onIceGatheringFailure(ICE_GATHERING_FAIL_TIMEOUT)},ICE_GATHERING_TIMEOUT)};PeerConnection.prototype._stopIceGatheringTimeout=function stopIceGatheringTimeout(){clearInterval(this._iceGatheringTimeoutId)};PeerConnection.prototype._updateAudioOutputs=function updateAudioOutputs(){var addedOutputIds=Array.from(this.sinkIds).filter(function(id){return!this.outputs.has(id)},this);var removedOutputIds=Array.from(this.outputs.keys()).filter(function(id){return!this.sinkIds.has(id)},this);var self=this;var createOutputPromises=addedOutputIds.map(this._createAudioOutput,this);return Promise.all(createOutputPromises).then(function(){return Promise.all(removedOutputIds.map(self._removeAudioOutput,self))})};PeerConnection.prototype._createAudio=function createAudio(arr){return new Audio(arr)};PeerConnection.prototype._createAudioOutput=function createAudioOutput(id){var dest=this._audioContext.createMediaStreamDestination();this._mediaStreamSource.connect(dest);var audio=this._createAudio();setAudioSource(audio,dest.stream);var self=this;return audio.setSinkId(id).then(function(){return audio.play()}).then(function(){self.outputs.set(id,{audio:audio,dest:dest})})};PeerConnection.prototype._removeAudioOutputs=function removeAudioOutputs(){if(this._masterAudio&&typeof this._masterAudioDeviceId!=="undefined"){this._disableOutput(this,this._masterAudioDeviceId);this.outputs.delete(this._masterAudioDeviceId);this._masterAudioDeviceId=null;if(!this._masterAudio.paused){this._masterAudio.pause()}if(typeof this._masterAudio.srcObject!=="undefined"){this._masterAudio.srcObject=null}else{this._masterAudio.src=""}this._masterAudio=null}return Array.from(this.outputs.keys()).map(this._removeAudioOutput,this)};PeerConnection.prototype._disableOutput=function disableOutput(pc,id){var output=pc.outputs.get(id);if(!output){return}if(output.audio){output.audio.pause();output.audio.src=""}if(output.dest){output.dest.disconnect()}};PeerConnection.prototype._reassignMasterOutput=function reassignMasterOutput(pc,masterId){var masterOutput=pc.outputs.get(masterId);pc.outputs.delete(masterId);var self=this;var idToReplace=Array.from(pc.outputs.keys())[0]||"default";return masterOutput.audio.setSinkId(idToReplace).then(function(){self._disableOutput(pc,idToReplace);pc.outputs.set(idToReplace,masterOutput);pc._masterAudioDeviceId=idToReplace}).catch(function rollback(){pc.outputs.set(masterId,masterOutput);self._log.info("Could not reassign master output. Attempted to roll back.")})};PeerConnection.prototype._removeAudioOutput=function removeAudioOutput(id){if(this._masterAudioDeviceId===id){return this._reassignMasterOutput(this,id)}this._disableOutput(this,id);this.outputs.delete(id);return Promise.resolve()};PeerConnection.prototype._onAddTrack=function onAddTrack(pc,stream){var audio=pc._masterAudio=this._createAudio();setAudioSource(audio,stream);audio.play();var deviceId=Array.from(pc.outputs.keys())[0]||"default";pc._masterAudioDeviceId=deviceId;pc.outputs.set(deviceId,{audio:audio});pc._mediaStreamSource=pc._audioContext.createMediaStreamSource(stream);pc.pcStream=stream;pc._updateAudioOutputs()};PeerConnection.prototype._fallbackOnAddTrack=function fallbackOnAddTrack(pc,stream){var audio=document&&document.createElement("audio");audio.autoplay=true;if(!setAudioSource(audio,stream)){pc._log.info("Error attaching stream to element.")}pc.outputs.set("default",{audio:audio})};PeerConnection.prototype._setEncodingParameters=function(enableDscp){if(!enableDscp||!this._sender||typeof this._sender.getParameters!=="function"||typeof this._sender.setParameters!=="function"){return}var params=this._sender.getParameters();if(!params.priority&&!(params.encodings&¶ms.encodings.length)){return}params.priority="high";if(params.encodings&¶ms.encodings.length){params.encodings.forEach(function(encoding){encoding.priority="high";encoding.networkPriority="high"})}this._sender.setParameters(params)};PeerConnection.prototype._setupPeerConnection=function(rtcConstraints,rtcConfiguration){var _this4=this;var self=this;var version=new(this.options.rtcpcFactory||RTCPC);version.create(rtcConstraints,rtcConfiguration);addStream(version.pc,this.stream);var eventName="ontrack"in version.pc?"ontrack":"onaddstream";version.pc[eventName]=function(event){var stream=self._remoteStream=event.stream||event.streams[0];if(typeof version.pc.getSenders==="function"){_this4._sender=version.pc.getSenders()[0]}if(self._isSinkSupported){self._onAddTrack(self,stream)}else{self._fallbackOnAddTrack(self,stream)}self._startPollingVolume()};return version};PeerConnection.prototype._maybeSetIceAggressiveNomination=function(sdp){return this.options.forceAggressiveIceNomination?setIceAggressiveNomination(sdp):sdp};PeerConnection.prototype._setupChannel=function(){var _this5=this;var pc=this.version.pc;this.version.pc.onopen=function(){_this5.status="open";_this5.onopen()};this.version.pc.onstatechange=function(){if(_this5.version.pc&&_this5.version.pc.readyState==="stable"){_this5.status="open";_this5.onopen()}};this.version.pc.onsignalingstatechange=function(){var state=pc.signalingState;_this5._log.info('signalingState is "'+state+'"');if(_this5.version.pc&&_this5.version.pc.signalingState==="stable"){_this5.status="open";_this5.onopen()}_this5.onsignalingstatechange(pc.signalingState)};pc.onconnectionstatechange=function(){_this5._log.info('pc.connectionState is "'+pc.connectionState+'"');_this5.onpcconnectionstatechange(pc.connectionState);_this5._onMediaConnectionStateChange(pc.connectionState)};pc.onicecandidate=function(event){var candidate=event.candidate;if(candidate){_this5._hasIceCandidates=true;_this5.onicecandidate(candidate);_this5._setupRTCIceTransportListener()}_this5._log.info("ICE Candidate: "+JSON.stringify(candidate))};pc.onicegatheringstatechange=function(){var state=pc.iceGatheringState;if(state==="gathering"){_this5._startIceGatheringTimeout()}else if(state==="complete"){_this5._stopIceGatheringTimeout();if(!_this5._hasIceCandidates){_this5._onIceGatheringFailure(ICE_GATHERING_FAIL_NONE)}if(_this5._hasIceCandidates&&_this5._hasIceGatheringFailures){_this5._startIceGatheringTimeout()}}_this5._log.info('pc.iceGatheringState is "'+pc.iceGatheringState+'"');_this5.onicegatheringstatechange(state)};pc.oniceconnectionstatechange=function(){_this5._log.info('pc.iceConnectionState is "'+pc.iceConnectionState+'"');_this5.oniceconnectionstatechange(pc.iceConnectionState);_this5._onMediaConnectionStateChange(pc.iceConnectionState)}};PeerConnection.prototype._initializeMediaStream=function(rtcConstraints,rtcConfiguration){if(this.status==="open"){return false}if(this.pstream.status==="disconnected"){this.onerror({info:{code:31e3,message:"Cannot establish connection. Client is disconnected",twilioError:new SignalingErrors.ConnectionDisconnected}});this.close();return false}this.version=this._setupPeerConnection(rtcConstraints,rtcConfiguration);this._setupChannel();return true};PeerConnection.prototype._removeReconnectionListeners=function(){if(this.pstream){this.pstream.removeListener("answer",this._onAnswerOrRinging);this.pstream.removeListener("hangup",this._onHangup)}};PeerConnection.prototype._setupRTCDtlsTransportListener=function(){var _this6=this;var dtlsTransport=this.getRTCDtlsTransport();if(!dtlsTransport||dtlsTransport.onstatechange){return}var handler=function handler(){_this6._log.info('dtlsTransportState is "'+dtlsTransport.state+'"');_this6.ondtlstransportstatechange(dtlsTransport.state)};handler();dtlsTransport.onstatechange=handler};PeerConnection.prototype._setupRTCIceTransportListener=function(){var _this7=this;var iceTransport=this._getRTCIceTransport();if(!iceTransport||iceTransport.onselectedcandidatepairchange){return}iceTransport.onselectedcandidatepairchange=function(){return _this7.onselectedcandidatepairchange(iceTransport.getSelectedCandidatePair())}};PeerConnection.prototype.iceRestart=function(){var _this8=this;this._log.info("Attempting to restart ICE...");this._hasIceCandidates=false;this.version.createOffer(this.options.maxAverageBitrate,this.codecPreferences,{iceRestart:true}).then(function(){_this8._removeReconnectionListeners();_this8._onAnswerOrRinging=function(payload){_this8._removeReconnectionListeners();if(!payload.sdp||_this8.version.pc.signalingState!=="have-local-offer"){var message="Invalid state or param during ICE Restart:"+("hasSdp:"+!!payload.sdp+", signalingState:"+_this8.version.pc.signalingState);_this8._log.info(message);return}var sdp=_this8._maybeSetIceAggressiveNomination(payload.sdp);_this8._answerSdp=sdp;if(_this8.status!=="closed"){_this8.version.processAnswer(_this8.codecPreferences,sdp,null,function(err){var message=err&&err.message?err.message:err;_this8._log.info("Failed to process answer during ICE Restart. Error: "+message)})}};_this8._onHangup=function(){_this8._log.info("Received hangup during ICE Restart");_this8._removeReconnectionListeners()};_this8.pstream.on("answer",_this8._onAnswerOrRinging);_this8.pstream.on("hangup",_this8._onHangup);_this8.pstream.reinvite(_this8.version.getSDP(),_this8.callSid)}).catch(function(err){var message=err&&err.message?err.message:err;_this8._log.info("Failed to createOffer during ICE Restart. Error: "+message);_this8.onfailed(message)})};PeerConnection.prototype.makeOutgoingCall=function(token,params,callsid,rtcConstraints,rtcConfiguration,onMediaStarted){var _this9=this;if(!this._initializeMediaStream(rtcConstraints,rtcConfiguration)){return}var self=this;this.callSid=callsid;function onAnswerSuccess(){if(self.options){self._setEncodingParameters(self.options.dscp)}onMediaStarted(self.version.pc)}function onAnswerError(err){var errMsg=err.message||err;self.onerror({info:{code:31e3,message:"Error processing answer: "+errMsg,twilioError:new MediaErrors.ClientRemoteDescFailed}})}this._onAnswerOrRinging=function(payload){if(!payload.sdp){return}var sdp=_this9._maybeSetIceAggressiveNomination(payload.sdp);self._answerSdp=sdp;if(self.status!=="closed"){self.version.processAnswer(_this9.codecPreferences,sdp,onAnswerSuccess,onAnswerError)}self.pstream.removeListener("answer",self._onAnswerOrRinging);self.pstream.removeListener("ringing",self._onAnswerOrRinging)};this.pstream.on("answer",this._onAnswerOrRinging);this.pstream.on("ringing",this._onAnswerOrRinging);function onOfferSuccess(){if(self.status!=="closed"){self.pstream.invite(self.version.getSDP(),self.callSid,self.options.preflight,params);self._setupRTCDtlsTransportListener()}}function onOfferError(err){var errMsg=err.message||err;self.onerror({info:{code:31e3,message:"Error creating the offer: "+errMsg,twilioError:new MediaErrors.ClientLocalDescFailed}})}this.version.createOffer(this.options.maxAverageBitrate,this.codecPreferences,{audio:true},onOfferSuccess,onOfferError)};PeerConnection.prototype.answerIncomingCall=function(callSid,sdp,rtcConstraints,rtcConfiguration,onMediaStarted){if(!this._initializeMediaStream(rtcConstraints,rtcConfiguration)){return}sdp=this._maybeSetIceAggressiveNomination(sdp);this._answerSdp=sdp.replace(/^a=setup:actpass$/gm,"a=setup:passive");this.callSid=callSid;var self=this;function onAnswerSuccess(){if(self.status!=="closed"){self.pstream.answer(self.version.getSDP(),callSid);if(self.options){self._setEncodingParameters(self.options.dscp)}onMediaStarted(self.version.pc);self._setupRTCDtlsTransportListener()}}function onAnswerError(err){var errMsg=err.message||err;self.onerror({info:{code:31e3,message:"Error creating the answer: "+errMsg,twilioError:new MediaErrors.ClientRemoteDescFailed}})}this.version.processSDP(this.options.maxAverageBitrate,this.codecPreferences,sdp,{audio:true},onAnswerSuccess,onAnswerError)};PeerConnection.prototype.close=function(){if(this.version&&this.version.pc){if(this.version.pc.signalingState!=="closed"){this.version.pc.close()}this.version.pc=null}if(this.stream){this.mute(false);this._stopStream(this.stream)}this.stream=null;this._removeReconnectionListeners();this._stopIceGatheringTimeout();Promise.all(this._removeAudioOutputs()).catch(function(){});if(this._mediaStreamSource){this._mediaStreamSource.disconnect()}if(this._inputAnalyser){this._inputAnalyser.disconnect()}if(this._outputAnalyser){this._outputAnalyser.disconnect()}if(this._inputAnalyser2){this._inputAnalyser2.disconnect()}if(this._outputAnalyser2){this._outputAnalyser2.disconnect()}this.status="closed";this.onclose()};PeerConnection.prototype.reject=function(callSid){this.callSid=callSid};PeerConnection.prototype.ignore=function(callSid){this.callSid=callSid};PeerConnection.prototype.mute=function(shouldMute){this.isMuted=shouldMute;if(!this.stream){return}if(this._sender&&this._sender.track){this._sender.track.enabled=!shouldMute}else{var audioTracks=typeof this.stream.getAudioTracks==="function"?this.stream.getAudioTracks():this.stream.audioTracks;audioTracks.forEach(function(track){track.enabled=!shouldMute})}};PeerConnection.prototype.getOrCreateDTMFSender=function getOrCreateDTMFSender(){if(this._dtmfSender||this._dtmfSenderUnsupported){return this._dtmfSender||null}var self=this;var pc=this.version.pc;if(!pc){this._log.info("No RTCPeerConnection available to call createDTMFSender on");return null}if(typeof pc.getSenders==="function"&&(typeof RTCDTMFSender==="function"||typeof RTCDtmfSender==="function")){var chosenSender=pc.getSenders().find(function(sender){return sender.dtmf});if(chosenSender){this._log.info("Using RTCRtpSender#dtmf");this._dtmfSender=chosenSender.dtmf;return this._dtmfSender}}if(typeof pc.createDTMFSender==="function"&&typeof pc.getLocalStreams==="function"){var track=pc.getLocalStreams().map(function(stream){var tracks=self._getAudioTracks(stream);return tracks&&tracks[0]})[0];if(!track){this._log.info("No local audio MediaStreamTrack available on the RTCPeerConnection to pass to createDTMFSender");return null}this._log.info("Creating RTCDTMFSender");this._dtmfSender=pc.createDTMFSender(track);return this._dtmfSender}this._log.info("RTCPeerConnection does not support RTCDTMFSender");this._dtmfSenderUnsupported=true;return null};PeerConnection.prototype.getRTCDtlsTransport=function getRTCDtlsTransport(){var sender=this.version&&this.version.pc&&typeof this.version.pc.getSenders==="function"&&this.version.pc.getSenders()[0];return sender&&sender.transport||null};PeerConnection.prototype._canStopMediaStreamTrack=function(){return typeof MediaStreamTrack.prototype.stop==="function"};PeerConnection.prototype._getAudioTracks=function(stream){return typeof stream.getAudioTracks==="function"?stream.getAudioTracks():stream.audioTracks};PeerConnection.prototype._getRTCIceTransport=function _getRTCIceTransport(){var dtlsTransport=this.getRTCDtlsTransport();return dtlsTransport&&dtlsTransport.iceTransport||null};PeerConnection.protocol=function(){return RTCPC.test()?new RTCPC:null}();function addStream(pc,stream){if(typeof pc.addTrack==="function"){stream.getAudioTracks().forEach(function(track){pc.addTrack(track,stream)})}else{pc.addStream(stream)}}function cloneStream(oldStream){var newStream=typeof MediaStream!=="undefined"?new MediaStream:new webkitMediaStream;oldStream.getAudioTracks().forEach(newStream.addTrack,newStream);return newStream}function removeStream(pc,stream){if(typeof pc.removeTrack==="function"){pc.getSenders().forEach(function(sender){pc.removeTrack(sender)})}else{pc.removeStream(stream)}}function setAudioSource(audio,stream){if(typeof audio.srcObject!=="undefined"){audio.srcObject=stream}else if(typeof audio.mozSrcObject!=="undefined"){audio.mozSrcObject=stream}else if(typeof audio.src!=="undefined"){var _window=audio.options.window||window;audio.src=(_window.URL||_window.webkitURL).createObjectURL(stream)}else{return false}return true}PeerConnection.enabled=RTCPC.test();module.exports=PeerConnection},{"../errors":12,"../log":15,"../util":35,"./rtcpc":27,"./sdp":28}],27:[function(require,module,exports){(function(global){(function(){"use strict";var _typeof=typeof Symbol==="function"&&typeof Symbol.iterator==="symbol"?function(obj){return typeof obj}:function(obj){return obj&&typeof Symbol==="function"&&obj.constructor===Symbol&&obj!==Symbol.prototype?"symbol":typeof obj};var RTCPeerConnectionShim=require("rtcpeerconnection-shim");var Log=require("../log").default;var _require=require("./sdp"),setCodecPreferences=_require.setCodecPreferences,setMaxAverageBitrate=_require.setMaxAverageBitrate;var util=require("../util");function RTCPC(){if(typeof window==="undefined"){this.log.info("No RTCPeerConnection implementation available. The window object was not found.");return}if(util.isLegacyEdge()){this.RTCPeerConnection=new RTCPeerConnectionShim(typeof window!=="undefined"?window:global)}else if(typeof window.RTCPeerConnection==="function"){this.RTCPeerConnection=window.RTCPeerConnection}else if(typeof window.webkitRTCPeerConnection==="function"){this.RTCPeerConnection=webkitRTCPeerConnection}else if(typeof window.mozRTCPeerConnection==="function"){this.RTCPeerConnection=mozRTCPeerConnection;window.RTCSessionDescription=mozRTCSessionDescription;window.RTCIceCandidate=mozRTCIceCandidate}else{this.log.info("No RTCPeerConnection implementation available")}}RTCPC.prototype.create=function(rtcConstraints,rtcConfiguration){this.log=Log.getInstance();this.pc=new this.RTCPeerConnection(rtcConfiguration,rtcConstraints)};RTCPC.prototype.createModernConstraints=function(c){if(typeof c==="undefined"){return null}var nc=Object.assign({},c);if(typeof webkitRTCPeerConnection!=="undefined"&&!util.isLegacyEdge()){nc.mandatory={};if(typeof c.audio!=="undefined"){nc.mandatory.OfferToReceiveAudio=c.audio}if(typeof c.video!=="undefined"){nc.mandatory.OfferToReceiveVideo=c.video}}else{if(typeof c.audio!=="undefined"){nc.offerToReceiveAudio=c.audio}if(typeof c.video!=="undefined"){nc.offerToReceiveVideo=c.video}}delete nc.audio;delete nc.video;return nc};RTCPC.prototype.createOffer=function(maxAverageBitrate,codecPreferences,constraints,onSuccess,onError){var _this=this;constraints=this.createModernConstraints(constraints);return promisifyCreate(this.pc.createOffer,this.pc)(constraints).then(function(offer){if(!_this.pc){return Promise.resolve()}var sdp=setMaxAverageBitrate(offer.sdp,maxAverageBitrate);return promisifySet(_this.pc.setLocalDescription,_this.pc)(new RTCSessionDescription({type:"offer",sdp:setCodecPreferences(sdp,codecPreferences)}))}).then(onSuccess,onError)};RTCPC.prototype.createAnswer=function(maxAverageBitrate,codecPreferences,constraints,onSuccess,onError){var _this2=this;constraints=this.createModernConstraints(constraints);return promisifyCreate(this.pc.createAnswer,this.pc)(constraints).then(function(answer){if(!_this2.pc){return Promise.resolve()}var sdp=setMaxAverageBitrate(answer.sdp,maxAverageBitrate);return promisifySet(_this2.pc.setLocalDescription,_this2.pc)(new RTCSessionDescription({type:"answer",sdp:setCodecPreferences(sdp,codecPreferences)}))}).then(onSuccess,onError)};RTCPC.prototype.processSDP=function(maxAverageBitrate,codecPreferences,sdp,constraints,onSuccess,onError){var _this3=this;sdp=setCodecPreferences(sdp,codecPreferences);var desc=new RTCSessionDescription({sdp:sdp,type:"offer"});return promisifySet(this.pc.setRemoteDescription,this.pc)(desc).then(function(){_this3.createAnswer(maxAverageBitrate,codecPreferences,constraints,onSuccess,onError)})};RTCPC.prototype.getSDP=function(){return this.pc.localDescription.sdp};RTCPC.prototype.processAnswer=function(codecPreferences,sdp,onSuccess,onError){if(!this.pc){return Promise.resolve()}sdp=setCodecPreferences(sdp,codecPreferences);return promisifySet(this.pc.setRemoteDescription,this.pc)(new RTCSessionDescription({sdp:sdp,type:"answer"})).then(onSuccess,onError)};RTCPC.test=function(){if((typeof navigator==="undefined"?"undefined":_typeof(navigator))==="object"){var getUserMedia=navigator.mediaDevices&&navigator.mediaDevices.getUserMedia||navigator.webkitGetUserMedia||navigator.mozGetUserMedia||navigator.getUserMedia;if(util.isLegacyEdge(navigator)){return false}if(getUserMedia&&typeof window.RTCPeerConnection==="function"){return true}else if(getUserMedia&&typeof window.webkitRTCPeerConnection==="function"){return true}else if(getUserMedia&&typeof window.mozRTCPeerConnection==="function"){try{var test=new window.mozRTCPeerConnection;if(typeof test.getLocalStreams!=="function")return false}catch(e){return false}return true}else if(typeof RTCIceGatherer!=="undefined"){return true}}return false};function promisify(fn,ctx,areCallbacksFirst){return function(){var args=Array.prototype.slice.call(arguments);return new Promise(function(resolve){resolve(fn.apply(ctx,args))}).catch(function(){return new Promise(function(resolve,reject){fn.apply(ctx,areCallbacksFirst?[resolve,reject].concat(args):args.concat([resolve,reject]))})})}}function promisifyCreate(fn,ctx){return promisify(fn,ctx,true)}function promisifySet(fn,ctx){return promisify(fn,ctx,false)}module.exports=RTCPC}).call(this)}).call(this,typeof global!=="undefined"?global:typeof self!=="undefined"?self:typeof window!=="undefined"?window:{})},{"../log":15,"../util":35,"./sdp":28,"rtcpeerconnection-shim":61}],28:[function(require,module,exports){"use strict";var _slicedToArray=function(){function sliceIterator(arr,i){var _arr=[];var _n=true;var _d=false;var _e=undefined;try{for(var _i=arr[Symbol.iterator](),_s;!(_n=(_s=_i.next()).done);_n=true){_arr.push(_s.value);if(i&&_arr.length===i)break}}catch(err){_d=true;_e=err}finally{try{if(!_n&&_i["return"])_i["return"]()}finally{if(_d)throw _e}}return _arr}return function(arr,i){if(Array.isArray(arr)){return arr}else if(Symbol.iterator in Object(arr)){return sliceIterator(arr,i)}else{throw new TypeError("Invalid attempt to destructure non-iterable instance")}}}();var util=require("../util");var ptToFixedBitrateAudioCodecName={0:"PCMU",8:"PCMA"};var defaultOpusId=111;var BITRATE_MAX=51e4;var BITRATE_MIN=6e3;function getPreferredCodecInfo(sdp){var _ref=/a=rtpmap:(\d+) (\S+)/m.exec(sdp)||[null,"",""],_ref2=_slicedToArray(_ref,3),codecId=_ref2[1],codecName=_ref2[2];var regex=new RegExp("a=fmtp:"+codecId+" (\\S+)","m");var _ref3=regex.exec(sdp)||[null,""],_ref4=_slicedToArray(_ref3,2),codecParams=_ref4[1];return{codecName:codecName,codecParams:codecParams}}function setIceAggressiveNomination(sdp){if(!util.isChrome(window,window.navigator)){return sdp}return sdp.split("\n").filter(function(line){return line.indexOf("a=ice-lite")===-1}).join("\n")}function setMaxAverageBitrate(sdp,maxAverageBitrate){if(typeof maxAverageBitrate!=="number"||maxAverageBitrateBITRATE_MAX){return sdp}var matches=/a=rtpmap:(\d+) opus/m.exec(sdp);var opusId=matches&&matches.length?matches[1]:defaultOpusId;var regex=new RegExp("a=fmtp:"+opusId);var lines=sdp.split("\n").map(function(line){return regex.test(line)?line+(";maxaveragebitrate="+maxAverageBitrate):line});return lines.join("\n")}function setCodecPreferences(sdp,preferredCodecs){var mediaSections=getMediaSections(sdp);var session=sdp.split("\r\nm=")[0];return[session].concat(mediaSections.map(function(section){if(!/^m=(audio|video)/.test(section)){return section}var kind=section.match(/^m=(audio|video)/)[1];var codecMap=createCodecMapForMediaSection(section);var payloadTypes=getReorderedPayloadTypes(codecMap,preferredCodecs);var newSection=setPayloadTypesInMediaSection(payloadTypes,section);var pcmaPayloadTypes=codecMap.get("pcma")||[];var pcmuPayloadTypes=codecMap.get("pcmu")||[];var fixedBitratePayloadTypes=kind==="audio"?new Set(pcmaPayloadTypes.concat(pcmuPayloadTypes)):new Set;return fixedBitratePayloadTypes.has(payloadTypes[0])?newSection.replace(/\r\nb=(AS|TIAS):([0-9]+)/g,""):newSection})).join("\r\n")}function getMediaSections(sdp,kind,direction){return sdp.replace(/\r\n\r\n$/,"\r\n").split("\r\nm=").slice(1).map(function(mediaSection){return"m="+mediaSection}).filter(function(mediaSection){var kindPattern=new RegExp("m="+(kind||".*"),"gm");var directionPattern=new RegExp("a="+(direction||".*"),"gm");return kindPattern.test(mediaSection)&&directionPattern.test(mediaSection)})}function createCodecMapForMediaSection(section){return Array.from(createPtToCodecName(section)).reduce(function(codecMap,pair){var pt=pair[0];var codecName=pair[1];var pts=codecMap.get(codecName)||[];return codecMap.set(codecName,pts.concat(pt))},new Map)}function getReorderedPayloadTypes(codecMap,preferredCodecs){preferredCodecs=preferredCodecs.map(function(codecName){return codecName.toLowerCase()});var preferredPayloadTypes=util.flatMap(preferredCodecs,function(codecName){return codecMap.get(codecName)||[]});var remainingCodecs=util.difference(Array.from(codecMap.keys()),preferredCodecs);var remainingPayloadTypes=util.flatMap(remainingCodecs,function(codecName){return codecMap.get(codecName)});return preferredPayloadTypes.concat(remainingPayloadTypes)}function setPayloadTypesInMediaSection(payloadTypes,section){var lines=section.split("\r\n");var mLine=lines[0];var otherLines=lines.slice(1);mLine=mLine.replace(/([0-9]+\s?)+$/,payloadTypes.join(" "));return[mLine].concat(otherLines).join("\r\n")}function createPtToCodecName(mediaSection){return getPayloadTypesInMediaSection(mediaSection).reduce(function(ptToCodecName,pt){var rtpmapPattern=new RegExp("a=rtpmap:"+pt+" ([^/]+)");var matches=mediaSection.match(rtpmapPattern);var codecName=matches?matches[1].toLowerCase():ptToFixedBitrateAudioCodecName[pt]?ptToFixedBitrateAudioCodecName[pt].toLowerCase():"";return ptToCodecName.set(pt,codecName)},new Map)}function getPayloadTypesInMediaSection(section){var mLine=section.split("\r\n")[0];var matches=mLine.match(/([0-9]+)/g);if(!matches){return[]}return matches.slice(1).map(function(match){return parseInt(match,10)})}module.exports={getPreferredCodecInfo:getPreferredCodecInfo,setCodecPreferences:setCodecPreferences,setIceAggressiveNomination:setIceAggressiveNomination,setMaxAverageBitrate:setMaxAverageBitrate}},{"../util":35}],29:[function(require,module,exports){var __spreadArrays=this&&this.__spreadArrays||function(){for(var s=0,i=0,il=arguments.length;i0}function sampleDevices(mediaDevices){nativeMediaDevices.enumerateDevices().then(function(newDevices){var knownDevices=mediaDevices._knownDevices;var oldDevices=knownDevices.slice();[].splice.apply(knownDevices,[0,knownDevices.length].concat(newDevices.sort(sortDevicesById)));if(!mediaDevices._deviceChangeIsNative&&devicesHaveChanged(knownDevices,oldDevices)){mediaDevices.dispatchEvent(new Event("devicechange"))}if(!mediaDevices._deviceInfoChangeIsNative&&deviceInfosHaveChanged(knownDevices,oldDevices)){mediaDevices.dispatchEvent(new Event("deviceinfochange"))}})}function propertyHasChanged(propertyName,as,bs){return as.some(function(a,i){return a[propertyName]!==bs[i][propertyName]})}function reemitNativeEvent(mediaDevices,eventName){var methodName="on"+eventName;function dispatchEvent(event){mediaDevices.dispatchEvent(event)}if(methodName in nativeMediaDevices){if("addEventListener"in nativeMediaDevices){nativeMediaDevices.addEventListener(eventName,dispatchEvent)}else{nativeMediaDevices[methodName]=dispatchEvent}return true}return false}function sortDevicesById(a,b){return a.deviceId0){this._maxDurationTimeout=setTimeout(this._stop.bind(this),this._maxDuration)}forceShouldLoop=typeof forceShouldLoop==="boolean"?forceShouldLoop:this._shouldLoop;var self=this;var playPromise=this._playPromise=Promise.all(this._sinkIds.map(function createAudioElement(sinkId){if(!self._Audio){return Promise.resolve()}var audioElement=self._activeEls.get(sinkId);if(audioElement){return self._playAudioElement(sinkId,forceIsMuted,forceShouldLoop)}audioElement=new self._Audio(self.url);if(typeof audioElement.setAttribute==="function"){audioElement.setAttribute("crossorigin","anonymous")}return new Promise(function(resolve){audioElement.addEventListener("canplaythrough",resolve)}).then(function(){return(self._isSinkSupported?audioElement.setSinkId(sinkId):Promise.resolve()).then(function setSinkIdSuccess(){self._activeEls.set(sinkId,audioElement);if(!self._playPromise){return Promise.resolve()}return self._playAudioElement(sinkId,forceIsMuted,forceShouldLoop)})})}));return playPromise};Sound.prototype._stop=function _stop(){var _this2=this;this._activeEls.forEach(function(audioEl,sinkId){if(_this2._sinkIds.includes(sinkId)){audioEl.pause();audioEl.currentTime=0}else{destroyAudioElement(audioEl);_this2._activeEls.delete(sinkId)}});clearTimeout(this._maxDurationTimeout);this._playPromise=null;this._maxDurationTimeout=null};Sound.prototype.setSinkIds=function setSinkIds(ids){if(!this._isSinkSupported){return}ids=ids.forEach?ids:[ids];[].splice.apply(this._sinkIds,[0,this._sinkIds.length].concat(ids))};Sound.prototype.stop=function stop(){var _this3=this;this._operations.enqueue(function(){_this3._stop();return Promise.resolve()})};Sound.prototype.play=function play(){var _this4=this;return this._operations.enqueue(function(){return _this4._play()})};module.exports=Sound},{"./asyncQueue":4,"./errors":12,"@twilio/audioplayer":41}],34:[function(require,module,exports){"use strict";var __extends=this&&this.__extends||function(){var extendStatics=function(d,b){extendStatics=Object.setPrototypeOf||{__proto__:[]}instanceof Array&&function(d,b){d.__proto__=b}||function(d,b){for(var p in b)if(b.hasOwnProperty(p))d[p]=b[p]};return extendStatics(d,b)};return function(d,b){extendStatics(d,b);function __(){this.constructor=d}d.prototype=b===null?Object.create(b):(__.prototype=b.prototype,new __)}}();var __assign=this&&this.__assign||function(){__assign=Object.assign||function(t){for(var s,i=1,n=arguments.length;imax?1:0},0)}function countLow(min,values){return values.reduce(function(lowCount,value){return lowCount+=valuethis._maxSampleCount){samples.splice(0,samples.length-this._maxSampleCount)}};StatsMonitor.prototype._clearWarning=function(statName,thresholdName,data){var warningId=statName+":"+thresholdName;var activeWarning=this._activeWarnings.get(warningId);if(!activeWarning||Date.now()-activeWarning.timeRaised0?currentPacketsLost/currentInboundPackets*100:0;var totalInboundPackets=stats.packetsReceived+stats.packetsLost;var totalPacketsLostFraction=totalInboundPackets>0?stats.packetsLost/totalInboundPackets*100:100;var rttValue=typeof stats.rtt==="number"||!previousSample?stats.rtt:previousSample.rtt;var audioInputLevelValues=this._inputVolumes.splice(0);this._supplementalSampleBuffers.audioInputLevel.push(audioInputLevelValues);var audioOutputLevelValues=this._outputVolumes.splice(0);this._supplementalSampleBuffers.audioOutputLevel.push(audioOutputLevelValues);return{audioInputLevel:Math.round(util_1.average(audioInputLevelValues)),audioOutputLevel:Math.round(util_1.average(audioOutputLevelValues)),bytesReceived:currentBytesReceived,bytesSent:currentBytesSent,codecName:stats.codecName,jitter:stats.jitter,mos:this._mos.calculate(rttValue,stats.jitter,previousSample&¤tPacketsLostFraction),packetsLost:currentPacketsLost,packetsLostFraction:currentPacketsLostFraction,packetsReceived:currentPacketsReceived,packetsSent:currentPacketsSent,rtt:rttValue,timestamp:stats.timestamp,totals:{bytesReceived:stats.bytesReceived,bytesSent:stats.bytesSent,packetsLost:stats.packetsLost,packetsLostFraction:totalPacketsLostFraction,packetsReceived:stats.packetsReceived,packetsSent:stats.packetsSent}}};StatsMonitor.prototype._fetchSample=function(){var _this=this;this._getSample().then(function(sample){_this._addSample(sample);_this._raiseWarnings();_this.emit("sample",sample)}).catch(function(error){_this.disable();_this.emit("error",error)})};StatsMonitor.prototype._getSample=function(){var _this=this;return this._getRTCStats(this._peerConnection).then(function(stats){var previousSample=null;if(_this._sampleBuffer.length){previousSample=_this._sampleBuffer[_this._sampleBuffer.length-1]}return _this._createSample(stats,previousSample)})};StatsMonitor.prototype._raiseWarning=function(statName,thresholdName,data){var warningId=statName+":"+thresholdName;if(this._activeWarnings.has(warningId)){return}this._activeWarnings.set(warningId,{timeRaised:Date.now()});var thresholds=this._thresholds[statName];var thresholdValue;if(Array.isArray(thresholds)){var foundThreshold=thresholds.find(function(threshold){return thresholdName in threshold});if(foundThreshold){thresholdValue=foundThreshold[thresholdName]}}else{thresholdValue=this._thresholds[statName][thresholdName]}this.emit("warning",__assign(__assign({},data),{name:statName,threshold:{name:thresholdName,value:thresholdValue}}))};StatsMonitor.prototype._raiseWarnings=function(){var _this=this;if(!this._warningsEnabled){return}Object.keys(this._thresholds).forEach(function(name){return _this._raiseWarningsForStat(name)})};StatsMonitor.prototype._raiseWarningsForStat=function(statName){var _this=this;var limits=Array.isArray(this._thresholds[statName])?this._thresholds[statName]:[this._thresholds[statName]];limits.forEach(function(limit){var samples=_this._sampleBuffer;var clearCount=limit.clearCount||SAMPLE_COUNT_CLEAR;var raiseCount=limit.raiseCount||SAMPLE_COUNT_RAISE;var sampleCount=limit.sampleCount||_this._maxSampleCount;var relevantSamples=samples.slice(-sampleCount);var values=relevantSamples.map(function(sample){return sample[statName]});var containsNull=values.some(function(value){return typeof value==="undefined"||value===null});if(containsNull){return}var count;if(typeof limit.max==="number"){count=countHigh(limit.max,values);if(count>=raiseCount){_this._raiseWarning(statName,"max",{values:values,samples:relevantSamples})}else if(count<=clearCount){_this._clearWarning(statName,"max",{values:values,samples:relevantSamples})}}if(typeof limit.min==="number"){count=countLow(limit.min,values);if(count>=raiseCount){_this._raiseWarning(statName,"min",{values:values,samples:relevantSamples})}else if(count<=clearCount){_this._clearWarning(statName,"min",{values:values,samples:relevantSamples})}}if(typeof limit.maxDuration==="number"&&samples.length>1){relevantSamples=samples.slice(-2);var prevValue=relevantSamples[0][statName];var curValue=relevantSamples[1][statName];var prevStreak=_this._currentStreaks.get(statName)||0;var streak=prevValue===curValue?prevStreak+1:0;_this._currentStreaks.set(statName,streak);if(streak>=limit.maxDuration){_this._raiseWarning(statName,"maxDuration",{value:streak})}else if(streak===0){_this._clearWarning(statName,"maxDuration",{value:prevStreak})}}if(typeof limit.minStandardDeviation==="number"){var sampleSets=_this._supplementalSampleBuffers[statName];if(!sampleSets||sampleSets.lengthlimit.sampleCount){sampleSets.splice(0,sampleSets.length-limit.sampleCount)}var flatSamples=flattenSamples(sampleSets.slice(-sampleCount));var stdDev=calculateStandardDeviation(flatSamples);if(typeof stdDev!=="number"){return}if(stdDevy}],["minAverage",function(x,y){return x=sampleCount){var avg=util_1.average(values);if(comparator(avg,limit[thresholdName])){_this._raiseWarning(statName,thresholdName,{values:values,samples:relevantSamples})}else if(!comparator(avg,limit.clearValue||limit[thresholdName])){_this._clearWarning(statName,thresholdName,{values:values,samples:relevantSamples})}}})})};return StatsMonitor}(events_1.EventEmitter);exports.default=StatsMonitor},{"./errors":12,"./rtc/mos":25,"./rtc/stats":29,"./util":35,events:53}],35:[function(require,module,exports){(function(global){(function(){function TwilioException(message){if(!(this instanceof TwilioException)){return new TwilioException(message)}this.message=message}TwilioException.prototype.toString=function(){return"Twilio.Exception: "+this.message};function average(values){return values&&values.length?values.reduce(function(t,v){return t+v})/values.length:0}function difference(lefts,rights,getKey){getKey=getKey||function(a){return a};var rightKeys=new Set(rights.map(getKey));return lefts.filter(function(left){return!rightKeys.has(getKey(left))})}function isElectron(navigator){return!!navigator.userAgent.match("Electron")}function isChrome(window,navigator){var isCriOS=!!navigator.userAgent.match("CriOS");var isHeadlessChrome=!!navigator.userAgent.match("HeadlessChrome");var isGoogle=typeof window.chrome!=="undefined"&&navigator.vendor==="Google Inc."&&navigator.userAgent.indexOf("OPR")===-1&&navigator.userAgent.indexOf("Edge")===-1;return isCriOS||isElectron(navigator)||isGoogle||isHeadlessChrome}function isFirefox(navigator){navigator=navigator||(typeof window==="undefined"?global.navigator:window.navigator);return!!navigator&&typeof navigator.userAgent==="string"&&/firefox|fxios/i.test(navigator.userAgent)}function isLegacyEdge(navigator){navigator=navigator||(typeof window==="undefined"?global.navigator:window.navigator);return!!navigator&&typeof navigator.userAgent==="string"&&/edge\/\d+/i.test(navigator.userAgent)}function isSafari(navigator){return!!navigator.vendor&&navigator.vendor.indexOf("Apple")!==-1&&navigator.userAgent&&navigator.userAgent.indexOf("CriOS")===-1&&navigator.userAgent.indexOf("FxiOS")===-1}function isUnifiedPlanDefault(window,navigator,PeerConnection,RtpTransceiver){if(typeof window==="undefined"||typeof navigator==="undefined"||typeof PeerConnection==="undefined"||typeof RtpTransceiver==="undefined"||typeof PeerConnection.prototype==="undefined"||typeof RtpTransceiver.prototype==="undefined"){return false}if(isChrome(window,navigator)&&PeerConnection.prototype.addTransceiver){var pc=new PeerConnection;var isUnifiedPlan=true;try{pc.addTransceiver("audio")}catch(e){isUnifiedPlan=false}pc.close();return isUnifiedPlan}else if(isFirefox(navigator)){return true}else if(isSafari(navigator)){return"currentDirection"in RtpTransceiver.prototype}return false}function queryToJson(params){if(!params){return""}return params.split("&").reduce(function(output,pair){var parts=pair.split("=");var key=parts[0];var value=decodeURIComponent((parts[1]||"").replace(/\+/g,"%20"));if(key){output[key]=value}return output},{})}function flatMap(list,mapFn){var listArray=list instanceof Map||list instanceof Set?Array.from(list.values()):list;mapFn=mapFn||function(item){return item};return listArray.reduce(function(flattened,item){var mapped=mapFn(item);return flattened.concat(mapped)},[])}exports.Exception=TwilioException;exports.average=average;exports.difference=difference;exports.isElectron=isElectron;exports.isChrome=isChrome;exports.isFirefox=isFirefox;exports.isLegacyEdge=isLegacyEdge;exports.isSafari=isSafari;exports.isUnifiedPlanDefault=isUnifiedPlanDefault;exports.queryToJson=queryToJson;exports.flatMap=flatMap}).call(this)}).call(this,typeof global!=="undefined"?global:typeof self!=="undefined"?self:typeof window!=="undefined"?window:{})},{}],36:[function(require,module,exports){"use strict";Object.defineProperty(exports,"__esModule",{value:true});var md5=require("md5");var errors_1=require("../twilio/errors");function generateUuid(){if(typeof window!=="object"){throw new errors_1.NotSupportedError("This platform is not supported.")}var crypto=window.crypto;if(typeof crypto!=="object"){throw new errors_1.NotSupportedError("The `crypto` module is not available on this platform.")}if(typeof(crypto.randomUUID||crypto.getRandomValues)==="undefined"){throw new errors_1.NotSupportedError("Neither `crypto.randomUUID` or `crypto.getRandomValues` are available "+"on this platform.")}var uInt32Arr=window.Uint32Array;if(typeof uInt32Arr==="undefined"){throw new errors_1.NotSupportedError("The `Uint32Array` module is not available on this platform.")}var generateRandomValues=typeof crypto.randomUUID==="function"?function(){return crypto.randomUUID()}:function(){return crypto.getRandomValues(new Uint32Array(32)).toString()};return md5(generateRandomValues())}function generateVoiceEventSid(){return"KX"+generateUuid()}exports.generateVoiceEventSid=generateVoiceEventSid},{"../twilio/errors":12,md5:56}],37:[function(require,module,exports){"use strict";var __extends=this&&this.__extends||function(){var extendStatics=function(d,b){extendStatics=Object.setPrototypeOf||{__proto__:[]}instanceof Array&&function(d,b){d.__proto__=b}||function(d,b){for(var p in b)if(b.hasOwnProperty(p))d[p]=b[p]};return extendStatics(d,b)};return function(d,b){extendStatics(d,b);function __(){this.constructor=d}d.prototype=b===null?Object.create(b):(__.prototype=b.prototype,new __)}}();var __assign=this&&this.__assign||function(){__assign=Object.assign||function(t){for(var s,i=1,n=arguments.length;i=_this._uris.length){_this._uriIndex=0}};_this._onSocketClose=function(event){_this._log.info("Received websocket close event code: "+event.code+". Reason: "+event.reason);if(event.code===1006||event.code===1015){_this.emit("error",{code:31005,message:event.reason||"Websocket connection to Twilio's signaling servers were "+"unexpectedly ended. If this is happening consistently, there may "+"be an issue resolving the hostname provided. If a region or an "+"edge is being specified in Device setup, ensure it is valid.",twilioError:new errors_1.SignalingErrors.ConnectionError});var wasConnected=_this.state===WSTransportState.Open||_this._previousState===WSTransportState.Open;if(_this._shouldFallback||!wasConnected){_this._moveUriIndex()}_this._shouldFallback=true}_this._closeSocket()};_this._onSocketError=function(err){_this._log.info("WebSocket received error: "+err.message);_this.emit("error",{code:31e3,message:err.message||"WSTransport socket error",twilioError:new errors_1.SignalingErrors.ConnectionDisconnected})};_this._onSocketMessage=function(message){_this._setHeartbeatTimeout();if(_this._socket&&message.data==="\n"){_this._socket.send("\n");return}_this.emit("message",message)};_this._onSocketOpen=function(){_this._log.info("WebSocket opened successfully.");_this._timeOpened=Date.now();_this._shouldFallback=false;_this._setState(WSTransportState.Open);clearTimeout(_this._connectTimeout);_this._resetBackoffs();_this._setHeartbeatTimeout();_this.emit("open")};_this._options=__assign(__assign({},WSTransport.defaultConstructorOptions),options);_this._uris=uris;_this._backoff=_this._setupBackoffs();return _this}WSTransport.prototype.close=function(){this._log.info("WSTransport.close() called...");this._close()};WSTransport.prototype.open=function(){this._log.info("WSTransport.open() called...");if(this._socket&&(this._socket.readyState===WebSocket.CONNECTING||this._socket.readyState===WebSocket.OPEN)){this._log.info("WebSocket already open.");return}if(this._preferredUri){this._connect(this._preferredUri)}else{this._connect(this._uris[this._uriIndex])}};WSTransport.prototype.send=function(message){if(!this._socket||this._socket.readyState!==WebSocket.OPEN){return false}try{this._socket.send(message)}catch(e){this._log.info("Error while sending message:",e.message);this._closeSocket();return false}return true};WSTransport.prototype.updatePreferredURI=function(uri){this._preferredUri=uri};WSTransport.prototype.updateURIs=function(uris){if(typeof uris==="string"){uris=[uris]}this._uris=uris;this._uriIndex=0};WSTransport.prototype._close=function(){this._setState(WSTransportState.Closed);this._closeSocket()};WSTransport.prototype._closeSocket=function(){clearTimeout(this._connectTimeout);clearTimeout(this._heartbeatTimeout);this._log.info("Closing and cleaning up WebSocket...");if(!this._socket){this._log.info("No WebSocket to clean up.");return}this._socket.removeEventListener("close",this._onSocketClose);this._socket.removeEventListener("error",this._onSocketError);this._socket.removeEventListener("message",this._onSocketMessage);this._socket.removeEventListener("open",this._onSocketOpen);if(this._socket.readyState===WebSocket.CONNECTING||this._socket.readyState===WebSocket.OPEN){this._socket.close()}if(this._timeOpened&&Date.now()-this._timeOpened>CONNECT_SUCCESS_TIMEOUT){this._resetBackoffs()}if(this.state!==WSTransportState.Closed){this._performBackoff()}delete this._socket;this.emit("close")};WSTransport.prototype._connect=function(uri,retryCount){var _this=this;this._log.info(typeof retryCount==="number"?"Attempting to reconnect (retry #"+retryCount+")...":"Attempting to connect...");this._closeSocket();this._setState(WSTransportState.Connecting);this._connectedUri=uri;try{this._socket=new this._options.WebSocket(this._connectedUri)}catch(e){this._log.info("Could not connect to endpoint:",e.message);this._close();this.emit("error",{code:31e3,message:e.message||"Could not connect to "+this._connectedUri,twilioError:new errors_1.SignalingErrors.ConnectionDisconnected});return}this._socket.addEventListener("close",this._onSocketClose);this._socket.addEventListener("error",this._onSocketError);this._socket.addEventListener("message",this._onSocketMessage);this._socket.addEventListener("open",this._onSocketOpen);delete this._timeOpened;this._connectTimeout=setTimeout(function(){_this._log.info("WebSocket connection attempt timed out.");_this._moveUriIndex();_this._closeSocket()},this._options.connectTimeoutMs)};WSTransport.prototype._performBackoff=function(){if(this._preferredUri){this._log.info("Preferred URI set; backing off.");this._backoff.preferred.backoff()}else{this._log.info("Preferred URI not set; backing off.");this._backoff.primary.backoff()}};WSTransport.prototype._resetBackoffs=function(){this._backoff.preferred.reset();this._backoff.primary.reset();this._backoffStartTime.preferred=null;this._backoffStartTime.primary=null};WSTransport.prototype._setHeartbeatTimeout=function(){var _this=this;clearTimeout(this._heartbeatTimeout);this._heartbeatTimeout=setTimeout(function(){_this._log.info("No messages received in "+HEARTBEAT_TIMEOUT/1e3+" seconds. Reconnecting...");_this._shouldFallback=true;_this._closeSocket()},HEARTBEAT_TIMEOUT)};WSTransport.prototype._setState=function(state){this._previousState=this.state;this.state=state};WSTransport.prototype._setupBackoffs=function(){var _this=this;var preferredBackoffConfig={factor:2,maxDelay:this._options.maxPreferredDelayMs,randomisationFactor:.4};this._log.info("Initializing preferred transport backoff using config: ",preferredBackoffConfig);var preferredBackoff=Backoff.exponential(preferredBackoffConfig);preferredBackoff.on("backoff",function(attempt,delay){if(_this.state===WSTransportState.Closed){_this._log.info("Preferred backoff initiated but transport state is closed; not attempting a connection.");return}_this._log.info("Will attempt to reconnect Websocket to preferred URI in "+delay+"ms");if(attempt===0){_this._backoffStartTime.preferred=Date.now();_this._log.info("Preferred backoff start; "+_this._backoffStartTime.preferred)}});preferredBackoff.on("ready",function(attempt,_delay){if(_this.state===WSTransportState.Closed){_this._log.info("Preferred backoff ready but transport state is closed; not attempting a connection.");return}if(_this._backoffStartTime.preferred===null){_this._log.info("Preferred backoff start time invalid; not attempting a connection.");return}if(Date.now()-_this._backoffStartTime.preferred>_this._options.maxPreferredDurationMs){_this._log.info("Max preferred backoff attempt time exceeded; falling back to primary backoff.");_this._preferredUri=null;_this._backoff.primary.backoff();return}if(typeof _this._preferredUri!=="string"){_this._log.info("Preferred URI cleared; falling back to primary backoff.");_this._preferredUri=null;_this._backoff.primary.backoff();return}_this._connect(_this._preferredUri,attempt+1)});var primaryBackoffConfig={factor:2,initialDelay:this._uris&&this._uris.length>1?Math.floor(Math.random()*(5e3-1e3+1))+1e3:100,maxDelay:this._options.maxPrimaryDelayMs,randomisationFactor:.4};this._log.info("Initializing primary transport backoff using config: ",primaryBackoffConfig);var primaryBackoff=Backoff.exponential(primaryBackoffConfig);primaryBackoff.on("backoff",function(attempt,delay){if(_this.state===WSTransportState.Closed){_this._log.info("Primary backoff initiated but transport state is closed; not attempting a connection.");return}_this._log.info("Will attempt to reconnect WebSocket in "+delay+"ms");if(attempt===0){_this._backoffStartTime.primary=Date.now();_this._log.info("Primary backoff start; "+_this._backoffStartTime.primary)}});primaryBackoff.on("ready",function(attempt,_delay){if(_this.state===WSTransportState.Closed){_this._log.info("Primary backoff ready but transport state is closed; not attempting a connection.");return}if(_this._backoffStartTime.primary===null){_this._log.info("Primary backoff start time invalid; not attempting a connection.");return}if(Date.now()-_this._backoffStartTime.primary>_this._options.maxPrimaryDurationMs){_this._log.info("Max primary backoff attempt time exceeded; not attempting a connection.");return}_this._connect(_this._uris[_this._uriIndex],attempt+1)});return{preferred:preferredBackoff,primary:primaryBackoff}};Object.defineProperty(WSTransport.prototype,"uri",{get:function(){return this._connectedUri},enumerable:true,configurable:true});WSTransport.defaultConstructorOptions={WebSocket:WebSocket,connectTimeoutMs:CONNECT_TIMEOUT,maxPreferredDelayMs:MAX_PREFERRED_DELAY,maxPreferredDurationMs:MAX_PREFERRED_DURATION,maxPrimaryDelayMs:MAX_PRIMARY_DELAY,maxPrimaryDurationMs:MAX_PRIMARY_DURATION};return WSTransport}(events_1.EventEmitter);exports.default=WSTransport},{"./errors":12,"./log":15,backoff:45,events:53,ws:1}],38:[function(require,module,exports){"use strict";var _regenerator=require("babel-runtime/regenerator");var _regenerator2=_interopRequireDefault(_regenerator);var _createClass=function(){function defineProperties(target,props){for(var i=0;i1&&arguments[1]!==undefined?arguments[1]:{};var options=arguments.length>2&&arguments[2]!==undefined?arguments[2]:{};_classCallCheck(this,AudioPlayer);var _this=_possibleConstructorReturn(this,(AudioPlayer.__proto__||Object.getPrototypeOf(AudioPlayer)).call(this));_this._audioNode=null;_this._pendingPlayDeferreds=[];_this._loop=false;_this._src="";_this._sinkId="default";if(typeof srcOrOptions!=="string"){options=srcOrOptions}_this._audioContext=audioContext;_this._audioElement=new(options.AudioFactory||Audio);_this._bufferPromise=_this._createPlayDeferred().promise;_this._destination=_this._audioContext.destination;_this._gainNode=_this._audioContext.createGain();_this._gainNode.connect(_this._destination);_this._XMLHttpRequest=options.XMLHttpRequestFactory||XMLHttpRequest;_this.addEventListener("canplaythrough",function(){_this._resolvePlayDeferreds()});if(typeof srcOrOptions==="string"){_this.src=srcOrOptions}return _this}_createClass(AudioPlayer,[{key:"load",value:function load(){this._load(this._src)}},{key:"pause",value:function pause(){if(this.paused){return}this._audioElement.pause();this._audioNode.stop();this._audioNode.disconnect(this._gainNode);this._audioNode=null;this._rejectPlayDeferreds(new Error("The play() request was interrupted by a call to pause()."))}},{key:"play",value:function play(){return __awaiter(this,void 0,void 0,_regenerator2.default.mark(function _callee(){var _this2=this;var buffer;return _regenerator2.default.wrap(function _callee$(_context){while(1){switch(_context.prev=_context.next){case 0:if(this.paused){_context.next=6;break}_context.next=3;return this._bufferPromise;case 3:if(this.paused){_context.next=5;break}return _context.abrupt("return");case 5:throw new Error("The play() request was interrupted by a call to pause().");case 6:this._audioNode=this._audioContext.createBufferSource();this._audioNode.loop=this.loop;this._audioNode.addEventListener("ended",function(){if(_this2._audioNode&&_this2._audioNode.loop){return}_this2.dispatchEvent("ended")});_context.next=11;return this._bufferPromise;case 11:buffer=_context.sent;if(!this.paused){_context.next=14;break}throw new Error("The play() request was interrupted by a call to pause().");case 14:this._audioNode.buffer=buffer;this._audioNode.connect(this._gainNode);this._audioNode.start();if(!this._audioElement.srcObject){_context.next=19;break}return _context.abrupt("return",this._audioElement.play());case 19:case"end":return _context.stop()}}},_callee,this)}))}},{key:"setSinkId",value:function setSinkId(sinkId){return __awaiter(this,void 0,void 0,_regenerator2.default.mark(function _callee2(){return _regenerator2.default.wrap(function _callee2$(_context2){while(1){switch(_context2.prev=_context2.next){case 0:if(!(typeof this._audioElement.setSinkId!=="function")){_context2.next=2;break}throw new Error("This browser does not support setSinkId.");case 2:if(!(sinkId===this.sinkId)){_context2.next=4;break}return _context2.abrupt("return");case 4:if(!(sinkId==="default")){_context2.next=11;break}if(!this.paused){this._gainNode.disconnect(this._destination)}this._audioElement.srcObject=null;this._destination=this._audioContext.destination;this._gainNode.connect(this._destination);this._sinkId=sinkId;return _context2.abrupt("return");case 11:_context2.next=13;return this._audioElement.setSinkId(sinkId);case 13:if(!this._audioElement.srcObject){_context2.next=15;break}return _context2.abrupt("return");case 15:this._gainNode.disconnect(this._audioContext.destination);this._destination=this._audioContext.createMediaStreamDestination();this._audioElement.srcObject=this._destination.stream;this._sinkId=sinkId;this._gainNode.connect(this._destination);case 20:case"end":return _context2.stop()}}},_callee2,this)}))}},{key:"_createPlayDeferred",value:function _createPlayDeferred(){var deferred=new Deferred_1.default;this._pendingPlayDeferreds.push(deferred);return deferred}},{key:"_load",value:function _load(src){var _this3=this;if(this._src&&this._src!==src){this.pause()}this._src=src;this._bufferPromise=new Promise(function(resolve,reject){return __awaiter(_this3,void 0,void 0,_regenerator2.default.mark(function _callee3(){var buffer;return _regenerator2.default.wrap(function _callee3$(_context3){while(1){switch(_context3.prev=_context3.next){case 0:if(src){_context3.next=2;break}return _context3.abrupt("return",this._createPlayDeferred().promise);case 2:_context3.next=4;return bufferSound(this._audioContext,this._XMLHttpRequest,src);case 4:buffer=_context3.sent;this.dispatchEvent("canplaythrough");resolve(buffer);case 7:case"end":return _context3.stop()}}},_callee3,this)}))})}},{key:"_rejectPlayDeferreds",value:function _rejectPlayDeferreds(reason){var deferreds=this._pendingPlayDeferreds;deferreds.splice(0,deferreds.length).forEach(function(_ref){var reject=_ref.reject;return reject(reason)})}},{key:"_resolvePlayDeferreds",value:function _resolvePlayDeferreds(result){var deferreds=this._pendingPlayDeferreds;deferreds.splice(0,deferreds.length).forEach(function(_ref2){var resolve=_ref2.resolve;return resolve(result)})}},{key:"destination",get:function get(){return this._destination}},{key:"loop",get:function get(){return this._loop},set:function set(shouldLoop){if(!shouldLoop&&this.loop&&!this.paused){var _pauseAfterPlaythrough=function _pauseAfterPlaythrough(){self._audioNode.removeEventListener("ended",_pauseAfterPlaythrough);self.pause()};var self=this;this._audioNode.addEventListener("ended",_pauseAfterPlaythrough)}this._loop=shouldLoop}},{key:"muted",get:function get(){return this._gainNode.gain.value===0},set:function set(shouldBeMuted){this._gainNode.gain.value=shouldBeMuted?0:1}},{key:"paused",get:function get(){return this._audioNode===null}},{key:"src",get:function get(){return this._src},set:function set(src){this._load(src)}},{key:"srcObject",get:function get(){return this._audioElement.srcObject},set:function set(srcObject){this._audioElement.srcObject=srcObject}},{key:"sinkId",get:function get(){return this._sinkId}}]);return AudioPlayer}(EventTarget_1.default);exports.default=AudioPlayer;function bufferSound(context,RequestFactory,src){return __awaiter(this,void 0,void 0,_regenerator2.default.mark(function _callee4(){var request,event;return _regenerator2.default.wrap(function _callee4$(_context4){while(1){switch(_context4.prev=_context4.next){case 0:request=new RequestFactory;request.open("GET",src,true);request.responseType="arraybuffer";_context4.next=5;return new Promise(function(resolve){request.addEventListener("load",resolve);request.send()});case 5:event=_context4.sent;_context4.prev=6;return _context4.abrupt("return",context.decodeAudioData(event.target.response));case 10:_context4.prev=10;_context4.t0=_context4["catch"](6);return _context4.abrupt("return",new Promise(function(resolve){context.decodeAudioData(event.target.response,resolve)}));case 13:case"end":return _context4.stop()}}},_callee4,this,[[6,10]])}))}},{"./Deferred":39,"./EventTarget":40,"babel-runtime/regenerator":44}],39:[function(require,module,exports){"use strict";var _createClass=function(){function defineProperties(target,props){for(var i=0;i1?_len-1:0),_key=1;_key<_len;_key++){args[_key-1]=arguments[_key]}return(_eventEmitter=this._eventEmitter).emit.apply(_eventEmitter,[name].concat(args))}},{key:"removeEventListener",value:function removeEventListener(name,handler){return this._eventEmitter.removeListener(name,handler)}}]);return EventTarget}();exports.default=EventTarget},{events:53}],41:[function(require,module,exports){"use strict";var AudioPlayer=require("./AudioPlayer");module.exports=AudioPlayer.default},{"./AudioPlayer":38}],42:[function(require,module,exports){var g=function(){return this}()||Function("return this")();var hadRuntime=g.regeneratorRuntime&&Object.getOwnPropertyNames(g).indexOf("regeneratorRuntime")>=0;var oldRuntime=hadRuntime&&g.regeneratorRuntime;g.regeneratorRuntime=undefined;module.exports=require("./runtime");if(hadRuntime){g.regeneratorRuntime=oldRuntime}else{try{delete g.regeneratorRuntime}catch(e){g.regeneratorRuntime=undefined}}},{"./runtime":43}],43:[function(require,module,exports){!function(global){"use strict";var Op=Object.prototype;var hasOwn=Op.hasOwnProperty;var undefined;var $Symbol=typeof Symbol==="function"?Symbol:{};var iteratorSymbol=$Symbol.iterator||"@@iterator";var asyncIteratorSymbol=$Symbol.asyncIterator||"@@asyncIterator";var toStringTagSymbol=$Symbol.toStringTag||"@@toStringTag";var inModule=typeof module==="object";var runtime=global.regeneratorRuntime;if(runtime){if(inModule){module.exports=runtime}return}runtime=global.regeneratorRuntime=inModule?module.exports:{};function wrap(innerFn,outerFn,self,tryLocsList){var protoGenerator=outerFn&&outerFn.prototype instanceof Generator?outerFn:Generator;var generator=Object.create(protoGenerator.prototype);var context=new Context(tryLocsList||[]);generator._invoke=makeInvokeMethod(innerFn,self,context);return generator}runtime.wrap=wrap;function tryCatch(fn,obj,arg){try{return{type:"normal",arg:fn.call(obj,arg)}}catch(err){return{type:"throw",arg:err}}}var GenStateSuspendedStart="suspendedStart";var GenStateSuspendedYield="suspendedYield";var GenStateExecuting="executing";var GenStateCompleted="completed";var ContinueSentinel={};function Generator(){}function GeneratorFunction(){}function GeneratorFunctionPrototype(){}var IteratorPrototype={};IteratorPrototype[iteratorSymbol]=function(){return this};var getProto=Object.getPrototypeOf;var NativeIteratorPrototype=getProto&&getProto(getProto(values([])));if(NativeIteratorPrototype&&NativeIteratorPrototype!==Op&&hasOwn.call(NativeIteratorPrototype,iteratorSymbol)){IteratorPrototype=NativeIteratorPrototype}var Gp=GeneratorFunctionPrototype.prototype=Generator.prototype=Object.create(IteratorPrototype);GeneratorFunction.prototype=Gp.constructor=GeneratorFunctionPrototype;GeneratorFunctionPrototype.constructor=GeneratorFunction;GeneratorFunctionPrototype[toStringTagSymbol]=GeneratorFunction.displayName="GeneratorFunction";function defineIteratorMethods(prototype){["next","throw","return"].forEach(function(method){prototype[method]=function(arg){return this._invoke(method,arg)}})}runtime.isGeneratorFunction=function(genFun){var ctor=typeof genFun==="function"&&genFun.constructor;return ctor?ctor===GeneratorFunction||(ctor.displayName||ctor.name)==="GeneratorFunction":false};runtime.mark=function(genFun){if(Object.setPrototypeOf){Object.setPrototypeOf(genFun,GeneratorFunctionPrototype)}else{genFun.__proto__=GeneratorFunctionPrototype;if(!(toStringTagSymbol in genFun)){genFun[toStringTagSymbol]="GeneratorFunction"}}genFun.prototype=Object.create(Gp);return genFun};runtime.awrap=function(arg){return{__await:arg}};function AsyncIterator(generator){function invoke(method,arg,resolve,reject){var record=tryCatch(generator[method],generator,arg);if(record.type==="throw"){reject(record.arg)}else{var result=record.arg;var value=result.value;if(value&&typeof value==="object"&&hasOwn.call(value,"__await")){return Promise.resolve(value.__await).then(function(value){invoke("next",value,resolve,reject)},function(err){invoke("throw",err,resolve,reject)})}return Promise.resolve(value).then(function(unwrapped){result.value=unwrapped;resolve(result)},reject)}}var previousPromise;function enqueue(method,arg){function callInvokeWithMethodAndArg(){return new Promise(function(resolve,reject){invoke(method,arg,resolve,reject)})}return previousPromise=previousPromise?previousPromise.then(callInvokeWithMethodAndArg,callInvokeWithMethodAndArg):callInvokeWithMethodAndArg()}this._invoke=enqueue}defineIteratorMethods(AsyncIterator.prototype);AsyncIterator.prototype[asyncIteratorSymbol]=function(){return this};runtime.AsyncIterator=AsyncIterator;runtime.async=function(innerFn,outerFn,self,tryLocsList){var iter=new AsyncIterator(wrap(innerFn,outerFn,self,tryLocsList));return runtime.isGeneratorFunction(outerFn)?iter:iter.next().then(function(result){return result.done?result.value:iter.next()})};function makeInvokeMethod(innerFn,self,context){var state=GenStateSuspendedStart;return function invoke(method,arg){if(state===GenStateExecuting){throw new Error("Generator is already running")}if(state===GenStateCompleted){if(method==="throw"){throw arg}return doneResult()}context.method=method;context.arg=arg;while(true){var delegate=context.delegate;if(delegate){var delegateResult=maybeInvokeDelegate(delegate,context);if(delegateResult){if(delegateResult===ContinueSentinel)continue;return delegateResult}}if(context.method==="next"){context.sent=context._sent=context.arg}else if(context.method==="throw"){if(state===GenStateSuspendedStart){state=GenStateCompleted;throw context.arg}context.dispatchException(context.arg)}else if(context.method==="return"){context.abrupt("return",context.arg)}state=GenStateExecuting;var record=tryCatch(innerFn,self,context);if(record.type==="normal"){state=context.done?GenStateCompleted:GenStateSuspendedYield;if(record.arg===ContinueSentinel){continue}return{value:record.arg,done:context.done}}else if(record.type==="throw"){state=GenStateCompleted;context.method="throw";context.arg=record.arg}}}}function maybeInvokeDelegate(delegate,context){var method=delegate.iterator[context.method];if(method===undefined){context.delegate=null;if(context.method==="throw"){if(delegate.iterator.return){context.method="return";context.arg=undefined;maybeInvokeDelegate(delegate,context);if(context.method==="throw"){return ContinueSentinel}}context.method="throw";context.arg=new TypeError("The iterator does not provide a 'throw' method")}return ContinueSentinel}var record=tryCatch(method,delegate.iterator,context.arg);if(record.type==="throw"){context.method="throw";context.arg=record.arg;context.delegate=null;return ContinueSentinel}var info=record.arg;if(!info){context.method="throw";context.arg=new TypeError("iterator result is not an object");context.delegate=null;return ContinueSentinel}if(info.done){context[delegate.resultName]=info.value;context.next=delegate.nextLoc;if(context.method!=="return"){context.method="next";context.arg=undefined}}else{return info}context.delegate=null;return ContinueSentinel}defineIteratorMethods(Gp);Gp[toStringTagSymbol]="Generator";Gp[iteratorSymbol]=function(){return this};Gp.toString=function(){return"[object Generator]"};function pushTryEntry(locs){var entry={tryLoc:locs[0]};if(1 in locs){entry.catchLoc=locs[1]}if(2 in locs){entry.finallyLoc=locs[2];entry.afterLoc=locs[3]}this.tryEntries.push(entry)}function resetTryEntry(entry){var record=entry.completion||{};record.type="normal";delete record.arg;entry.completion=record}function Context(tryLocsList){this.tryEntries=[{tryLoc:"root"}];tryLocsList.forEach(pushTryEntry,this);this.reset(true)}runtime.keys=function(object){var keys=[];for(var key in object){keys.push(key)}keys.reverse();return function next(){while(keys.length){var key=keys.pop();if(key in object){next.value=key;next.done=false;return next}}next.done=true;return next}};function values(iterable){if(iterable){var iteratorMethod=iterable[iteratorSymbol];if(iteratorMethod){return iteratorMethod.call(iterable)}if(typeof iterable.next==="function"){return iterable}if(!isNaN(iterable.length)){var i=-1,next=function next(){while(++i=0;--i){var entry=this.tryEntries[i];var record=entry.completion;if(entry.tryLoc==="root"){return handle("end")}if(entry.tryLoc<=this.prev){var hasCatch=hasOwn.call(entry,"catchLoc");var hasFinally=hasOwn.call(entry,"finallyLoc");if(hasCatch&&hasFinally){if(this.prev=0;--i){var entry=this.tryEntries[i];if(entry.tryLoc<=this.prev&&hasOwn.call(entry,"finallyLoc")&&this.prev=0;--i){var entry=this.tryEntries[i];if(entry.finallyLoc===finallyLoc){this.complete(entry.completion,entry.afterLoc);resetTryEntry(entry);return ContinueSentinel}}},catch:function(tryLoc){for(var i=this.tryEntries.length-1;i>=0;--i){var entry=this.tryEntries[i];if(entry.tryLoc===tryLoc){var record=entry.completion;if(record.type==="throw"){var thrown=record.arg;resetTryEntry(entry)}return thrown}}throw new Error("illegal catch attempt")},delegateYield:function(iterable,resultName,nextLoc){this.delegate={iterator:values(iterable),resultName:resultName,nextLoc:nextLoc};if(this.method==="next"){this.arg=undefined}return ContinueSentinel}}}(function(){return this}()||Function("return this")())},{}],44:[function(require,module,exports){module.exports=require("regenerator-runtime")},{"regenerator-runtime":42}],45:[function(require,module,exports){var Backoff=require("./lib/backoff");var ExponentialBackoffStrategy=require("./lib/strategy/exponential");var FibonacciBackoffStrategy=require("./lib/strategy/fibonacci");var FunctionCall=require("./lib/function_call.js");module.exports.Backoff=Backoff;module.exports.FunctionCall=FunctionCall;module.exports.FibonacciStrategy=FibonacciBackoffStrategy;module.exports.ExponentialStrategy=ExponentialBackoffStrategy;module.exports.fibonacci=function(options){return new Backoff(new FibonacciBackoffStrategy(options))};module.exports.exponential=function(options){return new Backoff(new ExponentialBackoffStrategy(options))};module.exports.call=function(fn,vargs,callback){var args=Array.prototype.slice.call(arguments);fn=args[0];vargs=args.slice(1,args.length-1);callback=args[args.length-1];return new FunctionCall(fn,vargs,callback)}},{"./lib/backoff":46,"./lib/function_call.js":47,"./lib/strategy/exponential":48,"./lib/strategy/fibonacci":49}],46:[function(require,module,exports){var events=require("events");var precond=require("precond");var util=require("util");function Backoff(backoffStrategy){events.EventEmitter.call(this);this.backoffStrategy_=backoffStrategy;this.maxNumberOfRetry_=-1;this.backoffNumber_=0;this.backoffDelay_=0;this.timeoutID_=-1;this.handlers={backoff:this.onBackoff_.bind(this)}}util.inherits(Backoff,events.EventEmitter);Backoff.prototype.failAfter=function(maxNumberOfRetry){precond.checkArgument(maxNumberOfRetry>0,"Expected a maximum number of retry greater than 0 but got %s.",maxNumberOfRetry);this.maxNumberOfRetry_=maxNumberOfRetry};Backoff.prototype.backoff=function(err){precond.checkState(this.timeoutID_===-1,"Backoff in progress.");if(this.backoffNumber_===this.maxNumberOfRetry_){this.emit("fail",err);this.reset()}else{this.backoffDelay_=this.backoffStrategy_.next();this.timeoutID_=setTimeout(this.handlers.backoff,this.backoffDelay_);this.emit("backoff",this.backoffNumber_,this.backoffDelay_,err)}};Backoff.prototype.onBackoff_=function(){this.timeoutID_=-1;this.emit("ready",this.backoffNumber_,this.backoffDelay_);this.backoffNumber_++};Backoff.prototype.reset=function(){this.backoffNumber_=0;this.backoffStrategy_.reset();clearTimeout(this.timeoutID_);this.timeoutID_=-1};module.exports=Backoff},{events:53,precond:57,util:65}],47:[function(require,module,exports){var events=require("events");var precond=require("precond");var util=require("util");var Backoff=require("./backoff");var FibonacciBackoffStrategy=require("./strategy/fibonacci");function FunctionCall(fn,args,callback){events.EventEmitter.call(this);precond.checkIsFunction(fn,"Expected fn to be a function.");precond.checkIsArray(args,"Expected args to be an array.");precond.checkIsFunction(callback,"Expected callback to be a function.");this.function_=fn;this.arguments_=args;this.callback_=callback;this.lastResult_=[];this.numRetries_=0;this.backoff_=null;this.strategy_=null;this.failAfter_=-1;this.retryPredicate_=FunctionCall.DEFAULT_RETRY_PREDICATE_;this.state_=FunctionCall.State_.PENDING}util.inherits(FunctionCall,events.EventEmitter);FunctionCall.State_={PENDING:0,RUNNING:1,COMPLETED:2,ABORTED:3};FunctionCall.DEFAULT_RETRY_PREDICATE_=function(err){return true};FunctionCall.prototype.isPending=function(){return this.state_==FunctionCall.State_.PENDING};FunctionCall.prototype.isRunning=function(){return this.state_==FunctionCall.State_.RUNNING};FunctionCall.prototype.isCompleted=function(){return this.state_==FunctionCall.State_.COMPLETED};FunctionCall.prototype.isAborted=function(){return this.state_==FunctionCall.State_.ABORTED};FunctionCall.prototype.setStrategy=function(strategy){precond.checkState(this.isPending(),"FunctionCall in progress.");this.strategy_=strategy;return this};FunctionCall.prototype.retryIf=function(retryPredicate){precond.checkState(this.isPending(),"FunctionCall in progress.");this.retryPredicate_=retryPredicate;return this};FunctionCall.prototype.getLastResult=function(){return this.lastResult_.concat()};FunctionCall.prototype.getNumRetries=function(){return this.numRetries_};FunctionCall.prototype.failAfter=function(maxNumberOfRetry){precond.checkState(this.isPending(),"FunctionCall in progress.");this.failAfter_=maxNumberOfRetry;return this};FunctionCall.prototype.abort=function(){if(this.isCompleted()||this.isAborted()){return}if(this.isRunning()){this.backoff_.reset()}this.state_=FunctionCall.State_.ABORTED;this.lastResult_=[new Error("Backoff aborted.")];this.emit("abort");this.doCallback_()};FunctionCall.prototype.start=function(backoffFactory){precond.checkState(!this.isAborted(),"FunctionCall is aborted.");precond.checkState(this.isPending(),"FunctionCall already started.");var strategy=this.strategy_||new FibonacciBackoffStrategy;this.backoff_=backoffFactory?backoffFactory(strategy):new Backoff(strategy);this.backoff_.on("ready",this.doCall_.bind(this,true));this.backoff_.on("fail",this.doCallback_.bind(this));this.backoff_.on("backoff",this.handleBackoff_.bind(this));if(this.failAfter_>0){this.backoff_.failAfter(this.failAfter_)}this.state_=FunctionCall.State_.RUNNING;this.doCall_(false)};FunctionCall.prototype.doCall_=function(isRetry){if(isRetry){this.numRetries_++}var eventArgs=["call"].concat(this.arguments_);events.EventEmitter.prototype.emit.apply(this,eventArgs);var callback=this.handleFunctionCallback_.bind(this);this.function_.apply(null,this.arguments_.concat(callback))};FunctionCall.prototype.doCallback_=function(){this.callback_.apply(null,this.lastResult_)};FunctionCall.prototype.handleFunctionCallback_=function(){if(this.isAborted()){return}var args=Array.prototype.slice.call(arguments);this.lastResult_=args;events.EventEmitter.prototype.emit.apply(this,["callback"].concat(args));var err=args[0];if(err&&this.retryPredicate_(err)){this.backoff_.backoff(err)}else{this.state_=FunctionCall.State_.COMPLETED;this.doCallback_()}};FunctionCall.prototype.handleBackoff_=function(number,delay,err){this.emit("backoff",number,delay,err)};module.exports=FunctionCall},{"./backoff":46,"./strategy/fibonacci":49,events:53,precond:57,util:65}],48:[function(require,module,exports){var util=require("util");var precond=require("precond");var BackoffStrategy=require("./strategy");function ExponentialBackoffStrategy(options){BackoffStrategy.call(this,options);this.backoffDelay_=0;this.nextBackoffDelay_=this.getInitialDelay();this.factor_=ExponentialBackoffStrategy.DEFAULT_FACTOR;if(options&&options.factor!==undefined){precond.checkArgument(options.factor>1,"Exponential factor should be greater than 1 but got %s.",options.factor);this.factor_=options.factor}}util.inherits(ExponentialBackoffStrategy,BackoffStrategy);ExponentialBackoffStrategy.DEFAULT_FACTOR=2;ExponentialBackoffStrategy.prototype.next_=function(){this.backoffDelay_=Math.min(this.nextBackoffDelay_,this.getMaxDelay());this.nextBackoffDelay_=this.backoffDelay_*this.factor_;return this.backoffDelay_};ExponentialBackoffStrategy.prototype.reset_=function(){this.backoffDelay_=0;this.nextBackoffDelay_=this.getInitialDelay()};module.exports=ExponentialBackoffStrategy},{"./strategy":50,precond:57,util:65}],49:[function(require,module,exports){var util=require("util");var BackoffStrategy=require("./strategy");function FibonacciBackoffStrategy(options){BackoffStrategy.call(this,options);this.backoffDelay_=0;this.nextBackoffDelay_=this.getInitialDelay()}util.inherits(FibonacciBackoffStrategy,BackoffStrategy);FibonacciBackoffStrategy.prototype.next_=function(){var backoffDelay=Math.min(this.nextBackoffDelay_,this.getMaxDelay());this.nextBackoffDelay_+=this.backoffDelay_;this.backoffDelay_=backoffDelay;return backoffDelay};FibonacciBackoffStrategy.prototype.reset_=function(){this.nextBackoffDelay_=this.getInitialDelay();this.backoffDelay_=0};module.exports=FibonacciBackoffStrategy},{"./strategy":50,util:65}],50:[function(require,module,exports){var events=require("events");var util=require("util");function isDef(value){return value!==undefined&&value!==null}function BackoffStrategy(options){options=options||{};if(isDef(options.initialDelay)&&options.initialDelay<1){throw new Error("The initial timeout must be greater than 0.")}else if(isDef(options.maxDelay)&&options.maxDelay<1){throw new Error("The maximal timeout must be greater than 0.")}this.initialDelay_=options.initialDelay||100;this.maxDelay_=options.maxDelay||1e4;if(this.maxDelay_<=this.initialDelay_){throw new Error("The maximal backoff delay must be "+"greater than the initial backoff delay.")}if(isDef(options.randomisationFactor)&&(options.randomisationFactor<0||options.randomisationFactor>1)){throw new Error("The randomisation factor must be between 0 and 1.")}this.randomisationFactor_=options.randomisationFactor||0}BackoffStrategy.prototype.getMaxDelay=function(){return this.maxDelay_};BackoffStrategy.prototype.getInitialDelay=function(){return this.initialDelay_};BackoffStrategy.prototype.next=function(){var backoffDelay=this.next_();var randomisationMultiple=1+Math.random()*this.randomisationFactor_;var randomizedDelay=Math.round(backoffDelay*randomisationMultiple);return randomizedDelay};BackoffStrategy.prototype.next_=function(){throw new Error("BackoffStrategy.next_() unimplemented.")};BackoffStrategy.prototype.reset=function(){this.reset_()};BackoffStrategy.prototype.reset_=function(){throw new Error("BackoffStrategy.reset_() unimplemented.")};module.exports=BackoffStrategy},{events:53,util:65}],51:[function(require,module,exports){var charenc={utf8:{stringToBytes:function(str){return charenc.bin.stringToBytes(unescape(encodeURIComponent(str)))},bytesToString:function(bytes){return decodeURIComponent(escape(charenc.bin.bytesToString(bytes)))}},bin:{stringToBytes:function(str){for(var bytes=[],i=0;i>>32-b},rotr:function(n,b){return n<<32-b|n>>>b},endian:function(n){if(n.constructor==Number){return crypt.rotl(n,8)&16711935|crypt.rotl(n,24)&4278255360}for(var i=0;i0;n--)bytes.push(Math.floor(Math.random()*256));return bytes},bytesToWords:function(bytes){for(var words=[],i=0,b=0;i>>5]|=bytes[i]<<24-b%32;return words},wordsToBytes:function(words){for(var bytes=[],b=0;b>>5]>>>24-b%32&255);return bytes},bytesToHex:function(bytes){for(var hex=[],i=0;i>>4).toString(16));hex.push((bytes[i]&15).toString(16))}return hex.join("")},hexToBytes:function(hex){for(var bytes=[],c=0;c>>6*(3-j)&63));else base64.push("=")}return base64.join("")},base64ToBytes:function(base64){base64=base64.replace(/[^A-Z0-9+\/]/gi,"");for(var bytes=[],i=0,imod4=0;i>>6-imod4*2)}return bytes}};module.exports=crypt})()},{}],53:[function(require,module,exports){var objectCreate=Object.create||objectCreatePolyfill;var objectKeys=Object.keys||objectKeysPolyfill;var bind=Function.prototype.bind||functionBindPolyfill;function EventEmitter(){if(!this._events||!Object.prototype.hasOwnProperty.call(this,"_events")){this._events=objectCreate(null);this._eventsCount=0}this._maxListeners=this._maxListeners||undefined}module.exports=EventEmitter;EventEmitter.EventEmitter=EventEmitter;EventEmitter.prototype._events=undefined;EventEmitter.prototype._maxListeners=undefined;var defaultMaxListeners=10;var hasDefineProperty;try{var o={};if(Object.defineProperty)Object.defineProperty(o,"x",{value:0});hasDefineProperty=o.x===0}catch(err){hasDefineProperty=false}if(hasDefineProperty){Object.defineProperty(EventEmitter,"defaultMaxListeners",{enumerable:true,get:function(){return defaultMaxListeners},set:function(arg){if(typeof arg!=="number"||arg<0||arg!==arg)throw new TypeError('"defaultMaxListeners" must be a positive number');defaultMaxListeners=arg}})}else{EventEmitter.defaultMaxListeners=defaultMaxListeners}EventEmitter.prototype.setMaxListeners=function setMaxListeners(n){if(typeof n!=="number"||n<0||isNaN(n))throw new TypeError('"n" argument must be a positive number');this._maxListeners=n;return this};function $getMaxListeners(that){if(that._maxListeners===undefined)return EventEmitter.defaultMaxListeners;return that._maxListeners}EventEmitter.prototype.getMaxListeners=function getMaxListeners(){return $getMaxListeners(this)};function emitNone(handler,isFn,self){if(isFn)handler.call(self);else{var len=handler.length;var listeners=arrayClone(handler,len);for(var i=0;i1)er=arguments[1];if(er instanceof Error){throw er}else{var err=new Error('Unhandled "error" event. ('+er+")");err.context=er;throw err}return false}handler=events[type];if(!handler)return false;var isFn=typeof handler==="function";len=arguments.length;switch(len){case 1:emitNone(handler,isFn,this);break;case 2:emitOne(handler,isFn,this,arguments[1]);break;case 3:emitTwo(handler,isFn,this,arguments[1],arguments[2]);break;case 4:emitThree(handler,isFn,this,arguments[1],arguments[2],arguments[3]);break;default:args=new Array(len-1);for(i=1;i0&&existing.length>m){existing.warned=true;var w=new Error("Possible EventEmitter memory leak detected. "+existing.length+' "'+String(type)+'" listeners '+"added. Use emitter.setMaxListeners() to "+"increase limit.");w.name="MaxListenersExceededWarning";w.emitter=target;w.type=type;w.count=existing.length;if(typeof console==="object"&&console.warn){console.warn("%s: %s",w.name,w.message)}}}}return target}EventEmitter.prototype.addListener=function addListener(type,listener){return _addListener(this,type,listener,false)};EventEmitter.prototype.on=EventEmitter.prototype.addListener;EventEmitter.prototype.prependListener=function prependListener(type,listener){return _addListener(this,type,listener,true)};function onceWrapper(){if(!this.fired){this.target.removeListener(this.type,this.wrapFn);this.fired=true;switch(arguments.length){case 0:return this.listener.call(this.target);case 1:return this.listener.call(this.target,arguments[0]);case 2:return this.listener.call(this.target,arguments[0],arguments[1]);case 3:return this.listener.call(this.target,arguments[0],arguments[1],arguments[2]);default:var args=new Array(arguments.length);for(var i=0;i=0;i--){if(list[i]===listener||list[i].listener===listener){originalListener=list[i].listener;position=i;break}}if(position<0)return this;if(position===0)list.shift();else spliceOne(list,position);if(list.length===1)events[type]=list[0];if(events.removeListener)this.emit("removeListener",type,originalListener||listener)}return this};EventEmitter.prototype.removeAllListeners=function removeAllListeners(type){var listeners,events,i;events=this._events;if(!events)return this;if(!events.removeListener){if(arguments.length===0){this._events=objectCreate(null);this._eventsCount=0}else if(events[type]){if(--this._eventsCount===0)this._events=objectCreate(null);else delete events[type]}return this}if(arguments.length===0){var keys=objectKeys(events);var key;for(i=0;i=0;i--){this.removeListener(type,listeners[i])}}return this};function _listeners(target,type,unwrap){var events=target._events;if(!events)return[];var evlistener=events[type];if(!evlistener)return[];if(typeof evlistener==="function")return unwrap?[evlistener.listener||evlistener]:[evlistener];return unwrap?unwrapListeners(evlistener):arrayClone(evlistener,evlistener.length)}EventEmitter.prototype.listeners=function listeners(type){return _listeners(this,type,true)};EventEmitter.prototype.rawListeners=function rawListeners(type){return _listeners(this,type,false)};EventEmitter.listenerCount=function(emitter,type){if(typeof emitter.listenerCount==="function"){return emitter.listenerCount(type)}else{return listenerCount.call(emitter,type)}};EventEmitter.prototype.listenerCount=listenerCount;function listenerCount(type){var events=this._events;if(events){var evlistener=events[type];if(typeof evlistener==="function"){return 1}else if(evlistener){return evlistener.length}}return 0}EventEmitter.prototype.eventNames=function eventNames(){return this._eventsCount>0?Reflect.ownKeys(this._events):[]};function spliceOne(list,index){for(var i=index,k=i+1,n=list.length;k=0&&level<=self.levels.SILENT){currentLevel=level;if(persist!==false){persistLevelIfPossible(level)}replaceLoggingMethods.call(self,level,name);if(typeof console===undefinedType&&level>>24)&16711935|(m[i]<<24|m[i]>>>8)&4278255360}m[l>>>5]|=128<>>9<<4)+14]=l;var FF=md5._ff,GG=md5._gg,HH=md5._hh,II=md5._ii;for(var i=0;i>>0;b=b+bb>>>0;c=c+cc>>>0;d=d+dd>>>0}return crypt.endian([a,b,c,d])};md5._ff=function(a,b,c,d,x,s,t){var n=a+(b&c|~b&d)+(x>>>0)+t;return(n<>>32-s)+b};md5._gg=function(a,b,c,d,x,s,t){var n=a+(b&d|c&~d)+(x>>>0)+t;return(n<>>32-s)+b};md5._hh=function(a,b,c,d,x,s,t){var n=a+(b^c^d)+(x>>>0)+t;return(n<>>32-s)+b};md5._ii=function(a,b,c,d,x,s,t){var n=a+(c^(b|~d))+(x>>>0)+t;return(n<>>32-s)+b};md5._blocksize=16;md5._digestsize=16;module.exports=function(message,options){if(message===undefined||message===null)throw new Error("Illegal argument "+message);var digestbytes=crypt.wordsToBytes(md5(message,options));return options&&options.asBytes?digestbytes:options&&options.asString?bin.bytesToString(digestbytes):crypt.bytesToHex(digestbytes)}})()},{charenc:51,crypt:52,"is-buffer":54}],57:[function(require,module,exports){module.exports=require("./lib/checks")},{"./lib/checks":58}],58:[function(require,module,exports){var util=require("util");var errors=module.exports=require("./errors");function failCheck(ExceptionConstructor,callee,messageFormat,formatArgs){messageFormat=messageFormat||"";var message=util.format.apply(this,[messageFormat].concat(formatArgs));var error=new ExceptionConstructor(message);Error.captureStackTrace(error,callee);throw error}function failArgumentCheck(callee,message,formatArgs){failCheck(errors.IllegalArgumentError,callee,message,formatArgs)}function failStateCheck(callee,message,formatArgs){failCheck(errors.IllegalStateError,callee,message,formatArgs)}module.exports.checkArgument=function(value,message){if(!value){failArgumentCheck(arguments.callee,message,Array.prototype.slice.call(arguments,2))}};module.exports.checkState=function(value,message){if(!value){failStateCheck(arguments.callee,message,Array.prototype.slice.call(arguments,2))}};module.exports.checkIsDef=function(value,message){if(value!==undefined){return value}failArgumentCheck(arguments.callee,message||"Expected value to be defined but was undefined.",Array.prototype.slice.call(arguments,2))};module.exports.checkIsDefAndNotNull=function(value,message){if(value!=null){return value}failArgumentCheck(arguments.callee,message||'Expected value to be defined and not null but got "'+typeOf(value)+'".',Array.prototype.slice.call(arguments,2))};function typeOf(value){var s=typeof value;if(s=="object"){if(!value){return"null"}else if(value instanceof Array){return"array"}}return s}function typeCheck(expect){return function(value,message){var type=typeOf(value);if(type==expect){return value}failArgumentCheck(arguments.callee,message||'Expected "'+expect+'" but got "'+type+'".',Array.prototype.slice.call(arguments,2))}}module.exports.checkIsString=typeCheck("string");module.exports.checkIsArray=typeCheck("array");module.exports.checkIsNumber=typeCheck("number");module.exports.checkIsBoolean=typeCheck("boolean");module.exports.checkIsFunction=typeCheck("function");module.exports.checkIsObject=typeCheck("object")},{"./errors":59,util:65}],59:[function(require,module,exports){var util=require("util");function IllegalArgumentError(message){Error.call(this,message);this.message=message}util.inherits(IllegalArgumentError,Error);IllegalArgumentError.prototype.name="IllegalArgumentError";function IllegalStateError(message){Error.call(this,message);this.message=message}util.inherits(IllegalStateError,Error);IllegalStateError.prototype.name="IllegalStateError";module.exports.IllegalStateError=IllegalStateError;module.exports.IllegalArgumentError=IllegalArgumentError},{util:65}],60:[function(require,module,exports){var process=module.exports={};var cachedSetTimeout;var cachedClearTimeout;function defaultSetTimout(){throw new Error("setTimeout has not been defined")}function defaultClearTimeout(){throw new Error("clearTimeout has not been defined")}(function(){try{if(typeof setTimeout==="function"){cachedSetTimeout=setTimeout}else{cachedSetTimeout=defaultSetTimout}}catch(e){cachedSetTimeout=defaultSetTimout}try{if(typeof clearTimeout==="function"){cachedClearTimeout=clearTimeout}else{cachedClearTimeout=defaultClearTimeout}}catch(e){cachedClearTimeout=defaultClearTimeout}})();function runTimeout(fun){if(cachedSetTimeout===setTimeout){return setTimeout(fun,0)}if((cachedSetTimeout===defaultSetTimout||!cachedSetTimeout)&&setTimeout){cachedSetTimeout=setTimeout;return setTimeout(fun,0)}try{return cachedSetTimeout(fun,0)}catch(e){try{return cachedSetTimeout.call(null,fun,0)}catch(e){return cachedSetTimeout.call(this,fun,0)}}}function runClearTimeout(marker){if(cachedClearTimeout===clearTimeout){return clearTimeout(marker)}if((cachedClearTimeout===defaultClearTimeout||!cachedClearTimeout)&&clearTimeout){cachedClearTimeout=clearTimeout;return clearTimeout(marker)}try{return cachedClearTimeout(marker)}catch(e){try{return cachedClearTimeout.call(null,marker)}catch(e){return cachedClearTimeout.call(this,marker)}}}var queue=[];var draining=false;var currentQueue;var queueIndex=-1;function cleanUpNextTick(){if(!draining||!currentQueue){return}draining=false;if(currentQueue.length){queue=currentQueue.concat(queue)}else{queueIndex=-1}if(queue.length){drainQueue()}}function drainQueue(){if(draining){return}var timeout=runTimeout(cleanUpNextTick);draining=true;var len=queue.length;while(len){currentQueue=queue;queue=[];while(++queueIndex1){for(var i=1;i=14393&&url.indexOf("?transport=udp")===-1});delete server.url;server.urls=isString?urls[0]:urls;return!!urls.length}})}function getCommonCapabilities(localCapabilities,remoteCapabilities){var commonCapabilities={codecs:[],headerExtensions:[],fecMechanisms:[]};var findCodecByPayloadType=function(pt,codecs){pt=parseInt(pt,10);for(var i=0;i0;i--){this._iceGatherers.push(new window.RTCIceGatherer({iceServers:config.iceServers,gatherPolicy:config.iceTransportPolicy}))}}else{config.iceCandidatePoolSize=0}this._config=config;this.transceivers=[];this._sdpSessionId=SDPUtils.generateSessionId();this._sdpSessionVersion=0;this._dtlsRole=undefined;this._isClosed=false};RTCPeerConnection.prototype.onicecandidate=null;RTCPeerConnection.prototype.onaddstream=null;RTCPeerConnection.prototype.ontrack=null;RTCPeerConnection.prototype.onremovestream=null;RTCPeerConnection.prototype.onsignalingstatechange=null;RTCPeerConnection.prototype.oniceconnectionstatechange=null;RTCPeerConnection.prototype.onicegatheringstatechange=null;RTCPeerConnection.prototype.onnegotiationneeded=null;RTCPeerConnection.prototype.ondatachannel=null;RTCPeerConnection.prototype._dispatchEvent=function(name,event){if(this._isClosed){return}this.dispatchEvent(event);if(typeof this["on"+name]==="function"){this["on"+name](event)}};RTCPeerConnection.prototype._emitGatheringStateChange=function(){var event=new Event("icegatheringstatechange");this._dispatchEvent("icegatheringstatechange",event)};RTCPeerConnection.prototype.getConfiguration=function(){return this._config};RTCPeerConnection.prototype.getLocalStreams=function(){return this.localStreams};RTCPeerConnection.prototype.getRemoteStreams=function(){return this.remoteStreams};RTCPeerConnection.prototype._createTransceiver=function(kind){var hasBundleTransport=this.transceivers.length>0;var transceiver={track:null,iceGatherer:null,iceTransport:null,dtlsTransport:null,localCapabilities:null,remoteCapabilities:null,rtpSender:null,rtpReceiver:null,kind:kind,mid:null,sendEncodingParameters:null,recvEncodingParameters:null,stream:null,associatedRemoteMediaStreams:[],wantReceive:true};if(this.usingBundle&&hasBundleTransport){transceiver.iceTransport=this.transceivers[0].iceTransport;transceiver.dtlsTransport=this.transceivers[0].dtlsTransport}else{var transports=this._createIceAndDtlsTransports();transceiver.iceTransport=transports.iceTransport;transceiver.dtlsTransport=transports.dtlsTransport}this.transceivers.push(transceiver);return transceiver};RTCPeerConnection.prototype.addTrack=function(track,stream){if(this._isClosed){throw makeError("InvalidStateError","Attempted to call addTrack on a closed peerconnection.")}var alreadyExists=this.transceivers.find(function(s){return s.track===track});if(alreadyExists){throw makeError("InvalidAccessError","Track already exists.")}var transceiver;for(var i=0;i=15025){stream.getTracks().forEach(function(track){pc.addTrack(track,stream)})}else{var clonedStream=stream.clone();stream.getTracks().forEach(function(track,idx){var clonedTrack=clonedStream.getTracks()[idx];track.addEventListener("enabled",function(event){clonedTrack.enabled=event.enabled})});clonedStream.getTracks().forEach(function(track){pc.addTrack(track,clonedStream)})}};RTCPeerConnection.prototype.removeTrack=function(sender){if(this._isClosed){throw makeError("InvalidStateError","Attempted to call removeTrack on a closed peerconnection.")}if(!(sender instanceof window.RTCRtpSender)){throw new TypeError("Argument 1 of RTCPeerConnection.removeTrack "+"does not implement interface RTCRtpSender.")}var transceiver=this.transceivers.find(function(t){return t.rtpSender===sender});if(!transceiver){throw makeError("InvalidAccessError","Sender was not created by this connection.")}var stream=transceiver.stream;transceiver.rtpSender.stop();transceiver.rtpSender=null;transceiver.track=null;transceiver.stream=null;var localStreams=this.transceivers.map(function(t){return t.stream});if(localStreams.indexOf(stream)===-1&&this.localStreams.indexOf(stream)>-1){this.localStreams.splice(this.localStreams.indexOf(stream),1)}this._maybeFireNegotiationNeeded()};RTCPeerConnection.prototype.removeStream=function(stream){var pc=this;stream.getTracks().forEach(function(track){var sender=pc.getSenders().find(function(s){return s.track===track});if(sender){pc.removeTrack(sender)}})};RTCPeerConnection.prototype.getSenders=function(){return this.transceivers.filter(function(transceiver){return!!transceiver.rtpSender}).map(function(transceiver){return transceiver.rtpSender})};RTCPeerConnection.prototype.getReceivers=function(){return this.transceivers.filter(function(transceiver){return!!transceiver.rtpReceiver}).map(function(transceiver){return transceiver.rtpReceiver})};RTCPeerConnection.prototype._createIceGatherer=function(sdpMLineIndex,usingBundle){var pc=this;if(usingBundle&&sdpMLineIndex>0){return this.transceivers[0].iceGatherer}else if(this._iceGatherers.length){return this._iceGatherers.shift()}var iceGatherer=new window.RTCIceGatherer({iceServers:this._config.iceServers,gatherPolicy:this._config.iceTransportPolicy});Object.defineProperty(iceGatherer,"state",{value:"new",writable:true});this.transceivers[sdpMLineIndex].bufferedCandidateEvents=[];this.transceivers[sdpMLineIndex].bufferCandidates=function(event){var end=!event.candidate||Object.keys(event.candidate).length===0;iceGatherer.state=end?"completed":"gathering";if(pc.transceivers[sdpMLineIndex].bufferedCandidateEvents!==null){pc.transceivers[sdpMLineIndex].bufferedCandidateEvents.push(event)}};iceGatherer.addEventListener("localcandidate",this.transceivers[sdpMLineIndex].bufferCandidates);return iceGatherer};RTCPeerConnection.prototype._gather=function(mid,sdpMLineIndex){var pc=this;var iceGatherer=this.transceivers[sdpMLineIndex].iceGatherer;if(iceGatherer.onlocalcandidate){return}var bufferedCandidateEvents=this.transceivers[sdpMLineIndex].bufferedCandidateEvents;this.transceivers[sdpMLineIndex].bufferedCandidateEvents=null;iceGatherer.removeEventListener("localcandidate",this.transceivers[sdpMLineIndex].bufferCandidates);iceGatherer.onlocalcandidate=function(evt){if(pc.usingBundle&&sdpMLineIndex>0){return}var event=new Event("icecandidate");event.candidate={sdpMid:mid,sdpMLineIndex:sdpMLineIndex};var cand=evt.candidate;var end=!cand||Object.keys(cand).length===0;if(end){if(iceGatherer.state==="new"||iceGatherer.state==="gathering"){iceGatherer.state="completed"}}else{if(iceGatherer.state==="new"){iceGatherer.state="gathering"}cand.component=1;var serializedCandidate=SDPUtils.writeCandidate(cand);event.candidate=Object.assign(event.candidate,SDPUtils.parseCandidate(serializedCandidate));event.candidate.candidate=serializedCandidate}var sections=SDPUtils.getMediaSections(pc.localDescription.sdp);if(!end){sections[event.candidate.sdpMLineIndex]+="a="+event.candidate.candidate+"\r\n"}else{sections[event.candidate.sdpMLineIndex]+="a=end-of-candidates\r\n"}pc.localDescription.sdp=SDPUtils.getDescription(pc.localDescription.sdp)+sections.join("");var complete=pc.transceivers.every(function(transceiver){return transceiver.iceGatherer&&transceiver.iceGatherer.state==="completed"});if(pc.iceGatheringState!=="gathering"){pc.iceGatheringState="gathering";pc._emitGatheringStateChange()}if(!end){pc._dispatchEvent("icecandidate",event)}if(complete){pc._dispatchEvent("icecandidate",new Event("icecandidate"));pc.iceGatheringState="complete";pc._emitGatheringStateChange()}};window.setTimeout(function(){bufferedCandidateEvents.forEach(function(e){iceGatherer.onlocalcandidate(e)})},0)};RTCPeerConnection.prototype._createIceAndDtlsTransports=function(){var pc=this;var iceTransport=new window.RTCIceTransport(null);iceTransport.onicestatechange=function(){pc._updateConnectionState()};var dtlsTransport=new window.RTCDtlsTransport(iceTransport);dtlsTransport.ondtlsstatechange=function(){pc._updateConnectionState()};dtlsTransport.onerror=function(){Object.defineProperty(dtlsTransport,"state",{value:"failed",writable:true});pc._updateConnectionState()};return{iceTransport:iceTransport,dtlsTransport:dtlsTransport}};RTCPeerConnection.prototype._disposeIceAndDtlsTransports=function(sdpMLineIndex){var iceGatherer=this.transceivers[sdpMLineIndex].iceGatherer;if(iceGatherer){delete iceGatherer.onlocalcandidate;delete this.transceivers[sdpMLineIndex].iceGatherer}var iceTransport=this.transceivers[sdpMLineIndex].iceTransport;if(iceTransport){delete iceTransport.onicestatechange;delete this.transceivers[sdpMLineIndex].iceTransport}var dtlsTransport=this.transceivers[sdpMLineIndex].dtlsTransport;if(dtlsTransport){delete dtlsTransport.ondtlsstatechange;delete dtlsTransport.onerror;delete this.transceivers[sdpMLineIndex].dtlsTransport}};RTCPeerConnection.prototype._transceive=function(transceiver,send,recv){var params=getCommonCapabilities(transceiver.localCapabilities,transceiver.remoteCapabilities);if(send&&transceiver.rtpSender){params.encodings=transceiver.sendEncodingParameters;params.rtcp={cname:SDPUtils.localCName,compound:transceiver.rtcpParameters.compound};if(transceiver.recvEncodingParameters.length){params.rtcp.ssrc=transceiver.recvEncodingParameters[0].ssrc}transceiver.rtpSender.send(params)}if(recv&&transceiver.rtpReceiver&¶ms.codecs.length>0){if(transceiver.kind==="video"&&transceiver.recvEncodingParameters&&edgeVersion<15019){transceiver.recvEncodingParameters.forEach(function(p){delete p.rtx})}if(transceiver.recvEncodingParameters.length){params.encodings=transceiver.recvEncodingParameters}else{params.encodings=[{}]}params.rtcp={compound:transceiver.rtcpParameters.compound};if(transceiver.rtcpParameters.cname){params.rtcp.cname=transceiver.rtcpParameters.cname}if(transceiver.sendEncodingParameters.length){params.rtcp.ssrc=transceiver.sendEncodingParameters[0].ssrc}transceiver.rtpReceiver.receive(params)}};RTCPeerConnection.prototype.setLocalDescription=function(description){var pc=this;if(["offer","answer"].indexOf(description.type)===-1){return Promise.reject(makeError("TypeError",'Unsupported type "'+description.type+'"'))}if(!isActionAllowedInSignalingState("setLocalDescription",description.type,pc.signalingState)||pc._isClosed){return Promise.reject(makeError("InvalidStateError","Can not set local "+description.type+" in state "+pc.signalingState))}var sections;var sessionpart;if(description.type==="offer"){sections=SDPUtils.splitSections(description.sdp);sessionpart=sections.shift();sections.forEach(function(mediaSection,sdpMLineIndex){var caps=SDPUtils.parseRtpParameters(mediaSection);pc.transceivers[sdpMLineIndex].localCapabilities=caps});pc.transceivers.forEach(function(transceiver,sdpMLineIndex){pc._gather(transceiver.mid,sdpMLineIndex)})}else if(description.type==="answer"){sections=SDPUtils.splitSections(pc.remoteDescription.sdp);sessionpart=sections.shift();var isIceLite=SDPUtils.matchPrefix(sessionpart,"a=ice-lite").length>0;sections.forEach(function(mediaSection,sdpMLineIndex){var transceiver=pc.transceivers[sdpMLineIndex];var iceGatherer=transceiver.iceGatherer;var iceTransport=transceiver.iceTransport;var dtlsTransport=transceiver.dtlsTransport;var localCapabilities=transceiver.localCapabilities;var remoteCapabilities=transceiver.remoteCapabilities;var rejected=SDPUtils.isRejected(mediaSection)&&SDPUtils.matchPrefix(mediaSection,"a=bundle-only").length===0;if(!rejected&&!transceiver.isDatachannel){var remoteIceParameters=SDPUtils.getIceParameters(mediaSection,sessionpart);var remoteDtlsParameters=SDPUtils.getDtlsParameters(mediaSection,sessionpart);if(isIceLite){remoteDtlsParameters.role="server"}if(!pc.usingBundle||sdpMLineIndex===0){pc._gather(transceiver.mid,sdpMLineIndex);if(iceTransport.state==="new"){iceTransport.start(iceGatherer,remoteIceParameters,isIceLite?"controlling":"controlled")}if(dtlsTransport.state==="new"){dtlsTransport.start(remoteDtlsParameters)}}var params=getCommonCapabilities(localCapabilities,remoteCapabilities);pc._transceive(transceiver,params.codecs.length>0,false)}})}pc.localDescription={type:description.type,sdp:description.sdp};if(description.type==="offer"){pc._updateSignalingState("have-local-offer")}else{pc._updateSignalingState("stable")}return Promise.resolve()};RTCPeerConnection.prototype.setRemoteDescription=function(description){var pc=this;if(["offer","answer"].indexOf(description.type)===-1){return Promise.reject(makeError("TypeError",'Unsupported type "'+description.type+'"'))}if(!isActionAllowedInSignalingState("setRemoteDescription",description.type,pc.signalingState)||pc._isClosed){return Promise.reject(makeError("InvalidStateError","Can not set remote "+description.type+" in state "+pc.signalingState))}var streams={};pc.remoteStreams.forEach(function(stream){streams[stream.id]=stream});var receiverList=[];var sections=SDPUtils.splitSections(description.sdp);var sessionpart=sections.shift();var isIceLite=SDPUtils.matchPrefix(sessionpart,"a=ice-lite").length>0;var usingBundle=SDPUtils.matchPrefix(sessionpart,"a=group:BUNDLE ").length>0;pc.usingBundle=usingBundle;var iceOptions=SDPUtils.matchPrefix(sessionpart,"a=ice-options:")[0];if(iceOptions){pc.canTrickleIceCandidates=iceOptions.substr(14).split(" ").indexOf("trickle")>=0}else{pc.canTrickleIceCandidates=false}sections.forEach(function(mediaSection,sdpMLineIndex){var lines=SDPUtils.splitLines(mediaSection);var kind=SDPUtils.getKind(mediaSection);var rejected=SDPUtils.isRejected(mediaSection)&&SDPUtils.matchPrefix(mediaSection,"a=bundle-only").length===0;var protocol=lines[0].substr(2).split(" ")[2];var direction=SDPUtils.getDirection(mediaSection,sessionpart);var remoteMsid=SDPUtils.parseMsid(mediaSection);var mid=SDPUtils.getMid(mediaSection)||SDPUtils.generateIdentifier();if(kind==="application"&&protocol==="DTLS/SCTP"){pc.transceivers[sdpMLineIndex]={mid:mid,isDatachannel:true};return}var transceiver;var iceGatherer;var iceTransport;var dtlsTransport;var rtpReceiver;var sendEncodingParameters;var recvEncodingParameters;var localCapabilities;var track;var remoteCapabilities=SDPUtils.parseRtpParameters(mediaSection);var remoteIceParameters;var remoteDtlsParameters;if(!rejected){remoteIceParameters=SDPUtils.getIceParameters(mediaSection,sessionpart);remoteDtlsParameters=SDPUtils.getDtlsParameters(mediaSection,sessionpart);remoteDtlsParameters.role="client"}recvEncodingParameters=SDPUtils.parseRtpEncodingParameters(mediaSection);var rtcpParameters=SDPUtils.parseRtcpParameters(mediaSection);var isComplete=SDPUtils.matchPrefix(mediaSection,"a=end-of-candidates",sessionpart).length>0;var cands=SDPUtils.matchPrefix(mediaSection,"a=candidate:").map(function(cand){return SDPUtils.parseCandidate(cand)}).filter(function(cand){return cand.component===1});if((description.type==="offer"||description.type==="answer")&&!rejected&&usingBundle&&sdpMLineIndex>0&&pc.transceivers[sdpMLineIndex]){pc._disposeIceAndDtlsTransports(sdpMLineIndex);pc.transceivers[sdpMLineIndex].iceGatherer=pc.transceivers[0].iceGatherer;pc.transceivers[sdpMLineIndex].iceTransport=pc.transceivers[0].iceTransport;pc.transceivers[sdpMLineIndex].dtlsTransport=pc.transceivers[0].dtlsTransport;if(pc.transceivers[sdpMLineIndex].rtpSender){pc.transceivers[sdpMLineIndex].rtpSender.setTransport(pc.transceivers[0].dtlsTransport)}if(pc.transceivers[sdpMLineIndex].rtpReceiver){pc.transceivers[sdpMLineIndex].rtpReceiver.setTransport(pc.transceivers[0].dtlsTransport)}}if(description.type==="offer"&&!rejected){transceiver=pc.transceivers[sdpMLineIndex]||pc._createTransceiver(kind);transceiver.mid=mid;if(!transceiver.iceGatherer){transceiver.iceGatherer=pc._createIceGatherer(sdpMLineIndex,usingBundle)}if(cands.length&&transceiver.iceTransport.state==="new"){if(isComplete&&(!usingBundle||sdpMLineIndex===0)){transceiver.iceTransport.setRemoteCandidates(cands)}else{cands.forEach(function(candidate){maybeAddCandidate(transceiver.iceTransport,candidate)})}}localCapabilities=window.RTCRtpReceiver.getCapabilities(kind);if(edgeVersion<15019){localCapabilities.codecs=localCapabilities.codecs.filter(function(codec){return codec.name!=="rtx"})}sendEncodingParameters=transceiver.sendEncodingParameters||[{ssrc:(2*sdpMLineIndex+2)*1001}];var isNewTrack=false;if(direction==="sendrecv"||direction==="sendonly"){isNewTrack=!transceiver.rtpReceiver;rtpReceiver=transceiver.rtpReceiver||new window.RTCRtpReceiver(transceiver.dtlsTransport,kind);if(isNewTrack){var stream;track=rtpReceiver.track;if(remoteMsid&&remoteMsid.stream==="-"){}else if(remoteMsid){if(!streams[remoteMsid.stream]){streams[remoteMsid.stream]=new window.MediaStream;Object.defineProperty(streams[remoteMsid.stream],"id",{get:function(){return remoteMsid.stream}})}Object.defineProperty(track,"id",{get:function(){return remoteMsid.track}});stream=streams[remoteMsid.stream]}else{if(!streams.default){streams.default=new window.MediaStream}stream=streams.default}if(stream){addTrackToStreamAndFireEvent(track,stream);transceiver.associatedRemoteMediaStreams.push(stream)}receiverList.push([track,rtpReceiver,stream])}}else if(transceiver.rtpReceiver&&transceiver.rtpReceiver.track){transceiver.associatedRemoteMediaStreams.forEach(function(s){var nativeTrack=s.getTracks().find(function(t){return t.id===transceiver.rtpReceiver.track.id});if(nativeTrack){removeTrackFromStreamAndFireEvent(nativeTrack,s)}});transceiver.associatedRemoteMediaStreams=[]}transceiver.localCapabilities=localCapabilities;transceiver.remoteCapabilities=remoteCapabilities;transceiver.rtpReceiver=rtpReceiver;transceiver.rtcpParameters=rtcpParameters;transceiver.sendEncodingParameters=sendEncodingParameters;transceiver.recvEncodingParameters=recvEncodingParameters;pc._transceive(pc.transceivers[sdpMLineIndex],false,isNewTrack)}else if(description.type==="answer"&&!rejected){transceiver=pc.transceivers[sdpMLineIndex];iceGatherer=transceiver.iceGatherer;iceTransport=transceiver.iceTransport;dtlsTransport=transceiver.dtlsTransport;rtpReceiver=transceiver.rtpReceiver;sendEncodingParameters=transceiver.sendEncodingParameters;localCapabilities=transceiver.localCapabilities;pc.transceivers[sdpMLineIndex].recvEncodingParameters=recvEncodingParameters;pc.transceivers[sdpMLineIndex].remoteCapabilities=remoteCapabilities;pc.transceivers[sdpMLineIndex].rtcpParameters=rtcpParameters;if(cands.length&&iceTransport.state==="new"){if((isIceLite||isComplete)&&(!usingBundle||sdpMLineIndex===0)){iceTransport.setRemoteCandidates(cands)}else{cands.forEach(function(candidate){maybeAddCandidate(transceiver.iceTransport,candidate)})}}if(!usingBundle||sdpMLineIndex===0){if(iceTransport.state==="new"){iceTransport.start(iceGatherer,remoteIceParameters,"controlling")}if(dtlsTransport.state==="new"){dtlsTransport.start(remoteDtlsParameters)}}pc._transceive(transceiver,direction==="sendrecv"||direction==="recvonly",direction==="sendrecv"||direction==="sendonly");if(rtpReceiver&&(direction==="sendrecv"||direction==="sendonly")){track=rtpReceiver.track;if(remoteMsid){if(!streams[remoteMsid.stream]){streams[remoteMsid.stream]=new window.MediaStream}addTrackToStreamAndFireEvent(track,streams[remoteMsid.stream]);receiverList.push([track,rtpReceiver,streams[remoteMsid.stream]])}else{if(!streams.default){streams.default=new window.MediaStream}addTrackToStreamAndFireEvent(track,streams.default);receiverList.push([track,rtpReceiver,streams.default])}}else{delete transceiver.rtpReceiver}}});if(pc._dtlsRole===undefined){pc._dtlsRole=description.type==="offer"?"active":"passive"}pc.remoteDescription={type:description.type,sdp:description.sdp};if(description.type==="offer"){pc._updateSignalingState("have-remote-offer")}else{pc._updateSignalingState("stable")}Object.keys(streams).forEach(function(sid){var stream=streams[sid];if(stream.getTracks().length){if(pc.remoteStreams.indexOf(stream)===-1){pc.remoteStreams.push(stream);var event=new Event("addstream");event.stream=stream;window.setTimeout(function(){pc._dispatchEvent("addstream",event)})}receiverList.forEach(function(item){var track=item[0];var receiver=item[1];if(stream.id!==item[2].id){return}fireAddTrack(pc,track,receiver,[stream])})}});receiverList.forEach(function(item){if(item[2]){return}fireAddTrack(pc,item[0],item[1],[])});window.setTimeout(function(){if(!(pc&&pc.transceivers)){return}pc.transceivers.forEach(function(transceiver){if(transceiver.iceTransport&&transceiver.iceTransport.state==="new"&&transceiver.iceTransport.getRemoteCandidates().length>0){console.warn("Timeout for addRemoteCandidate. Consider sending "+"an end-of-candidates notification");transceiver.iceTransport.addRemoteCandidate({})}})},4e3);return Promise.resolve()};RTCPeerConnection.prototype.close=function(){this.transceivers.forEach(function(transceiver){if(transceiver.iceTransport){transceiver.iceTransport.stop()}if(transceiver.dtlsTransport){transceiver.dtlsTransport.stop()}if(transceiver.rtpSender){transceiver.rtpSender.stop()}if(transceiver.rtpReceiver){transceiver.rtpReceiver.stop()}});this._isClosed=true;this._updateSignalingState("closed")};RTCPeerConnection.prototype._updateSignalingState=function(newState){this.signalingState=newState;var event=new Event("signalingstatechange");this._dispatchEvent("signalingstatechange",event)};RTCPeerConnection.prototype._maybeFireNegotiationNeeded=function(){var pc=this;if(this.signalingState!=="stable"||this.needNegotiation===true){return}this.needNegotiation=true;window.setTimeout(function(){if(pc.needNegotiation){pc.needNegotiation=false;var event=new Event("negotiationneeded");pc._dispatchEvent("negotiationneeded",event)}},0)};RTCPeerConnection.prototype._updateConnectionState=function(){var newState;var states={new:0,closed:0,connecting:0,checking:0,connected:0,completed:0,disconnected:0,failed:0};this.transceivers.forEach(function(transceiver){states[transceiver.iceTransport.state]++;states[transceiver.dtlsTransport.state]++});states.connected+=states.completed;newState="new";if(states.failed>0){newState="failed"}else if(states.connecting>0||states.checking>0){newState="connecting"}else if(states.disconnected>0){newState="disconnected"}else if(states.new>0){newState="new"}else if(states.connected>0||states.completed>0){newState="connected"}if(newState!==this.iceConnectionState){this.iceConnectionState=newState;var event=new Event("iceconnectionstatechange");this._dispatchEvent("iceconnectionstatechange",event)}};RTCPeerConnection.prototype.createOffer=function(){var pc=this;if(pc._isClosed){return Promise.reject(makeError("InvalidStateError","Can not call createOffer after close"))}var numAudioTracks=pc.transceivers.filter(function(t){return t.kind==="audio"}).length;var numVideoTracks=pc.transceivers.filter(function(t){return t.kind==="video"}).length;var offerOptions=arguments[0];if(offerOptions){if(offerOptions.mandatory||offerOptions.optional){throw new TypeError("Legacy mandatory/optional constraints not supported.")}if(offerOptions.offerToReceiveAudio!==undefined){if(offerOptions.offerToReceiveAudio===true){numAudioTracks=1}else if(offerOptions.offerToReceiveAudio===false){numAudioTracks=0}else{numAudioTracks=offerOptions.offerToReceiveAudio}}if(offerOptions.offerToReceiveVideo!==undefined){if(offerOptions.offerToReceiveVideo===true){numVideoTracks=1}else if(offerOptions.offerToReceiveVideo===false){numVideoTracks=0}else{numVideoTracks=offerOptions.offerToReceiveVideo}}}pc.transceivers.forEach(function(transceiver){if(transceiver.kind==="audio"){numAudioTracks--;if(numAudioTracks<0){transceiver.wantReceive=false}}else if(transceiver.kind==="video"){numVideoTracks--;if(numVideoTracks<0){transceiver.wantReceive=false}}});while(numAudioTracks>0||numVideoTracks>0){if(numAudioTracks>0){pc._createTransceiver("audio");numAudioTracks--}if(numVideoTracks>0){pc._createTransceiver("video");numVideoTracks--}}var sdp=SDPUtils.writeSessionBoilerplate(pc._sdpSessionId,pc._sdpSessionVersion++);pc.transceivers.forEach(function(transceiver,sdpMLineIndex){var track=transceiver.track;var kind=transceiver.kind;var mid=transceiver.mid||SDPUtils.generateIdentifier();transceiver.mid=mid;if(!transceiver.iceGatherer){transceiver.iceGatherer=pc._createIceGatherer(sdpMLineIndex,pc.usingBundle)}var localCapabilities=window.RTCRtpSender.getCapabilities(kind);if(edgeVersion<15019){localCapabilities.codecs=localCapabilities.codecs.filter(function(codec){return codec.name!=="rtx"})}localCapabilities.codecs.forEach(function(codec){if(codec.name==="H264"&&codec.parameters["level-asymmetry-allowed"]===undefined){codec.parameters["level-asymmetry-allowed"]="1"}if(transceiver.remoteCapabilities&&transceiver.remoteCapabilities.codecs){transceiver.remoteCapabilities.codecs.forEach(function(remoteCodec){if(codec.name.toLowerCase()===remoteCodec.name.toLowerCase()&&codec.clockRate===remoteCodec.clockRate){codec.preferredPayloadType=remoteCodec.payloadType}})}});localCapabilities.headerExtensions.forEach(function(hdrExt){var remoteExtensions=transceiver.remoteCapabilities&&transceiver.remoteCapabilities.headerExtensions||[];remoteExtensions.forEach(function(rHdrExt){if(hdrExt.uri===rHdrExt.uri){hdrExt.id=rHdrExt.id}})});var sendEncodingParameters=transceiver.sendEncodingParameters||[{ssrc:(2*sdpMLineIndex+1)*1001}];if(track){if(edgeVersion>=15019&&kind==="video"&&!sendEncodingParameters[0].rtx){sendEncodingParameters[0].rtx={ssrc:sendEncodingParameters[0].ssrc+1}}}if(transceiver.wantReceive){transceiver.rtpReceiver=new window.RTCRtpReceiver(transceiver.dtlsTransport,kind)}transceiver.localCapabilities=localCapabilities;transceiver.sendEncodingParameters=sendEncodingParameters});if(pc._config.bundlePolicy!=="max-compat"){sdp+="a=group:BUNDLE "+pc.transceivers.map(function(t){return t.mid}).join(" ")+"\r\n"}sdp+="a=ice-options:trickle\r\n";pc.transceivers.forEach(function(transceiver,sdpMLineIndex){sdp+=writeMediaSection(transceiver,transceiver.localCapabilities,"offer",transceiver.stream,pc._dtlsRole);sdp+="a=rtcp-rsize\r\n";if(transceiver.iceGatherer&&pc.iceGatheringState!=="new"&&(sdpMLineIndex===0||!pc.usingBundle)){transceiver.iceGatherer.getLocalCandidates().forEach(function(cand){cand.component=1;sdp+="a="+SDPUtils.writeCandidate(cand)+"\r\n"});if(transceiver.iceGatherer.state==="completed"){sdp+="a=end-of-candidates\r\n"}}});var desc=new window.RTCSessionDescription({type:"offer",sdp:sdp});return Promise.resolve(desc)};RTCPeerConnection.prototype.createAnswer=function(){var pc=this;if(pc._isClosed){return Promise.reject(makeError("InvalidStateError","Can not call createAnswer after close"))}var sdp=SDPUtils.writeSessionBoilerplate(pc._sdpSessionId,pc._sdpSessionVersion++);if(pc.usingBundle){sdp+="a=group:BUNDLE "+pc.transceivers.map(function(t){return t.mid}).join(" ")+"\r\n"}var mediaSectionsInOffer=SDPUtils.getMediaSections(pc.remoteDescription.sdp).length;pc.transceivers.forEach(function(transceiver,sdpMLineIndex){if(sdpMLineIndex+1>mediaSectionsInOffer){return}if(transceiver.isDatachannel){sdp+="m=application 0 DTLS/SCTP 5000\r\n"+"c=IN IP4 0.0.0.0\r\n"+"a=mid:"+transceiver.mid+"\r\n";return}if(transceiver.stream){var localTrack;if(transceiver.kind==="audio"){localTrack=transceiver.stream.getAudioTracks()[0]}else if(transceiver.kind==="video"){localTrack=transceiver.stream.getVideoTracks()[0]}if(localTrack){if(edgeVersion>=15019&&transceiver.kind==="video"&&!transceiver.sendEncodingParameters[0].rtx){transceiver.sendEncodingParameters[0].rtx={ssrc:transceiver.sendEncodingParameters[0].ssrc+1}}}}var commonCapabilities=getCommonCapabilities(transceiver.localCapabilities,transceiver.remoteCapabilities);var hasRtx=commonCapabilities.codecs.filter(function(c){return c.name.toLowerCase()==="rtx"}).length;if(!hasRtx&&transceiver.sendEncodingParameters[0].rtx){delete transceiver.sendEncodingParameters[0].rtx}sdp+=writeMediaSection(transceiver,commonCapabilities,"answer",transceiver.stream,pc._dtlsRole);if(transceiver.rtcpParameters&&transceiver.rtcpParameters.reducedSize){sdp+="a=rtcp-rsize\r\n"}});var desc=new window.RTCSessionDescription({type:"answer",sdp:sdp});return Promise.resolve(desc)};RTCPeerConnection.prototype.addIceCandidate=function(candidate){var pc=this;var sections;if(candidate&&!(candidate.sdpMLineIndex!==undefined||candidate.sdpMid)){return Promise.reject(new TypeError("sdpMLineIndex or sdpMid required"))}return new Promise(function(resolve,reject){if(!pc.remoteDescription){return reject(makeError("InvalidStateError","Can not add ICE candidate without a remote description"))}else if(!candidate||candidate.candidate===""){for(var j=0;j0?SDPUtils.parseCandidate(candidate.candidate):{};if(cand.protocol==="tcp"&&(cand.port===0||cand.port===9)){return resolve()}if(cand.component&&cand.component!==1){return resolve()}if(sdpMLineIndex===0||sdpMLineIndex>0&&transceiver.iceTransport!==pc.transceivers[0].iceTransport){if(!maybeAddCandidate(transceiver.iceTransport,cand)){return reject(makeError("OperationError","Can not add ICE candidate"))}}var candidateString=candidate.candidate.trim();if(candidateString.indexOf("a=")===0){candidateString=candidateString.substr(2)}sections=SDPUtils.getMediaSections(pc.remoteDescription.sdp);sections[sdpMLineIndex]+="a="+(cand.type?candidateString:"end-of-candidates")+"\r\n";pc.remoteDescription.sdp=sections.join("")}else{return reject(makeError("OperationError","Can not add ICE candidate"))}}resolve()})};RTCPeerConnection.prototype.getStats=function(){var promises=[];this.transceivers.forEach(function(transceiver){["rtpSender","rtpReceiver","iceGatherer","iceTransport","dtlsTransport"].forEach(function(method){if(transceiver[method]){promises.push(transceiver[method].getStats())}})});var fixStatsType=function(stat){return{inboundrtp:"inbound-rtp",outboundrtp:"outbound-rtp",candidatepair:"candidate-pair",localcandidate:"local-candidate",remotecandidate:"remote-candidate"}[stat.type]||stat.type};return new Promise(function(resolve){var results=new Map;Promise.all(promises).then(function(res){res.forEach(function(result){Object.keys(result).forEach(function(id){result[id].type=fixStatsType(result[id]);results.set(id,result[id])})});resolve(results)})})};var methods=["createOffer","createAnswer"];methods.forEach(function(method){var nativeMethod=RTCPeerConnection.prototype[method];RTCPeerConnection.prototype[method]=function(){var args=arguments;if(typeof args[0]==="function"||typeof args[1]==="function"){return nativeMethod.apply(this,[arguments[2]]).then(function(description){if(typeof args[0]==="function"){args[0].apply(null,[description])}},function(error){if(typeof args[1]==="function"){args[1].apply(null,[error])}})}return nativeMethod.apply(this,arguments)}});methods=["setLocalDescription","setRemoteDescription","addIceCandidate"];methods.forEach(function(method){var nativeMethod=RTCPeerConnection.prototype[method];RTCPeerConnection.prototype[method]=function(){var args=arguments;if(typeof args[1]==="function"||typeof args[2]==="function"){return nativeMethod.apply(this,arguments).then(function(){if(typeof args[1]==="function"){args[1].apply(null)}},function(error){if(typeof args[2]==="function"){args[2].apply(null,[error])}})}return nativeMethod.apply(this,arguments)}});["getStats"].forEach(function(method){var nativeMethod=RTCPeerConnection.prototype[method];RTCPeerConnection.prototype[method]=function(){var args=arguments;if(typeof args[1]==="function"){return nativeMethod.apply(this,arguments).then(function(){if(typeof args[1]==="function"){args[1].apply(null)}})}return nativeMethod.apply(this,arguments)}});return RTCPeerConnection}},{sdp:62}],62:[function(require,module,exports){"use strict";var SDPUtils={};SDPUtils.generateIdentifier=function(){return Math.random().toString(36).substr(2,10)};SDPUtils.localCName=SDPUtils.generateIdentifier();SDPUtils.splitLines=function(blob){return blob.trim().split("\n").map(function(line){return line.trim()})};SDPUtils.splitSections=function(blob){var parts=blob.split("\nm=");return parts.map(function(part,index){return(index>0?"m="+part:part).trim()+"\r\n"})};SDPUtils.getDescription=function(blob){var sections=SDPUtils.splitSections(blob);return sections&§ions[0]};SDPUtils.getMediaSections=function(blob){var sections=SDPUtils.splitSections(blob);sections.shift();return sections};SDPUtils.matchPrefix=function(blob,prefix){return SDPUtils.splitLines(blob).filter(function(line){return line.indexOf(prefix)===0})};SDPUtils.parseCandidate=function(line){var parts;if(line.indexOf("a=candidate:")===0){parts=line.substring(12).split(" ")}else{parts=line.substring(10).split(" ")}var candidate={foundation:parts[0],component:parseInt(parts[1],10),protocol:parts[2].toLowerCase(),priority:parseInt(parts[3],10),ip:parts[4],address:parts[4],port:parseInt(parts[5],10),type:parts[7]};for(var i=8;i0?parts[0].split("/")[1]:"sendrecv",uri:parts[1]}};SDPUtils.writeExtmap=function(headerExtension){return"a=extmap:"+(headerExtension.id||headerExtension.preferredId)+(headerExtension.direction&&headerExtension.direction!=="sendrecv"?"/"+headerExtension.direction:"")+" "+headerExtension.uri+"\r\n"};SDPUtils.parseFmtp=function(line){var parsed={};var kv;var parts=line.substr(line.indexOf(" ")+1).split(";");for(var j=0;j-1){parts.attribute=line.substr(sp+1,colon-sp-1);parts.value=line.substr(colon+1)}else{parts.attribute=line.substr(sp+1)}return parts};SDPUtils.parseSsrcGroup=function(line){var parts=line.substr(13).split(" ");return{semantics:parts.shift(),ssrcs:parts.map(function(ssrc){return parseInt(ssrc,10)})}};SDPUtils.getMid=function(mediaSection){var mid=SDPUtils.matchPrefix(mediaSection,"a=mid:")[0];if(mid){return mid.substr(6)}};SDPUtils.parseFingerprint=function(line){var parts=line.substr(14).split(" ");return{algorithm:parts[0].toLowerCase(),value:parts[1]}};SDPUtils.getDtlsParameters=function(mediaSection,sessionpart){var lines=SDPUtils.matchPrefix(mediaSection+sessionpart,"a=fingerprint:");return{role:"auto",fingerprints:lines.map(SDPUtils.parseFingerprint)}};SDPUtils.writeDtlsParameters=function(params,setupType){var sdp="a=setup:"+setupType+"\r\n";params.fingerprints.forEach(function(fp){sdp+="a=fingerprint:"+fp.algorithm+" "+fp.value+"\r\n"});return sdp};SDPUtils.parseCryptoLine=function(line){var parts=line.substr(9).split(" ");return{tag:parseInt(parts[0],10),cryptoSuite:parts[1],keyParams:parts[2],sessionParams:parts.slice(3)}};SDPUtils.writeCryptoLine=function(parameters){return"a=crypto:"+parameters.tag+" "+parameters.cryptoSuite+" "+(typeof parameters.keyParams==="object"?SDPUtils.writeCryptoKeyParams(parameters.keyParams):parameters.keyParams)+(parameters.sessionParams?" "+parameters.sessionParams.join(" "):"")+"\r\n"};SDPUtils.parseCryptoKeyParams=function(keyParams){if(keyParams.indexOf("inline:")!==0){return null}var parts=keyParams.substr(7).split("|");return{keyMethod:"inline",keySalt:parts[0],lifeTime:parts[1],mkiValue:parts[2]?parts[2].split(":")[0]:undefined,mkiLength:parts[2]?parts[2].split(":")[1]:undefined}};SDPUtils.writeCryptoKeyParams=function(keyParams){return keyParams.keyMethod+":"+keyParams.keySalt+(keyParams.lifeTime?"|"+keyParams.lifeTime:"")+(keyParams.mkiValue&&keyParams.mkiLength?"|"+keyParams.mkiValue+":"+keyParams.mkiLength:"")};SDPUtils.getCryptoParameters=function(mediaSection,sessionpart){var lines=SDPUtils.matchPrefix(mediaSection+sessionpart,"a=crypto:");return lines.map(SDPUtils.parseCryptoLine)};SDPUtils.getIceParameters=function(mediaSection,sessionpart){var ufrag=SDPUtils.matchPrefix(mediaSection+sessionpart,"a=ice-ufrag:")[0];var pwd=SDPUtils.matchPrefix(mediaSection+sessionpart,"a=ice-pwd:")[0];if(!(ufrag&&pwd)){return null}return{usernameFragment:ufrag.substr(12),password:pwd.substr(10)}};SDPUtils.writeIceParameters=function(params){return"a=ice-ufrag:"+params.usernameFragment+"\r\n"+"a=ice-pwd:"+params.password+"\r\n"};SDPUtils.parseRtpParameters=function(mediaSection){var description={codecs:[],headerExtensions:[],fecMechanisms:[],rtcp:[]};var lines=SDPUtils.splitLines(mediaSection);var mline=lines[0].split(" ");for(var i=3;i0?"9":"0";sdp+=" UDP/TLS/RTP/SAVPF ";sdp+=caps.codecs.map(function(codec){if(codec.preferredPayloadType!==undefined){return codec.preferredPayloadType}return codec.payloadType}).join(" ")+"\r\n";sdp+="c=IN IP4 0.0.0.0\r\n";sdp+="a=rtcp:9 IN IP4 0.0.0.0\r\n";caps.codecs.forEach(function(codec){sdp+=SDPUtils.writeRtpMap(codec);sdp+=SDPUtils.writeFmtp(codec);sdp+=SDPUtils.writeRtcpFb(codec)});var maxptime=0;caps.codecs.forEach(function(codec){if(codec.maxptime>maxptime){maxptime=codec.maxptime}});if(maxptime>0){sdp+="a=maxptime:"+maxptime+"\r\n"}sdp+="a=rtcp-mux\r\n";if(caps.headerExtensions){caps.headerExtensions.forEach(function(extension){sdp+=SDPUtils.writeExtmap(extension)})}return sdp};SDPUtils.parseRtpEncodingParameters=function(mediaSection){var encodingParameters=[];var description=SDPUtils.parseRtpParameters(mediaSection);var hasRed=description.fecMechanisms.indexOf("RED")!==-1;var hasUlpfec=description.fecMechanisms.indexOf("ULPFEC")!==-1;var ssrcs=SDPUtils.matchPrefix(mediaSection,"a=ssrc:").map(function(line){return SDPUtils.parseSsrcMedia(line)}).filter(function(parts){return parts.attribute==="cname"});var primarySsrc=ssrcs.length>0&&ssrcs[0].ssrc;var secondarySsrc;var flows=SDPUtils.matchPrefix(mediaSection,"a=ssrc-group:FID").map(function(line){var parts=line.substr(17).split(" ");return parts.map(function(part){return parseInt(part,10)})});if(flows.length>0&&flows[0].length>1&&flows[0][0]===primarySsrc){secondarySsrc=flows[0][1]}description.codecs.forEach(function(codec){if(codec.name.toUpperCase()==="RTX"&&codec.parameters.apt){var encParam={ssrc:primarySsrc,codecPayloadType:parseInt(codec.parameters.apt,10)};if(primarySsrc&&secondarySsrc){encParam.rtx={ssrc:secondarySsrc}}encodingParameters.push(encParam);if(hasRed){encParam=JSON.parse(JSON.stringify(encParam));encParam.fec={ssrc:primarySsrc,mechanism:hasUlpfec?"red+ulpfec":"red"};encodingParameters.push(encParam)}}});if(encodingParameters.length===0&&primarySsrc){encodingParameters.push({ssrc:primarySsrc})}var bandwidth=SDPUtils.matchPrefix(mediaSection,"b=");if(bandwidth.length){if(bandwidth[0].indexOf("b=TIAS:")===0){bandwidth=parseInt(bandwidth[0].substr(7),10)}else if(bandwidth[0].indexOf("b=AS:")===0){bandwidth=parseInt(bandwidth[0].substr(5),10)*1e3*.95-50*40*8}else{bandwidth=undefined}encodingParameters.forEach(function(params){params.maxBitrate=bandwidth})}return encodingParameters};SDPUtils.parseRtcpParameters=function(mediaSection){var rtcpParameters={};var remoteSsrc=SDPUtils.matchPrefix(mediaSection,"a=ssrc:").map(function(line){return SDPUtils.parseSsrcMedia(line)}).filter(function(obj){return obj.attribute==="cname"})[0];if(remoteSsrc){rtcpParameters.cname=remoteSsrc.value;rtcpParameters.ssrc=remoteSsrc.ssrc}var rsize=SDPUtils.matchPrefix(mediaSection,"a=rtcp-rsize");rtcpParameters.reducedSize=rsize.length>0;rtcpParameters.compound=rsize.length===0;var mux=SDPUtils.matchPrefix(mediaSection,"a=rtcp-mux");rtcpParameters.mux=mux.length>0;return rtcpParameters};SDPUtils.parseMsid=function(mediaSection){var parts;var spec=SDPUtils.matchPrefix(mediaSection,"a=msid:");if(spec.length===1){parts=spec[0].substr(7).split(" ");return{stream:parts[0],track:parts[1]}}var planB=SDPUtils.matchPrefix(mediaSection,"a=ssrc:").map(function(line){return SDPUtils.parseSsrcMedia(line)}).filter(function(msidParts){return msidParts.attribute==="msid"});if(planB.length>0){parts=planB[0].value.split(" ");return{stream:parts[0],track:parts[1]}}};SDPUtils.parseSctpDescription=function(mediaSection){var mline=SDPUtils.parseMLine(mediaSection);var maxSizeLine=SDPUtils.matchPrefix(mediaSection,"a=max-message-size:");var maxMessageSize;if(maxSizeLine.length>0){maxMessageSize=parseInt(maxSizeLine[0].substr(19),10)}if(isNaN(maxMessageSize)){maxMessageSize=65536}var sctpPort=SDPUtils.matchPrefix(mediaSection,"a=sctp-port:");if(sctpPort.length>0){return{port:parseInt(sctpPort[0].substr(12),10),protocol:mline.fmt,maxMessageSize:maxMessageSize}}var sctpMapLines=SDPUtils.matchPrefix(mediaSection,"a=sctpmap:");if(sctpMapLines.length>0){var parts=SDPUtils.matchPrefix(mediaSection,"a=sctpmap:")[0].substr(10).split(" ");return{port:parseInt(parts[0],10),protocol:parts[1],maxMessageSize:maxMessageSize}}};SDPUtils.writeSctpDescription=function(media,sctp){var output=[];if(media.protocol!=="DTLS/SCTP"){output=["m="+media.kind+" 9 "+media.protocol+" "+sctp.protocol+"\r\n","c=IN IP4 0.0.0.0\r\n","a=sctp-port:"+sctp.port+"\r\n"]}else{output=["m="+media.kind+" 9 "+media.protocol+" "+sctp.port+"\r\n","c=IN IP4 0.0.0.0\r\n","a=sctpmap:"+sctp.port+" "+sctp.protocol+" 65535\r\n"]}if(sctp.maxMessageSize!==undefined){output.push("a=max-message-size:"+sctp.maxMessageSize+"\r\n")}return output.join("")};SDPUtils.generateSessionId=function(){return Math.random().toString().substr(2,21)};SDPUtils.writeSessionBoilerplate=function(sessId,sessVer,sessUser){var sessionId;var version=sessVer!==undefined?sessVer:2;if(sessId){sessionId=sessId}else{sessionId=SDPUtils.generateSessionId()}var user=sessUser||"thisisadapterortc";return"v=0\r\n"+"o="+user+" "+sessionId+" "+version+" IN IP4 127.0.0.1\r\n"+"s=-\r\n"+"t=0 0\r\n"};SDPUtils.writeMediaSection=function(transceiver,caps,type,stream){var sdp=SDPUtils.writeRtpDescription(transceiver.kind,caps);sdp+=SDPUtils.writeIceParameters(transceiver.iceGatherer.getLocalParameters());sdp+=SDPUtils.writeDtlsParameters(transceiver.dtlsTransport.getLocalParameters(),type==="offer"?"actpass":"active");sdp+="a=mid:"+transceiver.mid+"\r\n";if(transceiver.direction){sdp+="a="+transceiver.direction+"\r\n"}else if(transceiver.rtpSender&&transceiver.rtpReceiver){sdp+="a=sendrecv\r\n"}else if(transceiver.rtpSender){sdp+="a=sendonly\r\n"}else if(transceiver.rtpReceiver){sdp+="a=recvonly\r\n"}else{sdp+="a=inactive\r\n"}if(transceiver.rtpSender){var msid="msid:"+stream.id+" "+transceiver.rtpSender.track.id+"\r\n";sdp+="a="+msid;sdp+="a=ssrc:"+transceiver.sendEncodingParameters[0].ssrc+" "+msid;if(transceiver.sendEncodingParameters[0].rtx){sdp+="a=ssrc:"+transceiver.sendEncodingParameters[0].rtx.ssrc+" "+msid;sdp+="a=ssrc-group:FID "+transceiver.sendEncodingParameters[0].ssrc+" "+transceiver.sendEncodingParameters[0].rtx.ssrc+"\r\n"}}sdp+="a=ssrc:"+transceiver.sendEncodingParameters[0].ssrc+" cname:"+SDPUtils.localCName+"\r\n";if(transceiver.rtpSender&&transceiver.sendEncodingParameters[0].rtx){sdp+="a=ssrc:"+transceiver.sendEncodingParameters[0].rtx.ssrc+" cname:"+SDPUtils.localCName+"\r\n"}return sdp};SDPUtils.getDirection=function(mediaSection,sessionpart){var lines=SDPUtils.splitLines(mediaSection);for(var i=0;i=len)return x;switch(x){case"%s":return String(args[i++]);case"%d":return Number(args[i++]);case"%j":try{return JSON.stringify(args[i++])}catch(_){return"[Circular]"}default:return x}});for(var x=args[i];i=3)ctx.depth=arguments[2];if(arguments.length>=4)ctx.colors=arguments[3];if(isBoolean(opts)){ctx.showHidden=opts}else if(opts){exports._extend(ctx,opts)}if(isUndefined(ctx.showHidden))ctx.showHidden=false;if(isUndefined(ctx.depth))ctx.depth=2;if(isUndefined(ctx.colors))ctx.colors=false;if(isUndefined(ctx.customInspect))ctx.customInspect=true;if(ctx.colors)ctx.stylize=stylizeWithColor;return formatValue(ctx,obj,ctx.depth)}exports.inspect=inspect;inspect.colors={bold:[1,22],italic:[3,23],underline:[4,24],inverse:[7,27],white:[37,39],grey:[90,39],black:[30,39],blue:[34,39],cyan:[36,39],green:[32,39],magenta:[35,39],red:[31,39],yellow:[33,39]};inspect.styles={special:"cyan",number:"yellow",boolean:"yellow",undefined:"grey",null:"bold",string:"green",date:"magenta",regexp:"red"};function stylizeWithColor(str,styleType){var style=inspect.styles[styleType];if(style){return"\x1b["+inspect.colors[style][0]+"m"+str+"\x1b["+inspect.colors[style][1]+"m"}else{return str}}function stylizeNoColor(str,styleType){return str}function arrayToHash(array){var hash={};array.forEach(function(val,idx){hash[val]=true});return hash}function formatValue(ctx,value,recurseTimes){if(ctx.customInspect&&value&&isFunction(value.inspect)&&value.inspect!==exports.inspect&&!(value.constructor&&value.constructor.prototype===value)){var ret=value.inspect(recurseTimes,ctx);if(!isString(ret)){ret=formatValue(ctx,ret,recurseTimes)}return ret}var primitive=formatPrimitive(ctx,value);if(primitive){return primitive}var keys=Object.keys(value);var visibleKeys=arrayToHash(keys);if(ctx.showHidden){keys=Object.getOwnPropertyNames(value)}if(isError(value)&&(keys.indexOf("message")>=0||keys.indexOf("description")>=0)){return formatError(value)}if(keys.length===0){if(isFunction(value)){var name=value.name?": "+value.name:"";return ctx.stylize("[Function"+name+"]","special")}if(isRegExp(value)){return ctx.stylize(RegExp.prototype.toString.call(value),"regexp")}if(isDate(value)){return ctx.stylize(Date.prototype.toString.call(value),"date")}if(isError(value)){return formatError(value)}}var base="",array=false,braces=["{","}"];if(isArray(value)){array=true;braces=["[","]"]}if(isFunction(value)){var n=value.name?": "+value.name:"";base=" [Function"+n+"]"}if(isRegExp(value)){base=" "+RegExp.prototype.toString.call(value)}if(isDate(value)){base=" "+Date.prototype.toUTCString.call(value)}if(isError(value)){base=" "+formatError(value)}if(keys.length===0&&(!array||value.length==0)){return braces[0]+base+braces[1]}if(recurseTimes<0){if(isRegExp(value)){return ctx.stylize(RegExp.prototype.toString.call(value),"regexp")}else{return ctx.stylize("[Object]","special")}}ctx.seen.push(value);var output;if(array){output=formatArray(ctx,value,recurseTimes,visibleKeys,keys)}else{output=keys.map(function(key){return formatProperty(ctx,value,recurseTimes,visibleKeys,key,array)})}ctx.seen.pop();return reduceToSingleString(output,base,braces)}function formatPrimitive(ctx,value){if(isUndefined(value))return ctx.stylize("undefined","undefined");if(isString(value)){var simple="'"+JSON.stringify(value).replace(/^"|"$/g,"").replace(/'/g,"\\'").replace(/\\"/g,'"')+"'";return ctx.stylize(simple,"string")}if(isNumber(value))return ctx.stylize(""+value,"number");if(isBoolean(value))return ctx.stylize(""+value,"boolean");if(isNull(value))return ctx.stylize("null","null")}function formatError(value){return"["+Error.prototype.toString.call(value)+"]"}function formatArray(ctx,value,recurseTimes,visibleKeys,keys){var output=[];for(var i=0,l=value.length;i-1){if(array){str=str.split("\n").map(function(line){return" "+line}).join("\n").substr(2)}else{str="\n"+str.split("\n").map(function(line){return" "+line}).join("\n")}}}else{str=ctx.stylize("[Circular]","special")}}if(isUndefined(name)){if(array&&key.match(/^\d+$/)){return str}name=JSON.stringify(""+key);if(name.match(/^"([a-zA-Z_][a-zA-Z_0-9]*)"$/)){name=name.substr(1,name.length-2);name=ctx.stylize(name,"name")}else{name=name.replace(/'/g,"\\'").replace(/\\"/g,'"').replace(/(^"|"$)/g,"'");name=ctx.stylize(name,"string")}}return name+": "+str}function reduceToSingleString(output,base,braces){var numLinesEst=0;var length=output.reduce(function(prev,cur){numLinesEst++;if(cur.indexOf("\n")>=0)numLinesEst++;return prev+cur.replace(/\u001b\[\d\d?m/g,"").length+1},0);if(length>60){return braces[0]+(base===""?"":base+"\n ")+" "+output.join(",\n ")+" "+braces[1]}return braces[0]+base+" "+output.join(", ")+" "+braces[1]}function isArray(ar){return Array.isArray(ar)}exports.isArray=isArray;function isBoolean(arg){return typeof arg==="boolean"}exports.isBoolean=isBoolean;function isNull(arg){return arg===null}exports.isNull=isNull;function isNullOrUndefined(arg){return arg==null}exports.isNullOrUndefined=isNullOrUndefined;function isNumber(arg){return typeof arg==="number"}exports.isNumber=isNumber;function isString(arg){return typeof arg==="string"}exports.isString=isString;function isSymbol(arg){return typeof arg==="symbol"}exports.isSymbol=isSymbol;function isUndefined(arg){return arg===void 0}exports.isUndefined=isUndefined;function isRegExp(re){return isObject(re)&&objectToString(re)==="[object RegExp]"}exports.isRegExp=isRegExp;function isObject(arg){return typeof arg==="object"&&arg!==null}exports.isObject=isObject;function isDate(d){return isObject(d)&&objectToString(d)==="[object Date]"}exports.isDate=isDate;function isError(e){return isObject(e)&&(objectToString(e)==="[object Error]"||e instanceof Error)}exports.isError=isError;function isFunction(arg){return typeof arg==="function"}exports.isFunction=isFunction;function isPrimitive(arg){return arg===null||typeof arg==="boolean"||typeof arg==="number"||typeof arg==="string"||typeof arg==="symbol"||typeof arg==="undefined"}exports.isPrimitive=isPrimitive;exports.isBuffer=require("./support/isBuffer");function objectToString(o){return Object.prototype.toString.call(o)}function pad(n){return n<10?"0"+n.toString(10):n.toString(10)}var months=["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"];function timestamp(){var d=new Date;var time=[pad(d.getHours()),pad(d.getMinutes()),pad(d.getSeconds())].join(":");return[d.getDate(),months[d.getMonth()],time].join(" ")}exports.log=function(){console.log("%s - %s",timestamp(),exports.format.apply(exports,arguments))};exports.inherits=require("inherits");exports._extend=function(origin,add){if(!add||!isObject(add))return origin;var keys=Object.keys(add);var i=keys.length;while(i--){origin[keys[i]]=add[keys[i]]}return origin};function hasOwnProperty(obj,prop){return Object.prototype.hasOwnProperty.call(obj,prop)}}).call(this)}).call(this,require("_process"),typeof global!=="undefined"?global:typeof self!=="undefined"?self:typeof window!=="undefined"?window:{})},{"./support/isBuffer":64,_process:60,inherits:63}]},{},[3]);var Voice=bundle(3);if(typeof define==="function"&&define.amd){define([],function(){return Voice})}else{var Twilio=root.Twilio=root.Twilio||{};Twilio.Call=Twilio.Call||Voice.Call;Twilio.Device=Twilio.Device||Voice.Device;Twilio.PStream=Twilio.PStream||Voice.PStream;Twilio.PreflightTest=Twilio.PreflightTest||Voice.PreflightTest;Twilio.Logger=Twilio.Logger||Voice.Logger}})(typeof window!=="undefined"?window:typeof global!=="undefined"?global:this); \ No newline at end of file diff --git a/scripts/inbound-call.js b/scripts/inbound-call.js deleted file mode 100644 index a61b9e4d..00000000 --- a/scripts/inbound-call.js +++ /dev/null @@ -1,30 +0,0 @@ -require('dotenv').config(); - -// You can use this function to make a -// test call to your application by running -// npm inbound -async function makeInboundCall() { - const VoiceResponse = require('twilio').twiml.VoiceResponse; - const accountSid = process.env.TWILIO_ACCOUNT_SID; - const authToken = process.env.TWILIO_AUTH_TOKEN; - - const client = require('twilio')(accountSid, authToken); - - let twiml = new VoiceResponse(); - twiml.pause({ length: 10 }); - twiml.say('Which models of airpods do you have available right now?'); - twiml.pause({ length: 30 }); - twiml.hangup(); - - console.log(twiml.toString()); - - await client.calls - .create({ - twiml: twiml.toString(), - to: process.env.APP_NUMBER, - from: process.env.FROM_NUMBER - }) - .then(call => console.log(call.sid)); -} - -makeInboundCall(); \ No newline at end of file diff --git a/scripts/outbound-call.js b/scripts/outbound-call.js deleted file mode 100644 index 7f7c2d55..00000000 --- a/scripts/outbound-call.js +++ /dev/null @@ -1,23 +0,0 @@ -/* - You can use this script to place an outbound call - to your own mobile phone. -*/ - -require('dotenv').config(); - -async function makeOutBoundCall() { - const accountSid = process.env.TWILIO_ACCOUNT_SID; - const authToken = process.env.TWILIO_AUTH_TOKEN; - - const client = require('twilio')(accountSid, authToken); - - await client.calls - .create({ - url: `https://${process.env.SERVER}/incoming`, - to: process.env.YOUR_NUMBER, - from: process.env.FROM_NUMBER - }) - .then(call => console.log(call.sid)); -} - -makeOutBoundCall(); \ No newline at end of file diff --git a/services/gpt-service-mixed-mode.js b/services/gpt-service-mixed-mode.js deleted file mode 100644 index d0d0d50a..00000000 --- a/services/gpt-service-mixed-mode.js +++ /dev/null @@ -1,250 +0,0 @@ -const OpenAI = require("openai"); // or the appropriate module import -const EventEmitter = require("events"); -const availableFunctions = require("../functions/available-functions"); -const tools = require("../functions/function-manifest"); -let prompt = require("../prompts/prompt"); -const welcomePrompt = require("../prompts/welcomePrompt"); -const model = "gpt-4o"; - -const currentDate = new Date().toLocaleDateString("en-US", { - weekday: "long", - year: "numeric", - month: "long", - day: "numeric", -}); - -prompt = prompt.replace("{{currentDate}}", currentDate); - -class GptService extends EventEmitter { - constructor() { - super(); - this.openai = new OpenAI(); - this.userContext = [ - { role: "system", content: prompt }, - { - role: "assistant", - content: `${welcomePrompt}`, - }, - ]; - - this.isInterrupted = false; - } - - log(message) { - const timestamp = new Date().toISOString(); - console.log(`[${timestamp}] ${message}`); - } - - setCallSid(callSid) { - this.userContext.push({ role: "system", content: `callSid: ${callSid}` }); - } - - interrupt() { - this.isInterrupted = true; - } - - updateUserContext(role, text) { - this.userContext.push({ role: role, content: text }); - } - - async completion(text, interactionCount, role = "user") { - if (!text || typeof text !== "string") { - this.log(`[GptService] Invalid prompt received: ${text}`); - return; - } - - this.isInterrupted = false; - this.updateUserContext(role, text); - - let completeResponse = ""; - let detectedToolCall = null; - - try { - // Start with streaming enabled - const responseStream = await this.openai.chat.completions.create({ - model: model, - messages: this.userContext, - tools: tools, // Ensure this aligns with your tool definitions - stream: true, - }); - - for await (const chunk of responseStream) { - if (this.isInterrupted) { - break; - } - - const content = chunk.choices[0]?.delta?.content || ""; - completeResponse += content; - - // Check if a tool call is detected - const toolCalls = chunk.choices[0]?.delta?.tool_calls; - if (toolCalls && toolCalls[0]) { - this.log( - `[GptService] Tool call detected: ${toolCalls[0].function.name}` - ); - detectedToolCall = toolCalls[0]; // Store the tool call - break; // Exit the loop to process the tool call - } - - // Emit the chunk as soon as we know it's not a tool call - this.emit("gptreply", content, false, interactionCount); - - // Check if the current chunk is the last one in the stream - if (chunk.choices[0].finish_reason === "stop") { - // Push the final response to userContext - this.userContext.push({ - role: "assistant", - content: completeResponse, - }); - this.emit("gptreply", content, true, interactionCount); - this.log( - `[GptService] Final GPT -> user context length: ${this.userContext.length}` - ); - break; // Exit the loop since the response is complete - } - } - - // If a tool call was detected, handle it with a non-streaming API call - if (detectedToolCall) { - // Make a non-streaming API call to handle the tool response - const response = await this.openai.chat.completions.create({ - model: model, - messages: this.userContext, - tools: tools, - stream: false, // Disable streaming to process the tool response - }); - - const toolCall = response.choices[0]?.message?.tool_calls; - // If no tool call is detected after the non-streaming API call - if (!toolCall || !toolCall[0]) { - this.log( - "[GptService] No tool call detected after non-streaming API call" - ); - // Log the message content that would have been sent back to the user - this.log( - `[GptService] NON-TOOL-BASED Message content: ${ - response.choices[0]?.message?.content || "No content available" - }` - ); - - // Emit the non-tool response to the user - this.emit( - "gptreply", - response.choices[0]?.message?.content || - "I apologize, can you repeat that again just so I'm clear?", - true, - interactionCount - ); - - return; - } - - const functionName = toolCall[0].function.name; - const functionArgs = JSON.parse(toolCall[0].function.arguments); - - // // Emit the TTS message related to the tool call - // const ttsMessage = this.getTtsMessageForTool(functionName); - // this.emit("gptreply", ttsMessage, true, interactionCount); - - const functionToCall = availableFunctions[functionName]; - if (!functionToCall) { - this.log(`[GptService] Function ${functionName} is not available.`); - this.emit( - "gptreply", - "I'm unable to complete that action.", - true, - interactionCount - ); - return; - } - - this.log( - `[GptService] Calling function ${functionName} with arguments: ${JSON.stringify( - functionArgs - )}` - ); - const functionResponse = await functionToCall(functionArgs); - - let function_call_result_message; - if (functionResponse.status === "success") { - function_call_result_message = { - role: "tool", - content: JSON.stringify(functionResponse.data), - tool_call_id: response.choices[0].message.tool_calls[0].id, - }; - } else { - function_call_result_message = { - role: "tool", - content: JSON.stringify({ message: functionResponse.message }), - tool_call_id: response.choices[0].message.tool_calls[0].id, - }; - } - - // Prepare the chat completion call payload with the tool result - const completion_payload = { - model: model, - messages: [ - ...this.userContext, - { - role: "system", - content: - "Please ensure that the response is summarized, concise, and does not include any formatting characters like asterisks (*) in the output.", - }, - response.choices[0].message, // the tool_call message - function_call_result_message, - ], - }; - - // Call the API again with streaming enabled to process the tool response - const finalResponseStream = await this.openai.chat.completions.create({ - model: completion_payload.model, - messages: completion_payload.messages, - stream: true, // Enable streaming for the final response - }); - - let finalResponse = ""; - - for await (const chunk of finalResponseStream) { - const content = chunk.choices[0]?.delta?.content || ""; - finalResponse += content; - - // Emit each chunk as it comes in - this.emit("gptreply", content, false, interactionCount); - - // Check if the current chunk is the last one in the stream - if (chunk.choices[0].finish_reason === "stop") { - // Push the final response to userContext - this.userContext.push({ - role: "assistant", - content: finalResponse, - }); - this.emit("gptreply", content, true, interactionCount); - this.log( - `[GptService] Final GPT -> user context length: ${this.userContext.length}` - ); - break; // Exit the loop since the response is complete - } - } - } - } catch (error) { - this.log(`Error during completion: ${error.message}`); - } - } - getTtsMessageForTool(toolName) { - switch (toolName) { - case "listAvailableApartments": - return "Let me check on the available apartments for you."; - case "checkExistingAppointments": - return "I'll look up your existing appointments."; - case "scheduleTour": - return "I'll go ahead and schedule that tour for you."; - case "checkAvailability": - return "Let me verify the availability for the requested time."; - case "commonInquiries": - return "I'm gathering the information you asked for. Just a moment."; - default: - return "Give me a moment while I fetch the information."; - } - } -} -module.exports = { GptService }; diff --git a/services/recording-service.js b/services/recording-service.js deleted file mode 100644 index 9e49c2cf..00000000 --- a/services/recording-service.js +++ /dev/null @@ -1,23 +0,0 @@ - -require('colors'); - -async function recordingService(textService, callSid) { - try { - if (process.env.RECORDING_ENABLED === 'true') { - const client = require('twilio')(process.env.TWILIO_ACCOUNT_SID, process.env.TWILIO_AUTH_TOKEN); - - textService.sendText({partialResponseIndex: null, partialResponse: 'This call will be recorded.'}, 0); - const recording = await client.calls(callSid) - .recordings - .create({ - recordingChannels: 'dual' - }); - - console.log(`Recording Created: ${recording.sid}`.red); - } - } catch (err) { - console.log(err); - } -} - -module.exports = { recordingService }; \ No newline at end of file diff --git a/services/token-generator.js b/services/token-generator.js deleted file mode 100644 index 869f83c3..00000000 --- a/services/token-generator.js +++ /dev/null @@ -1,72 +0,0 @@ -const VoiceResponse = require("twilio").twiml.VoiceResponse; -const AccessToken = require("twilio").jwt.AccessToken; -const VoiceGrant = AccessToken.VoiceGrant; - -const nameGenerator = require("../name_generator"); -const config = require("../config"); - -var identity; - -exports.tokenGenerator = function tokenGenerator() { - identity = nameGenerator(); - - console.log(identity); - const accessToken = new AccessToken( - config.accountSid, - config.apiKey, - config.apiSecret, - { identity: identity } - ); - accessToken.identity = identity; - const grant = new VoiceGrant({ - outgoingApplicationSid: config.twimlAppSid, - incomingAllow: true, - }); - accessToken.addGrant(grant); - - // Include identity and token in a JSON response - return { - identity: identity, - token: accessToken.toJwt(), - }; -}; - -exports.voiceResponse = function voiceResponse(requestBody) { - const toNumberOrClientName = requestBody.To; - const callerId = config.callerId; - let twiml = new VoiceResponse(); - - // If the request to the /voice endpoint is TO your Twilio Number, - // then it is an incoming call towards your Twilio.Device. - if (toNumberOrClientName == callerId) { - let dial = twiml.dial(); - - // This will connect the caller with your Twilio.Device/client - dial.client(identity); - } else if (requestBody.To) { - // This is an outgoing call - - // set the callerId - let dial = twiml.dial({ callerId }); - - // Check if the 'To' parameter is a Phone Number or Client Name - // in order to use the appropriate TwiML noun - const attr = isAValidPhoneNumber(toNumberOrClientName) - ? "number" - : "client"; - dial[attr]({}, toNumberOrClientName); - } else { - twiml.say("Thanks for calling!"); - } - - return twiml.toString(); -}; - -/** - * Checks if the given value is valid as phone number - * @param {Number|String} number - * @return {Boolean} - */ -function isAValidPhoneNumber(number) { - return /^[\d+\-() ]+$/.test(number); -} diff --git a/test/checkInventory.test.js b/test/checkInventory.test.js deleted file mode 100644 index e65026ca..00000000 --- a/test/checkInventory.test.js +++ /dev/null @@ -1,13 +0,0 @@ -const checkInventory = require('../functions/checkInventory'); - -test('Expect Airpods Pro to have 10 units', () => { - expect(checkInventory({model: 'airpods pro'})).toBe('{"stock":10}'); -}); - -test('Expect Airpods Max to have 0 units', () => { - expect(checkInventory({model: 'airpods max'})).toBe('{"stock":0}'); -}); - -test('Expect all other values to have 100 units', () => { - expect(checkInventory({model: 'anything'})).toBe('{"stock":100}'); -}); \ No newline at end of file diff --git a/test/checkPrice.test.js b/test/checkPrice.test.js deleted file mode 100644 index e228c0eb..00000000 --- a/test/checkPrice.test.js +++ /dev/null @@ -1,13 +0,0 @@ -const checkPrice = require('../functions/checkPrice'); - -test('Expect Airpods Pro to cost $249', () => { - expect(checkPrice({model: 'airpods pro'})).toBe('{"price":249}'); -}); - -test('Expect Airpods Max to cost $549', () => { - expect(checkPrice({model: 'airpods max'})).toBe('{"price":549}'); -}); - -test('Expect all other models to cost $149', () => { - expect(checkPrice({model: 'anything'})).toBe('{"price":149}'); -}); \ No newline at end of file diff --git a/test/placeOrder.test.js b/test/placeOrder.test.js deleted file mode 100644 index 4c1b8f3d..00000000 --- a/test/placeOrder.test.js +++ /dev/null @@ -1,8 +0,0 @@ -const placeOrder = require('../functions/placeOrder'); - -test('Expect placeOrder to return an object with a price and order number', () => { - const order = JSON.parse(placeOrder({model: 'airpods pro', quantity: 10})); - - expect(order).toHaveProperty('orderNumber'); - expect(order).toHaveProperty('price'); -}); \ No newline at end of file diff --git a/test/transferCall.test.js b/test/transferCall.test.js deleted file mode 100644 index eaff272c..00000000 --- a/test/transferCall.test.js +++ /dev/null @@ -1,31 +0,0 @@ -require('dotenv').config(); -const setTimeout = require('timers/promises').setTimeout; -const transferCall = require('../functions/transferCall'); - -test('Expect transferCall to successfully redirect call', async () => { - - async function makeOutBoundCall() { - const accountSid = process.env.TWILIO_ACCOUNT_SID; - const authToken = process.env.TWILIO_AUTH_TOKEN; - - const client = require('twilio')(accountSid, authToken); - - const sid = await client.calls - .create({ - url: `https://${process.env.SERVER}/incoming`, - to: process.env.YOUR_NUMBER, - from: process.env.FROM_NUMBER - }) - .then(call => call.sid); - - return sid; - } - - const callSid = await makeOutBoundCall(); - console.log(callSid); - await setTimeout(10000); - - const transferResult = await transferCall(callSid); - - expect(transferResult).toBe('The call was transferred successfully'); -}, 20000); \ No newline at end of file From d395aa0987dab9d735fb822979e767f91d811a1c Mon Sep 17 00:00:00 2001 From: Chris Feehan Date: Tue, 3 Sep 2024 13:30:00 -0500 Subject: [PATCH 13/26] Fixed bugs in streaming code --- services/gpt-service-streaming.js | 226 ++++++++++++++++++++++-------- 1 file changed, 164 insertions(+), 62 deletions(-) diff --git a/services/gpt-service-streaming.js b/services/gpt-service-streaming.js index e10ee877..75a48e88 100644 --- a/services/gpt-service-streaming.js +++ b/services/gpt-service-streaming.js @@ -194,14 +194,12 @@ class GptService extends EventEmitter { refusal: null, }; // Final object to store content and tool call details - let lastContentChunk = ""; // To store the last content chunk received - let contentPending = false; // Flag to track if there's content pending to be emitted let currentToolCallId = null; // To store the ID of the active tool call for await (const chunk of response) { const { choices } = chunk; - // Log each chunk as it comes in + //Log each chunk as it comes in // this.log(`[GptService] Chunk received: ${JSON.stringify(chunk)}`); // Check if tool_calls are present in this chunk (could be part of multiple chunks) @@ -210,17 +208,51 @@ class GptService extends EventEmitter { // Check if this is a new tool call (only when an ID is present) if (toolCall.id && toolCall.id !== currentToolCallId) { + // Check if currentToolCallId is not null, indicating a subsequent tool call + const isFirstToolCall = currentToolCallId === null; + currentToolCallId = toolCall.id; // Initialize new tool call if not already in the map if (!toolCalls[currentToolCallId]) { + // this.log( + // `[GptService] Final Content of this tool call: ${contentAccumulator}` + // ); + + if (choices[0]?.delta?.content) { + // this.log( + // `[GptService] Last chunk to emit: ${choices[0].delta.content}` + // ); + this.emit( + "gptreply", + choices[0]?.delta?.content, + true, + interactionCount + ); + } else { + // this.log( + // `[GptService] Emitting empty string as final content chunk to voxray` + // ); + //If the content is empty (in the case of OpenAI Chat Completions, it will ALWAYS be this) then just send an empty token + this.emit("gptreply", "", true, interactionCount); + } + //Log the last content to an assistant message (IS THIS AN OPENAI BUG??? For some reason, the finish_reason never is "STOP" and we miss the final punctuation (eg. "One Moment" should be "One Moment." where the period is the final content, but that period is being sent back after the function call completes)) - if (contentAccumulator.length > 0) { + if (contentAccumulator.length > 0 && isFirstToolCall) { this.userContext.push({ role: "assistant", content: contentAccumulator.trim(), }); + // // Log the full userContext before making the API call + // console.log( + // `[GptService] Full userContext before next tool call id: ${JSON.stringify( + // this.userContext, + // null, + // 2 + // )}` + // ); + this.log( `[GptService] Final GPT -> user context length: ${this.userContext.length}` ); @@ -367,7 +399,7 @@ class GptService extends EventEmitter { ], }; - // Log the payload to the console + // //Log the payload to the console // console.log( // `[GptService] Completion payload: ${JSON.stringify( // completion_payload, @@ -395,39 +427,70 @@ class GptService extends EventEmitter { // ); // Accumulate the content from each chunk - if (choices[0]?.delta?.content) { - if (contentPending && lastContentChunk) { - this.emit( - "gptreply", - lastContentChunk, - false, - interactionCount - ); - } - - lastContentChunk = choices[0].delta.content; - finalContentAccumulator += lastContentChunk; - contentPending = true; - } + if (!choices[0]?.delta?.tool_calls) { + // Check if the current chunk is the last one in the stream + if (choices[0].finish_reason === "stop") { + this.log(`[GptService] In finish reason === STOP`); + + if (choices[0]?.delta?.content) { + // this.log( + // `[GptService] Last chunk to emit (non-tool call): ${choices[0].delta.content}` + // ); + this.emit( + "gptreply", + choices[0]?.delta?.content, + true, + interactionCount + ); + //accumulate the final content chunk before pushing to user context + finalContentAccumulator += choices[0].delta.content; + } else { + // this.log( + // `[GptService] Emitting empty string as final content chunk (non-tool call) to voxray` + // ); + //If the content is empty (in the case of OpenAI Chat Completions, it will ALWAYS be this) then just send an empty token + this.emit("gptreply", "", true, interactionCount); + } + // if (lastContentChunk) { + // this.emit("gptreply", lastContentChunk, true, interactionCount); + // } - // Handle 'finish_reason' to detect the end of streaming - if (choices[0].finish_reason === "stop") { - // this.log(`[GptService] Final response STOP detected`); + this.userContext.push({ + role: "assistant", + content: finalContentAccumulator.trim(), + }); - if (lastContentChunk) { - this.emit("gptreply", lastContentChunk, true, interactionCount); - } + // Log the full userContext before making the API call + // console.log( + // `[GptService] Full userContext after tool call: ${JSON.stringify( + // this.userContext, + // null, + // 2 + // )}` + // ); - // Push the final accumulated content into userContext - this.userContext.push({ - role: "assistant", - content: finalContentAccumulator.trim(), - }); + this.log( + `[GptService] Final GPT -> user context length: ${this.userContext.length}` + ); - this.log( - `[GptService] Final GPT -> user context length: ${this.userContext.length}` - ); - break; // Exit the loop once the final response is complete + break; // Exit the loop once the final response is complete + } else { + //We only will start emitting chunks after the chunk with ROLE defined "delta":{"role":"assistant","content":"","refusal":null} because there is no content here + if (!choices[0]?.delta?.role && choices[0]?.delta?.content) { + // this.log( + // `[GptService] Emitting intermediary chunk (tool call): ${choices[0].delta.content}` + // ); + //emit the chunk knowing its a content chunk and not the final one + this.emit( + "gptreply", + choices[0].delta.content, + false, + interactionCount + ); + //continue accumulating content chunks + finalContentAccumulator += choices[0].delta.content; + } + } } } // Reset tool call state after completion @@ -439,45 +502,84 @@ class GptService extends EventEmitter { if (choices[0]?.delta?.tool_calls[0]?.function?.arguments) { toolCalls[currentToolCallId].arguments += choices[0].delta.tool_calls[0].function.arguments; - this.log( - `[GptService] Accumulated arguments for tool call ${currentToolCallId}: ${toolCalls[currentToolCallId].arguments}` - ); + // this.log( + // `[GptService] Accumulated arguments for tool call ${currentToolCallId}: ${toolCalls[currentToolCallId].arguments}` + // ); } } } // Handle non-tool_call content chunks - if (choices[0]?.delta?.content) { - if (contentPending && lastContentChunk) { - this.emit("gptreply", lastContentChunk, false, interactionCount); - } - - lastContentChunk = choices[0].delta.content; - contentAccumulator += lastContentChunk; - contentPending = true; - } + if (!choices[0]?.delta?.tool_calls) { + // Check if the current chunk is the last one in the stream + if (choices[0].finish_reason === "stop") { + this.log(`[GptService] In finish reason === STOP`); - if (choices[0]?.delta?.refusal !== null) { - finalMessageObject.refusal = choices[0].delta.refusal; - } + if (choices[0]?.delta?.content) { + // this.log( + // `[GptService] Last chunk to emit (non-tool call): ${choices[0].delta.content}` + // ); + this.emit( + "gptreply", + choices[0]?.delta?.content, + true, + interactionCount + ); + //accumulate the final content chunk before pushing to user context + contentAccumulator += choices[0].delta.content; + } else { + // this.log( + // `[GptService] Emitting empty string as final content chunk (non-tool call) to voxray` + // ); + //If the content is empty (in the case of OpenAI Chat Completions, it will ALWAYS be this) then just send an empty token + this.emit("gptreply", "", true, interactionCount); + } + // if (lastContentChunk) { + // this.emit("gptreply", lastContentChunk, true, interactionCount); + // } - // Check if the current chunk is the last one in the stream - if (choices[0].finish_reason === "stop") { - this.log(`[GptService] In finish reason === STOP`); + this.userContext.push({ + role: "assistant", + content: contentAccumulator.trim(), + }); - if (lastContentChunk) { - this.emit("gptreply", lastContentChunk, true, interactionCount); - } + // // Log the full userContext before making the API call + // console.log( + // `[GptService] Full userContext after non tool call: ${JSON.stringify( + // this.userContext, + // null, + // 2 + // )}` + // ); - this.userContext.push({ - role: "assistant", - content: contentAccumulator.trim(), - }); + this.log( + `[GptService] Final GPT -> user context length: ${this.userContext.length}` + ); - this.log( - `[GptService] Final GPT -> user context length: ${this.userContext.length}` - ); + break; // Exit the loop once the final response is complete + } else { + //We only will start emitting chunks after the chunk with ROLE defined "delta":{"role":"assistant","content":"","refusal":null} because there is no content here, also need to make sure finish reason isn't a tool call + if (!choices[0]?.delta?.role && choices[0]?.delta?.content) { + //Log each chunk as it comes in + // this.log( + // `[GptService] Emitting intermediary chunk (non tool call): ${choices[0].delta.content}` + // ); + //emit the chunk knowing its a content chunk and not the final one + this.emit( + "gptreply", + choices[0].delta.content, + false, + interactionCount + ); + //continue accumulating content chunks + contentAccumulator += choices[0].delta.content; + } + } } + + // if (choices[0]?.delta?.refusal !== null) { + // finalMessageObject.refusal = choices[0].delta.refusal; + // } } } catch (error) { this.log(`[GptService] Error during completion: ${error.stack}`); From e2ebbb92e3d9496d076a5e14a7273afe615a1182 Mon Sep 17 00:00:00 2001 From: Chris Feehan Date: Wed, 9 Oct 2024 09:08:33 -0400 Subject: [PATCH 14/26] Updates to streaming and logging --- .DS_Store | Bin 0 -> 6148 bytes app.js | 337 ++++++----------- data/mock-database.js | 74 ++-- functions/available-functions.js | 34 +- functions/function-manifest.js | 20 +- functions/helper-functions.js | 404 ++++++++++++++++++++ services/gpt-service-non-streaming.js | 30 +- services/gpt-service-streaming.js | 511 +++++++++----------------- services/text-service.js | 8 +- 9 files changed, 797 insertions(+), 621 deletions(-) create mode 100644 .DS_Store create mode 100644 functions/helper-functions.js diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..648168d362b1ed4bc16ea2ec43f115836ab8136c GIT binary patch literal 6148 zcmeHK%}T>S5Z-O8-BN@e6nb3nTCgcpDqcdZFJMFuDm5`dgE3p0v^|tU&iX<=iO=KA z?glLF!IOy2z~-BspWU4gvOkP5KAeRPV=cy*fQHCX*&=A(>Z;gaM6Sn(*#nDM7A&&K zmVthw3D+)I2CJU3Is3ByAW;GM-h)XJXL+~#!7J6;&Td`QMN`~)Pio=iem={F{`3ZW zmr5l;sr$iI94*GJ{WF#3ew;>Aoe+l+gxp@oX{Z)MHA}-x=X%B=8lo|7wU^65uOppa zf7Ovo$LSw;eZ0Rfp;x?7E5>oWuvaSH%$|jK7y~zE8_`? z0b+m{AO`*q1NICM&Hqy}RZ0vH1K(r-_Xi0L(Y06@)LRF9@cNAY8X^kl_?AGF7F~;l zLGXZZlL}~3xqV`AlMZ%i<6Mh{L6gq7o*BlmGnbDSu4f0k)ZvV~2I)%-5CiKBR86;q z=l?nUGFu<{>nYSD28e+_#sII4ywMPfvS;f?d3e@JX!p=iFs?ua1oX8_0CaF4X=|tP cOVlCGwOAO$QP8f_0qG*3384=$@B<8d0=vOY8~^|S literal 0 HcmV?d00001 diff --git a/app.js b/app.js index 4f5d121b..b90dd0df 100644 --- a/app.js +++ b/app.js @@ -4,177 +4,38 @@ require("colors"); const express = require("express"); const ExpressWs = require("express-ws"); -//const { GptService } = require("./services/gpt-service-streaming"); -const { GptService } = require("./services/gpt-service-non-streaming"); +const { GptService } = require("./services/gpt-service-streaming"); +//const { GptService } = require("./services/gpt-service-non-streaming"); const { TextService } = require("./services/text-service"); const { EndSessionService } = require("./services/end-session-service"); -//const welcomePrompt = require("./prompts/welcomePrompt"); const customerProfiles = require("./data/personalization"); +// Import helper functions +const { + processUserInputForHandoff, + handleLiveAgentHandoff, + handleDtmfInput, +} = require("./functions/helper-functions"); + const app = express(); ExpressWs(app); const PORT = process.env.PORT || 3000; -async function processUserInputForHandoff(userInput) { - const handoffKeywords = [ - "live agent", - "real person", - "talk to a representative", - "transfer me to a human", - "speak to a person", - "customer service", - ]; - - // Check if the input contains any of the keywords - if ( - handoffKeywords.some((keyword) => - userInput.toLowerCase().includes(keyword.toLowerCase()) - ) - ) { - console.log(`[App.js] Live agent handoff requested by user input.`); - return true; // Signals that we should perform a handoff - } - return false; // No handoff needed -} - -async function handleLiveAgentHandoff( - gptService, - endSessionService, - textService, - userProfile, - userInput -) { - const name = userProfile?.profile?.firstName - ? userProfile.profile.firstName - : ""; // Get user's name if available - - const nameIntroOptions = name - ? [ - `Sure ${name},`, - `Okay ${name},`, - `Alright ${name},`, - `Got it ${name},`, - `Certainly ${name},`, - ] - : ["Sure,", "Okay,", "Alright,", "Got it,", "Certainly,"]; - - const randomIntro = - nameIntroOptions[Math.floor(Math.random() * nameIntroOptions.length)]; - - const handoffMessages = [ - `${randomIntro} one moment, I'll transfer you to a live agent now.`, - `${randomIntro} let me get a live agent to assist you. One moment please.`, - `${randomIntro} I'll connect you with a live person right away. Just a moment.`, - `${randomIntro} sure thing, I'll transfer you to customer service. Please hold for a moment.`, - ]; - - const randomHandoffMessage = - handoffMessages[Math.floor(Math.random() * handoffMessages.length)]; - - console.log(`[App.js] Hand off message: ${randomHandoffMessage}`); - - // Send the random handoff message to the user - textService.sendText(randomHandoffMessage, true); // Final message before handoff - - // Add the final user input to userContext for summarization - gptService.updateUserContext("user", userInput); - - // Add the randomHandoffMessage to the userContext - gptService.updateUserContext("assistant", randomHandoffMessage); - - // Proceed with summarizing the conversation, including the latest messages - const conversationSummary = await gptService.summarizeConversation(); - - // End the session and include the conversation summary in the handoff data - // Introduce a delay before ending the session - setTimeout(() => { - // End the session and include the conversation summary in the handoff data - endSessionService.endSession({ - reasonCode: "live-agent-handoff", - reason: "User requested to speak to a live agent.", - conversationSummary: conversationSummary, - }); - }, 1000); // 1 second delay -} - -async function handleDtmfInput( - digit, - gptService, - textService, - interactionCount, - userProfile = null // Pass in the user profile -) { - const name = userProfile?.profile?.firstName - ? userProfile.profile.firstName - : ""; // Get user's name if available - - const nameIntroOptions = name - ? [ - `Sure ${name},`, - `Okay ${name},`, - `Alright ${name},`, - `Got it ${name},`, - `Certainly ${name},`, - ] - : ["Sure,", "Okay,", "Alright,", "Got it,", "Certainly,"]; - - const randomIntro = - nameIntroOptions[Math.floor(Math.random() * nameIntroOptions.length)]; - - switch (digit) { - case "1": - textService.sendText( - `${randomIntro} you want info on available apartments, let me get that for you, it will just take a few moments so hang tight.`, - true - ); // Run concurrently without awaiting - await gptService.completion( - "Please provide a listing of all available apartments, but as a summary, not a list.", - interactionCount, - "user", - true // DTMF-triggered flag - ); - break; - case "2": - textService.sendText( - `${randomIntro} you want me to check on your existing appointments, gimme one sec.`, - true - ); // Run concurrently without awaiting - await gptService.completion( - "Please check all available scheduled appointments.", - interactionCount, - "user", - true // DTMF-triggered flag - ); - break; - // Add more cases as needed for different DTMF inputs - default: - textService.sendText( - `Oops! That button’s a dud. But hey, press '1' to hear about available apartments or '2' to check your scheduled appointments!`, - true - ); // Run concurrently without awaiting - break; - } -} - app.post("/incoming", (req, res) => { try { - //WITH WELCOME PROMPT - // const response = ` - // - // - // - // `; + // Build the response for Twilio's verb const response = ` - + `; res.type("text/xml"); - res.end(response.toString()); + res.send(response); } catch (err) { - console.log(err); + console.error(`[App.js] Error in /incoming route: ${err}`); + res.status(500).send("Internal Server Error"); } }); @@ -190,104 +51,114 @@ app.ws("/sockets", (ws) => { let awaitingUserInput = false; let userProfile = null; - // Incoming from MediaStream - ws.on("message", async function message(data) { - const msg = JSON.parse(data); - console.log(`[App.js] Message received: ${JSON.stringify(msg)}`); - - // Handle DTMF input and interrupt ongoing interaction - if (msg.type === "dtmf" && msg.digit) { - console.log("[App.js] DTMF input received, interrupting..."); - awaitingUserInput = false; // Allow new input processing - interactionCount += 1; - await handleDtmfInput( - msg.digit, - gptService, - textService, - interactionCount, - userProfile - ); - return; - } - - if (awaitingUserInput) { - console.log( - "[App.js] Still awaiting user input, skipping new API call." - ); - return; - } - - if (msg.type === "setup") { - // Extract the phone number from the setup message - const phoneNumber = msg.from; // The Caller's phone number (this will only work for INBOUND calls at the moment) - const smsSendNumber = msg.to; // Twilio's "to" number (we will use this as the 'from' number in SMS) - - // Store the numbers in gptService for future SMS calls - gptService.setPhoneNumbers(smsSendNumber, phoneNumber); - - // Lookup the user profile from the customerProfiles object - userProfile = customerProfiles[phoneNumber]; - - // Set the user profile within GptService - if (userProfile) { - gptService.setUserProfile(userProfile); // Pass the profile to GptService + // Handle incoming messages from the WebSocket + ws.on("message", async (data) => { + try { + const msg = JSON.parse(data); + console.log(`[App.js] Message received: ${JSON.stringify(msg)}`); + + // Handle DTMF input + if (msg.type === "dtmf" && msg.digit) { + console.log("[App.js] DTMF input received, processing..."); + awaitingUserInput = false; // Allow new input processing + interactionCount += 1; + await handleDtmfInput( + msg.digit, + gptService, + textService, + interactionCount, + userProfile + ); + return; } - // Now generate a dynamic personalized greeting based on whether the user is new or returning - const greetingText = userProfile - ? `Generate a warm, personalized greeting for ${userProfile.profile.firstName}, a returning prospect. Keep it brief, and use informal/casual language so you sound like a friend, not a call center agent.` - : "Generate a warm greeting for a new potential prospect. Keep it brief, and use informal/casual language so you sound like a friend, not a call center agent."; - - // Call the LLM to generate the greeting dynamically, and it should be a another "system" prompt - await gptService.completion(greetingText, interactionCount, "system"); - - interactionCount += 1; - } else if ( - msg.type === "prompt" || - (msg.type === "interrupt" && msg.voicePrompt) - ) { - const shouldHandoff = await processUserInputForHandoff(msg.voicePrompt); + if (awaitingUserInput) { + console.log("[App.js] Awaiting user input, skipping new API call."); + return; + } - if (shouldHandoff) { - // Call handleLiveAgentHandoff without awaiting the handoff message - handleLiveAgentHandoff( - gptService, - endSessionService, - textService, - userProfile, - msg.voicePrompt + if (msg.type === "setup") { + // Extract information from the setup message + const phoneNumber = msg.from; // Caller's phone number + const smsSendNumber = msg.to; // Twilio's "to" number + const callSid = msg.callSid; // Call SID for call controls + + // Store phone numbers and callSid in gptService + gptService.setPhoneNumbers(smsSendNumber, phoneNumber); + gptService.setCallSid(callSid); + + // Retrieve user profile based on phone number + userProfile = customerProfiles[phoneNumber]; + + // Set the user profile in gptService + if (userProfile) { + gptService.setUserProfile(userProfile); + } + + // Generate a personalized greeting + const greetingText = userProfile + ? `Generate a warm, personalized greeting for ${userProfile.profile.firstName}, a returning prospect. Keep it brief, and use informal/casual language so you sound like a friend, not a call center agent.` + : "Generate a warm greeting for a new potential prospect. Keep it brief, and use informal/casual language so you sound like a friend, not a call center agent."; + + // Send the greeting as a system prompt to the assistant + await gptService.completion(greetingText, interactionCount, "system"); + + interactionCount += 1; + } else if ( + msg.type === "prompt" || + (msg.type === "interrupt" && msg.voicePrompt) + ) { + const trimmedVoicePrompt = msg.voicePrompt.trim(); + const shouldHandoff = await processUserInputForHandoff( + trimmedVoicePrompt ); - return; // End session here if live agent handoff is triggered + + if (shouldHandoff) { + // Initiate live agent handoff + handleLiveAgentHandoff( + gptService, + endSessionService, + textService, + userProfile, + trimmedVoicePrompt + ); + return; // Exit after handoff + } + + // Process the user's voice prompt + awaitingUserInput = true; + await gptService.completion(trimmedVoicePrompt, interactionCount); + interactionCount += 1; } - // Process user prompt or interrupted prompt - awaitingUserInput = true; - await gptService.completion(msg.voicePrompt, interactionCount); - interactionCount += 1; + } catch (error) { + console.error(`[App.js] Error processing message: ${error}`); } }); - gptService.on("gptreply", async (gptReply, final) => { - textService.sendText(gptReply, final); + // Listen for assistant replies + gptService.on( + "gptreply", + (gptReply, final, interactionCount, accumulatedText) => { + textService.sendText(gptReply, final, accumulatedText); - if (final) { - awaitingUserInput = false; // Reset waiting state after final response + if (final) { + awaitingUserInput = false; // Reset waiting state after final response + } } - }); + ); - // Listen for the 'endSession' event emitted by gpt-service-non-streaming + // Listen for session end events gptService.on("endSession", (handoffData) => { - // Log the handoffData for debugging purposes console.log( `[App.js] Received endSession event: ${JSON.stringify(handoffData)}` ); - - // Call the endSessionService to handle the session termination endSessionService.endSession(handoffData); }); } catch (err) { - console.log(err); + console.error(`[App.js] Error in WebSocket connection: ${err}`); } }); -app.listen(PORT); -console.log(`Server running on port ${PORT}`); +app.listen(PORT, () => { + console.log(`Server running on port ${PORT}`); +}); diff --git a/data/mock-database.js b/data/mock-database.js index a7ad12f1..8b8c31ab 100644 --- a/data/mock-database.js +++ b/data/mock-database.js @@ -2,55 +2,55 @@ const mockDatabase = { availableAppointments: [ // Existing Week { - date: "2024-09-02", + date: "2024-11-02", time: "10:00 AM", type: "in-person", apartmentType: "one-bedroom", }, { - date: "2024-09-03", + date: "2024-11-03", time: "1:00 PM", type: "in-person", apartmentType: "two-bedroom", }, { - date: "2024-09-04", + date: "2024-11-04", time: "11:00 AM", type: "self-guided", apartmentType: "studio", }, { - date: "2024-09-05", + date: "2024-11-05", time: "2:00 PM", type: "in-person", apartmentType: "three-bedroom", }, { - date: "2024-09-06", + date: "2024-11-06", time: "3:00 PM", type: "self-guided", apartmentType: "one-bedroom", }, { - date: "2024-09-07", + date: "2024-11-07", time: "9:00 AM", type: "in-person", apartmentType: "two-bedroom", }, { - date: "2024-09-08", + date: "2024-11-08", time: "11:00 AM", type: "in-person", apartmentType: "two-bedroom", }, { - date: "2024-09-09", + date: "2024-11-09", time: "10:00 AM", type: "self-guided", apartmentType: "studio", }, { - date: "2024-09-10", + date: "2024-11-10", time: "4:00 PM", type: "in-person", apartmentType: "three-bedroom", @@ -58,61 +58,61 @@ const mockDatabase = { // Extended Week 1 { - date: "2024-09-11", + date: "2024-11-11", time: "8:00 AM", type: "in-person", apartmentType: "studio", }, { - date: "2024-09-11", + date: "2024-11-11", time: "11:00 AM", type: "in-person", apartmentType: "one-bedroom", }, { - date: "2024-09-11", + date: "2024-11-11", time: "3:00 PM", type: "self-guided", apartmentType: "two-bedroom", }, { - date: "2024-09-12", + date: "2024-11-12", time: "1:00 PM", type: "in-person", apartmentType: "three-bedroom", }, { - date: "2024-09-12", + date: "2024-11-12", time: "4:00 PM", type: "in-person", apartmentType: "one-bedroom", }, { - date: "2024-09-13", + date: "2024-11-13", time: "9:00 AM", type: "self-guided", apartmentType: "studio", }, { - date: "2024-09-13", + date: "2024-11-13", time: "2:00 PM", type: "in-person", apartmentType: "two-bedroom", }, { - date: "2024-09-14", + date: "2024-11-14", time: "10:00 AM", type: "in-person", apartmentType: "three-bedroom", }, { - date: "2024-09-14", + date: "2024-11-14", time: "4:00 PM", type: "self-guided", apartmentType: "two-bedroom", }, { - date: "2024-09-15", + date: "2024-11-15", time: "12:00 PM", type: "in-person", apartmentType: "studio", @@ -120,61 +120,61 @@ const mockDatabase = { // Extended Week 2 { - date: "2024-09-16", + date: "2024-11-16", time: "11:00 AM", type: "in-person", apartmentType: "two-bedroom", }, { - date: "2024-09-16", + date: "2024-11-16", time: "3:00 PM", type: "in-person", apartmentType: "three-bedroom", }, { - date: "2024-09-17", + date: "2024-11-17", time: "9:00 AM", type: "self-guided", apartmentType: "one-bedroom", }, { - date: "2024-09-17", + date: "2024-11-17", time: "2:00 PM", type: "in-person", apartmentType: "studio", }, { - date: "2024-09-18", + date: "2024-11-18", time: "4:00 PM", type: "in-person", apartmentType: "two-bedroom", }, { - date: "2024-09-18", + date: "2024-11-18", time: "12:00 PM", type: "self-guided", apartmentType: "three-bedroom", }, { - date: "2024-09-19", + date: "2024-11-19", time: "10:00 AM", type: "in-person", apartmentType: "one-bedroom", }, { - date: "2024-09-19", + date: "2024-11-19", time: "3:00 PM", type: "in-person", apartmentType: "two-bedroom", }, { - date: "2024-09-20", + date: "2024-11-20", time: "1:00 PM", type: "in-person", apartmentType: "three-bedroom", }, { - date: "2024-09-20", + date: "2024-11-20", time: "5:00 PM", type: "self-guided", apartmentType: "studio", @@ -186,7 +186,7 @@ const mockDatabase = { layout: "Studio", squareFeet: 450, rent: 1050, - moveInDate: "2024-09-15", + moveInDate: "2024-11-15", features: ["1 bathroom", "open kitchen", "private balcony"], petPolicy: "No pets allowed.", fees: { @@ -194,7 +194,7 @@ const mockDatabase = { securityDeposit: 300, }, parking: "1 reserved parking spot included.", - specials: "First month's rent free if you move in before 2024-09-30.", + specials: "First month's rent free if you move in before 2024-11-30.", incomeRequirements: "Income must be 2.5x the rent.", utilities: "Water, trash, and Wi-Fi internet included. Tenant pays electricity and gas.", @@ -209,7 +209,7 @@ const mockDatabase = { layout: "One-bedroom", squareFeet: 600, rent: 1200, - moveInDate: "2024-09-20", + moveInDate: "2024-11-20", features: ["1 bedroom", "1 bathroom", "walk-in closet"], petPolicy: "Cats only. No dogs or any other animals.", fees: { @@ -217,7 +217,7 @@ const mockDatabase = { securityDeposit: 400, }, parking: "1 reserved parking spot included.", - specials: "First month's rent free if you move in before 2024-09-25.", + specials: "First month's rent free if you move in before 2024-11-25.", incomeRequirements: "Income must be 3x the rent.", utilities: "Water, trash, gas, and Wi-Fi internet included. Tenant pays electricity.", @@ -232,7 +232,7 @@ const mockDatabase = { layout: "Two-bedroom", squareFeet: 950, rent: 1800, - moveInDate: "2024-09-10", + moveInDate: "2024-11-10", features: ["2 bedrooms", "2 bathrooms", "walk-in closets", "balcony"], petPolicy: "Cats and dogs allowed, but only 1 each.", fees: { @@ -240,7 +240,7 @@ const mockDatabase = { securityDeposit: 500, }, parking: "2 reserved parking spots included.", - specials: "Waived application fee if you move in before 2024-09-20.", + specials: "Waived application fee if you move in before 2024-11-20.", incomeRequirements: "Income must be 3x the rent.", utilities: "Water, trash, gas, and Wi-Fi internet included. Tenant pays electricity.", @@ -255,7 +255,7 @@ const mockDatabase = { layout: "Three-bedroom", squareFeet: 1200, rent: 2500, - moveInDate: "2024-09-25", + moveInDate: "2024-11-25", features: [ "3 bedrooms", "2 bathrooms", @@ -264,7 +264,7 @@ const mockDatabase = { "extra storage", ], petPolicy: - "Up to 2 dogs and 2 cats are allowed, and other small pets like hamsters are allwed as well. No more than 4 total pets.", + "Up to 2 dogs and 2 cats are allowed, and other small pets like hamsters are allowed as well. No more than 4 total pets.", fees: { applicationFee: 50, securityDeposit: 600, diff --git a/functions/available-functions.js b/functions/available-functions.js index 1ace9a18..55926bb6 100644 --- a/functions/available-functions.js +++ b/functions/available-functions.js @@ -1,6 +1,34 @@ const mockDatabase = require("../data/mock-database"); const twilio = require("twilio"); +// Send SMS using Twilio API +const accountSid = process.env.TWILIO_ACCOUNT_SID; +const authToken = process.env.TWILIO_AUTH_TOKEN; +const client = twilio(accountSid, authToken); + +// Call controls +async function endCall(args) { + const { callSid } = args; + try { + setTimeout(async () => { + const call = await client.calls(callSid).update({ + twiml: "", + }); + console.log("Call ended for:", call.sid); + }, 3000); + return { + status: "success", + message: `Call has ended`, + }; + } catch (error) { + console.error("Twilio end error: ", error); + return { + status: "error", + message: `An error occurred whilst trying to hangup`, + }; + } +} + // Utility function to normalize various time formats to the database's 12-hour AM/PM format function normalizeTimeFormat(time) { // Check if time is already in the desired AM/PM format @@ -84,11 +112,6 @@ async function sendAppointmentConfirmationSms(args) { appointmentDetails.type === "in-person" ? "an in-person" : "a self-guided"; const message = `Hi ${name}, your tour for a ${apartmentType} apartment at Parkview is confirmed for ${appointmentDetails.date} at ${appointmentDetails.time}. This will be ${tourType} tour. We'll be ready for your visit! Let us know if you have any questions.`; - // Send SMS using Twilio API - const accountSid = process.env.TWILIO_ACCOUNT_SID; - const authToken = process.env.TWILIO_AUTH_TOKEN; - const client = twilio(accountSid, authToken); - try { const smsResponse = await client.messages.create({ body: message, @@ -408,6 +431,7 @@ async function listAvailableApartments(args) { // Export all functions module.exports = { + endCall, liveAgentHandoff, sendAppointmentConfirmationSms, scheduleTour, diff --git a/functions/function-manifest.js b/functions/function-manifest.js index a78628d5..35063a14 100644 --- a/functions/function-manifest.js +++ b/functions/function-manifest.js @@ -1,4 +1,18 @@ const tools = [ + { + type: "function", + function: { + name: "endCall", + description: + "Ends the call by hanging up when the user explicitly requests it or when the conversation has naturally concluded with no further actions required.", + parameters: { + type: "object", + properties: {}, + required: [], + }, + }, + }, + { type: "function", function: { @@ -101,7 +115,7 @@ const tools = [ date: { type: "string", description: - "The date the user wants to schedule the tour for (YYYY-MM-DD).", + "The date the user wants to schedule the tour for (YYYY-MM-DD). **Convert any relative date expressions to this format based on {{currentDate}}.**", }, time: { type: "string", @@ -136,7 +150,7 @@ const tools = [ date: { type: "string", description: - "The date the user wants to check for tour availability (YYYY-MM-DD).", + "The date the user wants to check for tour availability (YYYY-MM-DD). **Convert any relative date expressions to this format based on {{currentDate}}.**", }, time: { type: "string", @@ -171,7 +185,7 @@ const tools = [ date: { type: "string", description: - "The move-in date the user prefers (optional, YYYY-MM-DD).", + "The move-in date the user prefers (optional, YYYY-MM-DD). **Convert any relative date expressions to this format based on {{currentDate}}.**", }, budget: { type: "integer", diff --git a/functions/helper-functions.js b/functions/helper-functions.js new file mode 100644 index 00000000..cac71d68 --- /dev/null +++ b/functions/helper-functions.js @@ -0,0 +1,404 @@ +// helper-functions.js + +// Helper function to format dates to 'YYYY-MM-DD' +function formatDate(date) { + return date.toISOString().split("T")[0]; +} + +// Helper function to add days to a date +function addDays(date, days) { + const result = new Date(date); + result.setDate(result.getDate() + days); + return result; +} + +// Function to generate a mock database +function generateMockDatabase(currentDate = new Date()) { + // Arrays for random selection + const apartmentTypes = [ + "studio", + "one-bedroom", + "two-bedroom", + "three-bedroom", + ]; + const tourTypes = ["in-person", "self-guided"]; + const times = [ + "9:00 AM", + "10:00 AM", + "11:00 AM", + "1:00 PM", + "2:00 PM", + "3:00 PM", + "4:00 PM", + "5:00 PM", + ]; + + // Generate available appointments for the next 20 days + const availableAppointments = Array.from({ length: 20 }, (_, i) => { + const date = addDays(currentDate, i + 1); + return { + date: formatDate(date), + time: times[Math.floor(Math.random() * times.length)], + type: tourTypes[Math.floor(Math.random() * tourTypes.length)], + apartmentType: + apartmentTypes[Math.floor(Math.random() * apartmentTypes.length)], + }; + }); + + // Dynamic move-in dates and specials for apartment details + const apartmentDetails = { + studio: { + layout: "Studio", + squareFeet: 450, + rent: 1050, + moveInDate: formatDate(addDays(currentDate, 15)), + features: [ + "Open floor plan", + "Compact kitchen with modern appliances", + "Large windows with city views", + "Walk-in shower", + "In-unit washer and dryer", + ], + amenities: [ + "Fitness center access", + "Community lounge", + "24-hour maintenance", + ], + petPolicy: "No pets allowed.", + fees: { + applicationFee: 50, + securityDeposit: 300, + }, + parking: "Street parking available.", + storage: "No additional storage available.", + specials: `First month's rent free if you move in before ${formatDate( + addDays(currentDate, 30) + )}.`, + leaseTerms: "12-month lease required.", + incomeRequirements: "Income must be 2.5x the rent.", + utilities: + "Water, trash, and Wi-Fi included. Tenant pays electricity and gas.", + location: { + street: "1657 Coolidge Street", + city: "Missoula", + state: "Montana", + zipCode: "59802", + }, + }, + "one-bedroom": { + layout: "One-bedroom", + squareFeet: 650, + rent: 1250, + moveInDate: formatDate(addDays(currentDate, 20)), + features: [ + "Separate bedroom", + "Full kitchen with dishwasher", + "Private balcony", + "Walk-in closet", + "In-unit washer and dryer", + ], + amenities: ["Swimming pool access", "Fitness center", "On-site laundry"], + petPolicy: "Cats allowed with a $200 pet deposit.", + fees: { + applicationFee: 50, + securityDeposit: 400, + petDeposit: 200, + }, + parking: "One reserved parking spot included.", + storage: "Additional storage units available for $50/month.", + specials: `Free parking for the first 6 months if you move in before ${formatDate( + addDays(currentDate, 35) + )}.`, + leaseTerms: "Flexible lease terms from 6 to 12 months.", + incomeRequirements: "Income must be 3x the rent.", + utilities: + "Water and trash included. Tenant pays electricity, gas, and internet.", + location: { + street: "1705 Adams Street", + city: "Missoula", + state: "Montana", + zipCode: "59802", + }, + }, + "two-bedroom": { + layout: "Two-bedroom", + squareFeet: 950, + rent: 1800, + moveInDate: formatDate(addDays(currentDate, 10)), + features: [ + "Two bedrooms", + "Two bathrooms", + "Open living and dining area", + "Modern kitchen with granite countertops", + "In-unit washer and dryer", + ], + amenities: [ + "Fitness center", + "Community garden", + "BBQ area", + "Covered parking", + ], + petPolicy: "Cats and small dogs allowed with a $250 pet deposit.", + fees: { + applicationFee: 50, + securityDeposit: 500, + petDeposit: 250, + }, + parking: "Two reserved covered parking spots included.", + storage: "Complimentary storage unit included.", + specials: `Reduced security deposit if you move in before ${formatDate( + addDays(currentDate, 25) + )}.`, + leaseTerms: "12-month lease preferred.", + incomeRequirements: "Income must be 3x the rent.", + utilities: + "Water, trash, and gas included. Tenant pays electricity and internet.", + location: { + street: "1833 Jefferson Avenue", + city: "Missoula", + state: "Montana", + zipCode: "59802", + }, + }, + "three-bedroom": { + layout: "Three-bedroom", + squareFeet: 1200, + rent: 2500, + moveInDate: formatDate(addDays(currentDate, 25)), + features: [ + "Three spacious bedrooms", + "Two bathrooms", + "Large kitchen with island", + "Private patio", + "Fireplace", + "In-unit washer and dryer", + "Smart home features", + ], + amenities: [ + "Swimming pool", + "Fitness center", + "Business center", + "Playground", + ], + petPolicy: "Pets allowed with breed restrictions; up to 2 pets.", + fees: { + applicationFee: 50, + securityDeposit: 600, + petDeposit: 300, + }, + parking: "Two reserved parking spots included.", + storage: "Large storage units available for $75/month.", + specials: "No application fee if you sign a 12-month lease.", + leaseTerms: "12 or 18-month lease options.", + incomeRequirements: "Income must be 3x the rent.", + utilities: + "Water, trash, gas, and Wi-Fi included. Tenant pays electricity.", + location: { + street: "1945 Roosevelt Way", + city: "Missoula", + state: "Montana", + zipCode: "59802", + }, + }, + }; + + return { + availableAppointments, + appointments: [], + apartmentDetails, + }; +} + +// Helper function to generate TTS messages for tools +function getTtsMessageForTool(toolName, userProfile, updateUserContext) { + const name = userProfile?.profile?.firstName || ""; + const nameIntroOptions = name + ? [ + `Sure ${name},`, + `Okay ${name},`, + `Alright ${name},`, + `Got it ${name},`, + `Certainly ${name},`, + ] + : ["Sure,", "Okay,", "Alright,", "Got it,", "Certainly,"]; + const randomIntro = + nameIntroOptions[Math.floor(Math.random() * nameIntroOptions.length)]; + + const toolMessages = { + listAvailableApartments: `${randomIntro} let me check on the available apartments for you.`, + checkExistingAppointments: `${randomIntro} I'll look up your existing appointments.`, + scheduleTour: `${randomIntro} I'll go ahead and schedule that tour for you.`, + checkAvailability: `${randomIntro} let me verify the availability for the requested time.`, + commonInquiries: `${randomIntro} one moment while I look that up.`, + sendAppointmentConfirmationSms: `${randomIntro} I'll send that SMS off to you shortly. Give it a few minutes, and you should see it come through.`, + liveAgentHandoff: `${randomIntro} that may be a challenging topic to discuss, so I'm going to get you over to a live agent. Hang tight.`, + }; + + const message = + toolMessages[toolName] || + `${randomIntro} give me a moment while I fetch the information.`; + + // Log the message to the userContext + updateUserContext("assistant", message); + + return message; +} +// Function to process user input for handoff +async function processUserInputForHandoff(userInput) { + const handoffKeywords = [ + "live agent", + "real person", + "talk to a representative", + "transfer me to a human", + "speak to a person", + "customer service", + ]; + + // Check if the input contains any of the keywords + if ( + handoffKeywords.some((keyword) => + userInput.toLowerCase().includes(keyword.toLowerCase()) + ) + ) { + console.log( + `[AppHelperFunctions] Live agent handoff requested by user input.` + ); + return true; // Signals that we should perform a handoff + } + return false; // No handoff needed +} + +// Function to handle live agent handoff +async function handleLiveAgentHandoff( + gptService, + endSessionService, + textService, + userProfile, + userInput +) { + const name = userProfile?.profile?.firstName + ? userProfile.profile.firstName + : ""; // Get user's name if available + + const nameIntroOptions = name + ? [ + `Sure ${name},`, + `Okay ${name},`, + `Alright ${name},`, + `Got it ${name},`, + `Certainly ${name},`, + ] + : ["Sure,", "Okay,", "Alright,", "Got it,", "Certainly,"]; + + const randomIntro = + nameIntroOptions[Math.floor(Math.random() * nameIntroOptions.length)]; + + const handoffMessages = [ + `${randomIntro} one moment, I'll transfer you to a live agent now.`, + `${randomIntro} let me get a live agent to assist you. One moment please.`, + `${randomIntro} I'll connect you with a live person right away. Just a moment.`, + `${randomIntro} sure thing, I'll transfer you to customer service. Please hold for a moment.`, + ]; + + const randomHandoffMessage = + handoffMessages[Math.floor(Math.random() * handoffMessages.length)]; + + console.log(`[AppHelperFunctions] Hand off message: ${randomHandoffMessage}`); + + // Send the random handoff message to the user + textService.sendText(randomHandoffMessage, true); // Final message before handoff + + // Add the final user input to userContext for summarization + gptService.updateUserContext("user", userInput); + + // Add the randomHandoffMessage to the userContext + gptService.updateUserContext("assistant", randomHandoffMessage); + + // Proceed with summarizing the conversation, including the latest messages + const conversationSummary = await gptService.summarizeConversation(); + + // End the session and include the conversation summary in the handoff data + // Introduce a delay before ending the session + setTimeout(() => { + // End the session and include the conversation summary in the handoff data + endSessionService.endSession({ + reasonCode: "live-agent-handoff", + reason: "User requested to speak to a live agent.", + conversationSummary: conversationSummary, + }); + }, 1000); // 1 second delay +} + +// Function to handle DTMF input +async function handleDtmfInput( + digit, + gptService, + textService, + interactionCount, + userProfile = null // Pass in the user profile +) { + const name = userProfile?.profile?.firstName + ? userProfile.profile.firstName + : ""; // Get user's name if available + + const nameIntroOptions = name + ? [ + `Sure ${name},`, + `Okay ${name},`, + `Alright ${name},`, + `Got it ${name},`, + `Certainly ${name},`, + ] + : ["Sure,", "Okay,", "Alright,", "Got it,", "Certainly,"]; + + const randomIntro = + nameIntroOptions[Math.floor(Math.random() * nameIntroOptions.length)]; + + switch (digit) { + case "1": + textService.sendText( + `${randomIntro} you want info on available apartments, let me get that for you, it will just take a few moments so hang tight.`, + true + ); // Send the message to the user + + // Process the request using gptService + await gptService.completion( + "Please provide a listing of all available apartments, but as a summary, not a list.", + interactionCount, + "user", + true // DTMF-triggered flag + ); + break; + + case "2": + textService.sendText( + `${randomIntro} you want me to check on your existing appointments, gimme one sec.`, + true + ); // Send the message to the user + + // Process the request using gptService + await gptService.completion( + "Please check all available scheduled appointments.", + interactionCount, + "user", + true // DTMF-triggered flag + ); + break; + + // Add more cases as needed for different DTMF inputs + default: + textService.sendText( + `Oops! That button’s a dud. But hey, press '1' to hear about available apartments or '2' to check your scheduled appointments!`, + true + ); // Send the default message + break; + } +} + +module.exports = { + generateMockDatabase, + getTtsMessageForTool, + processUserInputForHandoff, + handleLiveAgentHandoff, + handleDtmfInput, +}; diff --git a/services/gpt-service-non-streaming.js b/services/gpt-service-non-streaming.js index dfa1a8cd..75fedf61 100644 --- a/services/gpt-service-non-streaming.js +++ b/services/gpt-service-non-streaming.js @@ -116,6 +116,15 @@ class GptService extends EventEmitter { getPhoneNumbers() { return { to: this.smsSendNumber, from: this.phoneNumber }; } + // Store call SID from app.js + setCallSid(callSid) { + this.callSid = callSid; + } + + // Retrieve call SID + getCallSid() { + return { callSid: this.callSid }; + } log(message) { const timestamp = new Date().toISOString(); @@ -229,6 +238,11 @@ class GptService extends EventEmitter { const phoneNumbers = this.getPhoneNumbers(); functionArgs = { ...functionArgs, ...phoneNumbers }; } + + // Inject callSid for call controls + if (toolCall.functionName === "endCall") { + functionArgs = { ...functionArgs, ...this.getCallSid() }; + } const functionResponse = await functionToCall(functionArgs); function_call_result_message = { @@ -337,7 +351,13 @@ class GptService extends EventEmitter { }); // Emit the final response to the user - this.emit("gptreply", finalContent, true, interactionCount); + this.emit( + "gptreply", + finalContent, + true, + interactionCount, + finalContent + ); return; // Exit after processing the tool call } else { // If no tool call is detected, emit the final completion response @@ -347,7 +367,13 @@ class GptService extends EventEmitter { role: "assistant", content: finalResponse, }); - this.emit("gptreply", finalResponse, true, interactionCount); + this.emit( + "gptreply", + finalResponse, + true, + interactionCount, + finalResponse + ); } } } catch (error) { diff --git a/services/gpt-service-streaming.js b/services/gpt-service-streaming.js index 75a48e88..f9d706f4 100644 --- a/services/gpt-service-streaming.js +++ b/services/gpt-service-streaming.js @@ -1,89 +1,43 @@ -const OpenAI = require("openai"); // or the appropriate module import +// Import necessary modules +const OpenAI = require("openai"); const EventEmitter = require("events"); const availableFunctions = require("../functions/available-functions"); const tools = require("../functions/function-manifest"); let prompt = require("../prompts/prompt"); -//const welcomePrompt = require("../prompts/welcomePrompt"); -const model = "gpt-4o"; +const model = "gpt-4o-mini"; +// Import helper functions +const { + generateMockDatabase, + getTtsMessageForTool, +} = require("../functions/helper-functions"); + +// Set current date for prompt const currentDate = new Date().toLocaleDateString("en-US", { weekday: "long", year: "numeric", month: "long", day: "numeric", }); - prompt = prompt.replace("{{currentDate}}", currentDate); class GptService extends EventEmitter { constructor() { super(); this.openai = new OpenAI(); - this.userContext = [ - { role: "system", content: prompt }, - // //Only do this if you're going to use the WelcomePrompt in VoxRay config - // { - // role: "assistant", - // content: `${welcomePrompt}`, - // }, - ]; - this.smsSendNumber = null; // Store the "To" number (Twilio's "from") - this.phoneNumber = null; // Store the "From" number (user's phone) - } - // Arrow function for getTtsMessageForTool, so it can access `this` - getTtsMessageForTool = (toolName) => { - const name = this.userProfile?.profile?.firstName - ? this.userProfile.profile.firstName - : ""; // Get the user's name if available - - const nameIntroOptions = name - ? [ - `Sure ${name},`, - `Okay ${name},`, - `Alright ${name},`, - `Got it ${name},`, - `Certainly ${name},`, - ] - : ["Sure,", "Okay,", "Alright,", "Got it,", "Certainly,"]; - - const randomIntro = - nameIntroOptions[Math.floor(Math.random() * nameIntroOptions.length)]; - - let message; - - switch (toolName) { - case "listAvailableApartments": - message = `${randomIntro} let me check on the available apartments for you.`; - break; - case "checkExistingAppointments": - message = `${randomIntro} I'll look up your existing appointments.`; - break; - case "scheduleTour": - message = `${randomIntro} I'll go ahead and schedule that tour for you.`; - break; - case "checkAvailability": - message = `${randomIntro} let me verify the availability for the requested time.`; - break; - case "commonInquiries": - message = `${randomIntro} one moment.`; - break; - case "sendAppointmentConfirmationSms": - message = `${randomIntro} I'll send that SMS off to you shortly, give it a few minutes and you should see it come through.`; - break; - case "liveAgentHandoff": - message = `${randomIntro} that may be a challenging topic to discuss, so I'm going to get you over to a live agent so they can discuss this with you, hang tight.`; - break; - default: - message = `${randomIntro} give me a moment while I fetch the information.`; - break; - } + this.userContext = [{ role: "system", content: prompt }]; + this.smsSendNumber = null; + this.phoneNumber = null; + this.callSid = null; + this.userProfile = null; - // Log the message to the userContext in gptService - this.updateUserContext("assistant", message); + // Generate dynamic mock database + this.mockDatabase = generateMockDatabase(); - return message; // Return the message for TTS - }; + // No need to bind methods as helper functions are now imported + } + // Set user profile and update context with conversation history setUserProfile(userProfile) { this.userProfile = userProfile; if (userProfile) { @@ -94,7 +48,6 @@ class GptService extends EventEmitter { `On ${history.date}, ${firstName} asked: ${history.summary}` ) .join(" "); - // Add the conversation history to the system context this.userContext.push({ role: "system", content: `${firstName} has had previous interactions. Conversation history: ${historySummaries}`, @@ -102,66 +55,52 @@ class GptService extends EventEmitter { } } - // Method to store the phone numbers from app.js + // Store phone numbers from app.js setPhoneNumbers(smsSendNumber, phoneNumber) { this.smsSendNumber = smsSendNumber; this.phoneNumber = phoneNumber; } - // Method to retrieve the stored numbers (can be used in the function calls) + // Retrieve stored numbers getPhoneNumbers() { return { to: this.smsSendNumber, from: this.phoneNumber }; } + // Store call SID from app.js + setCallSid(callSid) { + this.callSid = callSid; + } + + // Retrieve call SID + getCallSid() { + return { callSid: this.callSid }; + } + + // Logging utility log(message) { const timestamp = new Date().toISOString(); console.log(`[${timestamp}] ${message}`); } - updateUserContext(role, text) { - this.userContext.push({ role: role, content: text }); + // Update user context + updateUserContext(role, content) { + this.userContext.push({ role, content }); } + // Summarize conversation async summarizeConversation() { const summaryPrompt = "Summarize the conversation so far in 2-3 sentences."; - - // // Log the full userContext before making the API call - // console.log( - // `[GptService] Full userContext: ${JSON.stringify( - // this.userContext, - // null, - // 2 - // )}` - // ); - - // // Validate and log each message in userContext - // this.userContext.forEach((message, index) => { - // if (typeof message.content !== "string") { - // console.error( - // `[GptService] Invalid content type at index ${index}: ${JSON.stringify( - // message - // )}` - // ); - // } else { - // console.log( - // `[GptService] Valid content at index ${index}: ${message.content}` - // ); - // } - // }); - const summaryResponse = await this.openai.chat.completions.create({ - model: model, + model, messages: [ ...this.userContext, { role: "system", content: summaryPrompt }, ], - stream: false, // Non-streaming }); - - const summary = summaryResponse.choices[0]?.message?.content || ""; - return summary; + return summaryResponse.choices[0]?.message?.content || ""; } + // Main completion method async completion( text, interactionCount, @@ -176,181 +115,151 @@ class GptService extends EventEmitter { this.updateUserContext(role, text); try { - // Streaming is enabled const response = await this.openai.chat.completions.create({ - model: model, + model, messages: this.userContext, - tools: tools, - stream: true, // Always streaming + tools, + stream: true, }); - let toolCalls = {}; // Object to accumulate multiple tool calls by their ID - let functionCallResults = []; // Array to accumulate function call results - let contentAccumulator = ""; // To accumulate the 'content' before tool_calls + let toolCalls = {}; + let functionCallResults = []; + let contentAccumulator = ""; let finalMessageObject = { role: "assistant", content: null, tool_calls: [], refusal: null, - }; // Final object to store content and tool call details - - let currentToolCallId = null; // To store the ID of the active tool call + }; + let currentToolCallId = null; for await (const chunk of response) { const { choices } = chunk; - //Log each chunk as it comes in - // this.log(`[GptService] Chunk received: ${JSON.stringify(chunk)}`); - - // Check if tool_calls are present in this chunk (could be part of multiple chunks) + // Handle tool calls if (choices[0]?.delta?.tool_calls) { const toolCall = choices[0].delta.tool_calls[0]; - - // Check if this is a new tool call (only when an ID is present) if (toolCall.id && toolCall.id !== currentToolCallId) { - // Check if currentToolCallId is not null, indicating a subsequent tool call const isFirstToolCall = currentToolCallId === null; - currentToolCallId = toolCall.id; - // Initialize new tool call if not already in the map if (!toolCalls[currentToolCallId]) { - // this.log( - // `[GptService] Final Content of this tool call: ${contentAccumulator}` - // ); - if (choices[0]?.delta?.content) { - // this.log( - // `[GptService] Last chunk to emit: ${choices[0].delta.content}` - // ); this.emit( "gptreply", - choices[0]?.delta?.content, + choices[0].delta.content, true, - interactionCount + interactionCount, + contentAccumulator ); } else { - // this.log( - // `[GptService] Emitting empty string as final content chunk to voxray` - // ); - //If the content is empty (in the case of OpenAI Chat Completions, it will ALWAYS be this) then just send an empty token - this.emit("gptreply", "", true, interactionCount); + this.emit( + "gptreply", + "", + true, + interactionCount, + contentAccumulator + ); } - //Log the last content to an assistant message (IS THIS AN OPENAI BUG??? For some reason, the finish_reason never is "STOP" and we miss the final punctuation (eg. "One Moment" should be "One Moment." where the period is the final content, but that period is being sent back after the function call completes)) if (contentAccumulator.length > 0 && isFirstToolCall) { this.userContext.push({ role: "assistant", content: contentAccumulator.trim(), }); - // // Log the full userContext before making the API call - // console.log( - // `[GptService] Full userContext before next tool call id: ${JSON.stringify( - // this.userContext, - // null, - // 2 - // )}` - // ); - - this.log( - `[GptService] Final GPT -> user context length: ${this.userContext.length}` - ); + // // Emit TTS message related to the tool call + // if (!dtmfTriggered) { + // const ttsMessage = getTtsMessageForTool( + // toolCall.functionName, + // this.userProfile, + // this.updateUserContext.bind(this) + // ); + // this.emit( + // "gptreply", + // ttsMessage, + // true, + // interactionCount, + // ttsMessage + // ); + // } } toolCalls[currentToolCallId] = { id: currentToolCallId, functionName: toolCall.function.name, - arguments: "", // Initialize an empty string for accumulating arguments + arguments: "", }; - // Log tool call detection this.log( `[GptService] Detected new tool call: ${toolCall.function.name}` ); - // Log tool call detection - // this.log( - // `[GptService] Log the choices: ${JSON.stringify(choices[0])}` - // ); } } } - // Separate block to handle when finish_reason is 'tool_calls' + // Finish reason is 'tool_calls' if (choices[0]?.finish_reason === "tool_calls") { this.log(`[GptService] All tool calls have been completed`); const systemMessages = []; - // Process each tool call in the accumulated toolCalls object + + // Process each tool call for (const toolCallId in toolCalls) { const toolCall = toolCalls[toolCallId]; let parsedArguments; try { - // Parse accumulated arguments for this tool call parsedArguments = JSON.parse(toolCall.arguments); - } catch (error) { - console.error("Error parsing arguments:", error); - parsedArguments = toolCall.arguments; // Fallback in case of parsing failure + } catch { + parsedArguments = toolCall.arguments; } - - // Finalize the tool call in the final message object + // Log the function name and arguments after collecting all arguments + this.log( + `[GptService] Tool call function: ${toolCall.functionName}` + ); + this.log( + `[GptService] Calling function ${ + toolCall.functionName + } with arguments: ${JSON.stringify(parsedArguments)}` + ); finalMessageObject.tool_calls.push({ id: toolCall.id, type: "function", function: { name: toolCall.functionName, - arguments: JSON.stringify(parsedArguments), // Ensure arguments are stringified + arguments: JSON.stringify(parsedArguments), }, }); - // if (!dtmfTriggered) { - // // Emit TTS message related to the tool call - // const ttsMessage = this.getTtsMessageForTool(toolCallFunctionName); - // this.emit("gptreply", ttsMessage, true, interactionCount); // Emit the TTS message immediately - // } - - // Inject phone numbers if it's the SMS function + // Inject phone numbers for SMS function if (toolCall.functionName === "sendAppointmentConfirmationSms") { - const phoneNumbers = this.getPhoneNumbers(); - parsedArguments = { ...parsedArguments, ...phoneNumbers }; + parsedArguments = { + ...parsedArguments, + ...this.getPhoneNumbers(), + }; } - // Now perform the tool logic as all tool_call data is ready - const functionToCall = availableFunctions[toolCall.functionName]; - this.log( - `[GptService] Calling function ${ - toolCall.functionName - } with arguments: ${JSON.stringify(parsedArguments)}` - ); + // Inject callSid for call controls + if (toolCall.functionName === "endCall") { + parsedArguments = { ...parsedArguments, ...this.getCallSid() }; + } // Call the respective function + const functionToCall = availableFunctions[toolCall.functionName]; const functionResponse = await functionToCall(parsedArguments); - // Construct the function call result message for this tool call + // Store function call result functionCallResults.push({ role: "tool", content: JSON.stringify(functionResponse), tool_call_id: toolCall.id, }); - // Check if specific tool calls require additional system messages - + // Additional system messages if (toolCall.functionName === "listAvailableApartments") { systemMessages.push({ role: "system", content: - "Provide a summary of available apartments. Do not you symbols, and do not use markdown in your response.", - }); - } - - // Personalize system messages based on user profile during relevant tool calls - if ( - toolCall.functionName === "checkAvailability" && - this.userProfile - ) { - const { firstName, moveInDate } = this.userProfile.profile; - systemMessages.push({ - role: "system", - content: `When checking availability for ${firstName}, remember that they are looking to move in on ${moveInDate}.`, + "Provide a summary of available apartments without using symbols or markdown.", }); } @@ -358,7 +267,6 @@ class GptService extends EventEmitter { toolCall.functionName === "scheduleTour" && functionResponse.available ) { - // Inject a system message to ask about SMS confirmation systemMessages.push({ role: "system", content: @@ -366,236 +274,161 @@ class GptService extends EventEmitter { }); } - // Check if the tool call is for the 'liveAgentHandoff' function if (toolCall.functionName === "liveAgentHandoff") { setTimeout(async () => { const conversationSummary = await this.summarizeConversation(); - this.emit("endSession", { reasonCode: "live-agent-handoff", reason: functionResponse.reason, - conversationSummary: conversationSummary, + conversationSummary, }); - this.log( `[GptService] Emitting endSession event with reason: ${functionResponse.reason}` ); - }, 3000); // 3-second delay - - this.log( - `[GptService] Emitting endSession event with reason: ${functionResponse.reason}` - ); + }, 3000); } } - // Prepare the chat completion call payload with the tool result - const completion_payload = { - model: model, + // Prepare the chat completion call payload + const completionPayload = { + model, messages: [ ...this.userContext, - ...systemMessages, // Inject dynamic system messages when relevant - finalMessageObject, // the tool_call message - ...functionCallResults, // The result of the tool call + ...systemMessages, + finalMessageObject, + ...functionCallResults, ], }; - // //Log the payload to the console - // console.log( - // `[GptService] Completion payload: ${JSON.stringify( - // completion_payload, - // null, - // 2 - // )}` - // ); - // Call the API again with streaming for final response const finalResponseStream = await this.openai.chat.completions.create( { - model: completion_payload.model, - messages: completion_payload.messages, + model: completionPayload.model, + messages: completionPayload.messages, stream: true, } ); - // Handle the final response stream (same logic as before) + // Handle the final response stream let finalContentAccumulator = ""; for await (const chunk of finalResponseStream) { const { choices } = chunk; - // this.log( - // `[GptService] Final Chunk received: ${JSON.stringify(chunk)}` - // ); - - // Accumulate the content from each chunk if (!choices[0]?.delta?.tool_calls) { - // Check if the current chunk is the last one in the stream if (choices[0].finish_reason === "stop") { - this.log(`[GptService] In finish reason === STOP`); - if (choices[0]?.delta?.content) { - // this.log( - // `[GptService] Last chunk to emit (non-tool call): ${choices[0].delta.content}` - // ); + finalContentAccumulator += choices[0].delta.content; this.emit( "gptreply", - choices[0]?.delta?.content, + choices[0].delta.content, true, - interactionCount + interactionCount, + finalContentAccumulator ); - //accumulate the final content chunk before pushing to user context - finalContentAccumulator += choices[0].delta.content; } else { - // this.log( - // `[GptService] Emitting empty string as final content chunk (non-tool call) to voxray` - // ); - //If the content is empty (in the case of OpenAI Chat Completions, it will ALWAYS be this) then just send an empty token - this.emit("gptreply", "", true, interactionCount); + this.emit( + "gptreply", + "", + true, + interactionCount, + finalContentAccumulator + ); } - // if (lastContentChunk) { - // this.emit("gptreply", lastContentChunk, true, interactionCount); - // } this.userContext.push({ role: "assistant", content: finalContentAccumulator.trim(), }); - // Log the full userContext before making the API call - // console.log( - // `[GptService] Full userContext after tool call: ${JSON.stringify( - // this.userContext, - // null, - // 2 - // )}` - // ); - - this.log( - `[GptService] Final GPT -> user context length: ${this.userContext.length}` + //Log the full userContext before making the API call + console.log( + `[GptService] Full userContext after tool call: ${JSON.stringify( + this.userContext, + null, + 2 + )}` ); - - break; // Exit the loop once the final response is complete - } else { - //We only will start emitting chunks after the chunk with ROLE defined "delta":{"role":"assistant","content":"","refusal":null} because there is no content here - if (!choices[0]?.delta?.role && choices[0]?.delta?.content) { - // this.log( - // `[GptService] Emitting intermediary chunk (tool call): ${choices[0].delta.content}` - // ); - //emit the chunk knowing its a content chunk and not the final one - this.emit( - "gptreply", - choices[0].delta.content, - false, - interactionCount - ); - //continue accumulating content chunks - finalContentAccumulator += choices[0].delta.content; - } + break; + } else if ( + !choices[0]?.delta?.role && + choices[0]?.delta?.content + ) { + this.emit( + "gptreply", + choices[0].delta.content, + false, + interactionCount + ); + finalContentAccumulator += choices[0].delta.content; } } } - // Reset tool call state after completion - toolCalls = {}; // Clear all stored tool calls - currentToolCallId = null; // Reset tool call ID + + // Reset tool call state + toolCalls = {}; + currentToolCallId = null; } else { - // If the Finish Reason isn't "tool_calls", then accumulate arguments for the current tool call + // Accumulate arguments for the current tool call if (currentToolCallId && toolCalls[currentToolCallId]) { if (choices[0]?.delta?.tool_calls[0]?.function?.arguments) { toolCalls[currentToolCallId].arguments += choices[0].delta.tool_calls[0].function.arguments; - // this.log( - // `[GptService] Accumulated arguments for tool call ${currentToolCallId}: ${toolCalls[currentToolCallId].arguments}` - // ); } } } // Handle non-tool_call content chunks if (!choices[0]?.delta?.tool_calls) { - // Check if the current chunk is the last one in the stream if (choices[0].finish_reason === "stop") { - this.log(`[GptService] In finish reason === STOP`); - if (choices[0]?.delta?.content) { - // this.log( - // `[GptService] Last chunk to emit (non-tool call): ${choices[0].delta.content}` - // ); + contentAccumulator += choices[0].delta.content; this.emit( "gptreply", - choices[0]?.delta?.content, + choices[0].delta.content, true, - interactionCount + interactionCount, + contentAccumulator ); - //accumulate the final content chunk before pushing to user context - contentAccumulator += choices[0].delta.content; } else { - // this.log( - // `[GptService] Emitting empty string as final content chunk (non-tool call) to voxray` - // ); - //If the content is empty (in the case of OpenAI Chat Completions, it will ALWAYS be this) then just send an empty token - this.emit("gptreply", "", true, interactionCount); + this.emit( + "gptreply", + "", + true, + interactionCount, + contentAccumulator + ); } - // if (lastContentChunk) { - // this.emit("gptreply", lastContentChunk, true, interactionCount); - // } this.userContext.push({ role: "assistant", content: contentAccumulator.trim(), }); - - // // Log the full userContext before making the API call - // console.log( - // `[GptService] Full userContext after non tool call: ${JSON.stringify( - // this.userContext, - // null, - // 2 - // )}` - // ); - - this.log( - `[GptService] Final GPT -> user context length: ${this.userContext.length}` + break; + } else if (!choices[0]?.delta?.role && choices[0]?.delta?.content) { + this.emit( + "gptreply", + choices[0].delta.content, + false, + interactionCount ); - - break; // Exit the loop once the final response is complete - } else { - //We only will start emitting chunks after the chunk with ROLE defined "delta":{"role":"assistant","content":"","refusal":null} because there is no content here, also need to make sure finish reason isn't a tool call - if (!choices[0]?.delta?.role && choices[0]?.delta?.content) { - //Log each chunk as it comes in - // this.log( - // `[GptService] Emitting intermediary chunk (non tool call): ${choices[0].delta.content}` - // ); - //emit the chunk knowing its a content chunk and not the final one - this.emit( - "gptreply", - choices[0].delta.content, - false, - interactionCount - ); - //continue accumulating content chunks - contentAccumulator += choices[0].delta.content; - } + contentAccumulator += choices[0].delta.content; } } - - // if (choices[0]?.delta?.refusal !== null) { - // finalMessageObject.refusal = choices[0].delta.refusal; - // } } } catch (error) { this.log(`[GptService] Error during completion: ${error.stack}`); - // Friendly response for any error encountered + // Friendly error message const friendlyMessage = "I apologize, that request might have been a bit too complex. Could you try asking one thing at a time? I'd be happy to help step by step!"; - // Emit the friendly message to the user + // Emit the friendly message this.emit("gptreply", friendlyMessage, true, interactionCount); - // Push the message into the assistant context + // Update user context this.updateUserContext("assistant", friendlyMessage); - - return; // Stop further processing } } } + module.exports = { GptService }; diff --git a/services/text-service.js b/services/text-service.js index 4e4cfe28..dd8c179b 100644 --- a/services/text-service.js +++ b/services/text-service.js @@ -1,3 +1,5 @@ +// text-service.js + const EventEmitter = require("events"); class TextService extends EventEmitter { @@ -6,8 +8,7 @@ class TextService extends EventEmitter { this.ws = websocket; } - sendText(text, last) { - console.log("[TextService] Sending text: ", text, last); + sendText(text, last, fullText = null) { this.ws.send( JSON.stringify({ type: "text", @@ -15,6 +16,9 @@ class TextService extends EventEmitter { last: last, }) ); + if (last && fullText) { + console.log("[TextService] Final Utterance:", fullText); + } } } From 4f589c141a33803ed6edf63661f719ce8e86e7c9 Mon Sep 17 00:00:00 2001 From: pBread <4473570+pBread@users.noreply.github.com> Date: Tue, 5 Nov 2024 11:24:36 -0700 Subject: [PATCH 15/26] added date utilities to mock-database --- data/mock-database.js | 92 +++++++++++++++++++++++++++---------------- 1 file changed, 59 insertions(+), 33 deletions(-) diff --git a/data/mock-database.js b/data/mock-database.js index 8b8c31ab..448423c2 100644 --- a/data/mock-database.js +++ b/data/mock-database.js @@ -2,55 +2,55 @@ const mockDatabase = { availableAppointments: [ // Existing Week { - date: "2024-11-02", + date: makeFutureDate(1), time: "10:00 AM", type: "in-person", apartmentType: "one-bedroom", }, { - date: "2024-11-03", + date: makeFutureDate(2), time: "1:00 PM", type: "in-person", apartmentType: "two-bedroom", }, { - date: "2024-11-04", + date: makeFutureDate(3), time: "11:00 AM", type: "self-guided", apartmentType: "studio", }, { - date: "2024-11-05", + date: makeFutureDate(3), time: "2:00 PM", type: "in-person", apartmentType: "three-bedroom", }, { - date: "2024-11-06", + date: makeFutureDate(3), time: "3:00 PM", type: "self-guided", apartmentType: "one-bedroom", }, { - date: "2024-11-07", + date: makeFutureDate(4), time: "9:00 AM", type: "in-person", apartmentType: "two-bedroom", }, { - date: "2024-11-08", + date: makeFutureDate(5), time: "11:00 AM", type: "in-person", apartmentType: "two-bedroom", }, { - date: "2024-11-09", + date: makeFutureDate(6), time: "10:00 AM", type: "self-guided", apartmentType: "studio", }, { - date: "2024-11-10", + date: makeFutureDate(6), time: "4:00 PM", type: "in-person", apartmentType: "three-bedroom", @@ -58,61 +58,61 @@ const mockDatabase = { // Extended Week 1 { - date: "2024-11-11", + date: makeFutureDate(7), time: "8:00 AM", type: "in-person", apartmentType: "studio", }, { - date: "2024-11-11", + date: makeFutureDate(7), time: "11:00 AM", type: "in-person", apartmentType: "one-bedroom", }, { - date: "2024-11-11", + date: makeFutureDate(7), time: "3:00 PM", type: "self-guided", apartmentType: "two-bedroom", }, { - date: "2024-11-12", + date: makeFutureDate(8), time: "1:00 PM", type: "in-person", apartmentType: "three-bedroom", }, { - date: "2024-11-12", + date: makeFutureDate(8), time: "4:00 PM", type: "in-person", apartmentType: "one-bedroom", }, { - date: "2024-11-13", + date: makeFutureDate(9), time: "9:00 AM", type: "self-guided", apartmentType: "studio", }, { - date: "2024-11-13", + date: makeFutureDate(9), time: "2:00 PM", type: "in-person", apartmentType: "two-bedroom", }, { - date: "2024-11-14", + date: makeFutureDate(10), time: "10:00 AM", type: "in-person", apartmentType: "three-bedroom", }, { - date: "2024-11-14", + date: makeFutureDate(10), time: "4:00 PM", type: "self-guided", apartmentType: "two-bedroom", }, { - date: "2024-11-15", + date: makeFutureDate(11), time: "12:00 PM", type: "in-person", apartmentType: "studio", @@ -120,61 +120,61 @@ const mockDatabase = { // Extended Week 2 { - date: "2024-11-16", + date: makeFutureDate(12), time: "11:00 AM", type: "in-person", apartmentType: "two-bedroom", }, { - date: "2024-11-16", + date: makeFutureDate(12), time: "3:00 PM", type: "in-person", apartmentType: "three-bedroom", }, { - date: "2024-11-17", + date: makeFutureDate(13), time: "9:00 AM", type: "self-guided", apartmentType: "one-bedroom", }, { - date: "2024-11-17", + date: makeFutureDate(13), time: "2:00 PM", type: "in-person", apartmentType: "studio", }, { - date: "2024-11-18", + date: makeFutureDate(14), time: "4:00 PM", type: "in-person", apartmentType: "two-bedroom", }, { - date: "2024-11-18", + date: makeFutureDate(14), time: "12:00 PM", type: "self-guided", apartmentType: "three-bedroom", }, { - date: "2024-11-19", + date: makeFutureDate(15), time: "10:00 AM", type: "in-person", apartmentType: "one-bedroom", }, { - date: "2024-11-19", + date: makeFutureDate(15), time: "3:00 PM", type: "in-person", apartmentType: "two-bedroom", }, { - date: "2024-11-20", + date: makeFutureDate(16), time: "1:00 PM", type: "in-person", apartmentType: "three-bedroom", }, { - date: "2024-11-20", + date: makeFutureDate(16), time: "5:00 PM", type: "self-guided", apartmentType: "studio", @@ -186,7 +186,7 @@ const mockDatabase = { layout: "Studio", squareFeet: 450, rent: 1050, - moveInDate: "2024-11-15", + moveInDate: makeFutureDayOfMonth(1, 1), features: ["1 bathroom", "open kitchen", "private balcony"], petPolicy: "No pets allowed.", fees: { @@ -209,7 +209,7 @@ const mockDatabase = { layout: "One-bedroom", squareFeet: 600, rent: 1200, - moveInDate: "2024-11-20", + moveInDate: makeFutureDayOfMonth(1, 15), features: ["1 bedroom", "1 bathroom", "walk-in closet"], petPolicy: "Cats only. No dogs or any other animals.", fees: { @@ -232,7 +232,7 @@ const mockDatabase = { layout: "Two-bedroom", squareFeet: 950, rent: 1800, - moveInDate: "2024-11-10", + moveInDate: makeFutureDayOfMonth(2, 1), features: ["2 bedrooms", "2 bathrooms", "walk-in closets", "balcony"], petPolicy: "Cats and dogs allowed, but only 1 each.", fees: { @@ -255,7 +255,7 @@ const mockDatabase = { layout: "Three-bedroom", squareFeet: 1200, rent: 2500, - moveInDate: "2024-11-25", + moveInDate: makeFutureDayOfMonth(2, 1), features: [ "3 bedrooms", "2 bathrooms", @@ -284,4 +284,30 @@ const mockDatabase = { }, }; +function makeFutureDate(daysToAdd) { + if (daysToAdd === undefined) daysToAdd = Math.floor(Math.random() * 10) + 1; + + const dt = new Date(); + dt.setDate(dt.getDate() + daysToAdd); + + const year = dt.getFullYear(); + const mo = `${dt.getMonth() + 1}`.padStart(2, "0"); + const da = `${dt.getDate()}`.padStart(2, "0"); + + return `${year}-${mo}-${da}`; +} + +function makeFutureDayOfMonth(monthsToAdd, dayOfMonth = 1) { + if (monthsToAdd === undefined) + monthsToAdd = Math.floor(Math.random() * 3) + 1; + + const dt = new Date(); + dt.setMonth(dt.getMonth() + monthsToAdd); + + const year = dt.getFullYear(); + const mo = `${dt.getMonth() + 1}`.padStart(2, "0"); + const da = `${dt.getDate()}`.padStart(2, "0"); + return `${year}-${mo}-${dayOfMonth}`; +} + module.exports = mockDatabase; From 26b7c72678fc65da5382ae4a6c6ab54455e5532b Mon Sep 17 00:00:00 2001 From: pBread <4473570+pBread@users.noreply.github.com> Date: Tue, 5 Nov 2024 11:36:45 -0700 Subject: [PATCH 16/26] removed deprecated deepgram & elevenlabs references --- .env.example | 10 ++++-- README.md | 88 ++++++++++++++++++++++++++-------------------------- config.js | 3 ++ 3 files changed, 54 insertions(+), 47 deletions(-) diff --git a/.env.example b/.env.example index 8b6150fc..22470d33 100644 --- a/.env.example +++ b/.env.example @@ -12,10 +12,14 @@ SERVER='myserver.website.com' # Service API Keys OPENAI_API_KEY= -DEEPGRAM_API_KEY= -# Deepgram voice model, see more options here: https://developers.deepgram.com/docs/tts-models -VOICE_MODEL=aura-asteria-en +# Supported TTS Providers and Voices: +# all of these: https://www.twilio.com/docs/voice/twiml/say/text-speech#available-voices-and-languages +# plus these... +# Google: en-US-Journey-D, en-US-Journey-F, en-US-Journey-O, en-IN-Journey-D, en-IN-Journey-F, en-GB-Journey-D, en-GB-Journey-F, de-DE-Journey-D, de-DE-Journey-F +# Amazon: Amy-Generative, Matthew-Generative, Ruth-Generative +TTS_PROVIDER='amazon' +TTS_VOICE='aura-asteria-en' # Call Recording # Important: Legal implications of call recording diff --git a/README.md b/README.md index 36c558d4..8aa73db1 100644 --- a/README.md +++ b/README.md @@ -5,12 +5,13 @@ Wouldn't it be neat if you could build an app that allowed you to chat with Chat Twilio gives you a superpower called [Media Streams](https://twilio.com/media-streams). Media Streams provides a Websocket connection to both sides of a phone call. You can get audio streamed to you, process it, and send audio back. This app serves as a demo exploring two services: -- [Deepgram](https://deepgram.com/) for Speech to Text and Text to Speech + - [OpenAI](https://openai.com) for GPT prompt completion These service combine to create a voice application that is remarkably better at transcribing, understanding, and speaking than traditional IVR systems. Features: + - 🏁 Returns responses with low latency, typically 1 second by utilizing streaming. - ❗️ Allows the user to interrupt the GPT assistant and ask a different question. - 📔 Maintains chat history with GPT. @@ -19,21 +20,25 @@ Features: ## Setting up for Development ### Prerequisites + Sign up for the following services and get an API key for each: -- [Deepgram](https://console.deepgram.com/signup) + - [OpenAI](https://platform.openai.com/signup) If you're hosting the app locally, we also recommend using a tunneling service like [ngrok](https://ngrok.com) so that Twilio can forward audio to your app. ### 1. Start Ngrok + Start an [ngrok](https://ngrok.com) tunnel for port `3000`: ```bash ngrok http 3000 ``` + Ngrok will give you a unique URL, like `abc123.ngrok.io`. Copy the URL without http:// or https://. You'll need this URL in the next step. ### 2. Configure Environment Variables + Copy `.env.example` to `.env` and configure the following environment variables: ```bash @@ -43,7 +48,6 @@ SERVER="yourserverdomain.com" # Service API Keys OPENAI_API_KEY="sk-XXXXXX" -DEEPGRAM_API_KEY="YOUR-DEEPGRAM-API-KEY" # Configure your Twilio credentials if you want # to make test calls using '$ npm test'. @@ -54,6 +58,7 @@ TO_NUMBER='+13334445555' ``` ### 3. Install Dependencies with NPM + Install the necessary packages: ```bash @@ -61,10 +66,13 @@ npm install ``` ### 4. Start Your Server in Development Mode + Run the following command: + ```bash npm run dev ``` + This will start your app using `nodemon` so that any changes to your code automatically refreshes and restarts the server. ### 5. Configure an Incoming Phone Number @@ -76,14 +84,16 @@ You can also use the Twilio CLI: ```bash twilio phone-numbers:update +1[your-twilio-number] --voice-url=https://your-server.ngrok.io/incoming ``` + This configuration tells Twilio to send incoming call audio to your app when someone calls your number. The app responds to the incoming call webhook with a [Stream](https://www.twilio.com/docs/voice/twiml/stream) TwiML verb that will connect an audio media stream to your websocket server. ## Application Workflow + CallGPT coordinates the data flow between multiple different services including Deepgram, OpenAI, and Twilio Media Streams: ![Call GPT Flow](https://github.com/twilio-labs/call-gpt/assets/1418949/0b7fcc0b-d5e5-4527-bc4c-2ffb8931139c) - ## Modifying the ChatGPT Context & Prompt + Within `gpt-service.js` you'll find the settings for the GPT's initial context and prompt. For example: ```javascript @@ -92,7 +102,9 @@ this.userContext = [ { "role": "assistant", "content": "Hello! I understand you're looking for a pair of AirPods, is that correct?" }, ], ``` + ### About the `system` Attribute + The `system` attribute is background information for the GPT. As you build your use-case, play around with modifying the context. A good starting point would be to imagine training a new employee on their first day and giving them the basics of how to help a customer. There are some context prompts that will likely be helpful to include by default. For example: @@ -109,17 +121,21 @@ These context items help shape a GPT so that it will act more naturally in a pho The `•` symbol context in particular is helpful for the app to be able to break sentences into natural chunks. This speeds up text-to-speech processing so that users hear audio faster. ### About the `content` Attribute + This attribute is your default conversations starter for the GPT. However, you could consider making it more complex and customized based on personalized user data. In this case, our bot will start off by saying, "Hello! I understand you're looking for a pair of AirPods, is that correct?" ## Using Function Calls with GPT + You can use function calls to interact with external APIs and data sources. For example, your GPT could check live inventory, check an item's price, or place an order. ### How Function Calling Works + Function calling is handled within the `gpt-service.js` file in the following sequence: 1. `gpt-service` loads `function-manifest.js` and requires (imports) all functions defined there from the `functions` directory. Our app will call these functions later when GPT gives us a function name and parameters. + ```javascript tools.forEach((tool) => { const functionName = tool.function.name; @@ -131,35 +147,42 @@ tools.forEach((tool) => { ```javascript const stream = await this.openai.chat.completions.create({ - model: 'gpt-4', + model: "gpt-4", messages: this.userContext, tools, // <-- function-manifest definition stream: true, }); ``` -3. When the GPT responds, it will send us a stream of chunks for the text completion. The GPT will tell us whether each text chunk is something to say to the user, or if it's a tool call that our app needs to execute. This is indicated by the `deltas.tool_calls` key: + +3. When the GPT responds, it will send us a stream of chunks for the text completion. The GPT will tell us whether each text chunk is something to say to the user, or if it's a tool call that our app needs to execute. This is indicated by the `deltas.tool_calls` key: + ```javascript if (deltas.tool_calls) { // handle function calling } ``` + 4. Once we have gathered all of the stream chunks about the tool call, our application can run the actual function code that we imported during the first step. The function name and parameters are provided by GPT: + ```javascript const functionToCall = availableFunctions[functionName]; const functionResponse = functionToCall(functionArgs); ``` + 5. As the final step, we add the function response data into the conversation context like this: ```javascript this.userContext.push({ - role: 'function', + role: "function", name: functionName, content: functionResponse, }); ``` + We then ask the GPT to generate another completion including what it knows from the function call. This allows the GPT to respond to the user with details gathered from the external data source. ### Adding Custom Function Calls + You can have your GPT call external data sources by adding functions to the `/functions` directory. Follow these steps: 1. Create a function (e.g. `checkInventory.js` in `/functions`) @@ -200,19 +223,23 @@ Example function manifest entry: }, } ``` + #### Using `say` in the Function Manifest + The `say` key in the function manifest allows you to define a sentence for the app to speak to the user before calling a function. For example, if a function will take a long time to call you might say "Give me a few moments to look that up for you..." ### Receiving Function Arguments + When ChatGPT calls a function, it will provide an object with multiple attributes as a single argument. The parameters included in the object are based on the definition in your `function-manifest.js` file. In the `checkInventory` example above, `model` is a required argument, so the data passed to the function will be a single object like this: ```javascript { - model: "airpods pro" + model: "airpods pro"; } ``` + For our `placeOrder` function, the arguments passed will look like this: ```javascript @@ -221,57 +248,28 @@ For our `placeOrder` function, the arguments passed will look like this: quantity: 10 } ``` + ### Returning Arguments to GPT -Your function should always return a value: GPT will get confused when the function returns nothing, and may continue trying to call the function expecting an answer. If your function doesn't have any data to return to the GPT, you should still return a response with an instruction like "Tell the user that their request was processed successfully." This prevents the GPT from calling the function repeatedly and wasting tokens. + +Your function should always return a value: GPT will get confused when the function returns nothing, and may continue trying to call the function expecting an answer. If your function doesn't have any data to return to the GPT, you should still return a response with an instruction like "Tell the user that their request was processed successfully." This prevents the GPT from calling the function repeatedly and wasting tokens. Any data that you return to the GPT should match the expected format listed in the `returns` key of `function-manifest.js`. ## Utility Scripts for Placing Calls + The `scripts` directory contains two files that allow you to place test calls: + - `npm run inbound` will place an automated call from a Twilio number to your app and speak a script. You can adjust this to your use-case, e.g. as an automated test. - `npm run outbound` will place an outbound call that connects to your app. This can be useful if you want the app to call your phone so that you can manually test it. -## Using Eleven Labs for Text to Speech -Replace the Deepgram API call and array transformation in tts-service.js with the following call to Eleven Labs. Note that sometimes Eleven Labs will hit a rate limit (especially on the free trial) and return 400 errors with no audio (or a clicking sound). - -``` -try { - const response = await fetch( - `https://api.elevenlabs.io/v1/text-to-speech/21m00Tcm4TlvDq8ikWAM/stream?output_format=ulaw_8000&optimize_streaming_latency=3`, - { - method: 'POST', - headers: { - 'xi-api-key': process.env.XI_API_KEY, - 'Content-Type': 'application/json', - accept: 'audio/wav', - }, - body: JSON.stringify({ - model_id: process.env.XI_MODEL_ID, - text: partialResponse, - }), - } - ); - - if (response.status === 200) { - const audioArrayBuffer = await response.arrayBuffer(); - this.emit('speech', partialResponseIndex, Buffer.from(audioArrayBuffer).toString('base64'), partialResponse, interactionCount); - } else { - console.log('Eleven Labs Error:'); - console.log(response); - } -} catch (err) { - console.error('Error occurred in XI LabsTextToSpeech service'); - console.error(err); -} -``` - - ## Testing with Jest + Repeatedly calling the app can be a time consuming way to test your tool function calls. This project contains example unit tests that can help you test your functions without relying on the GPT to call them. Simple example tests are available in the `/test` directory. To run them, simply run `npm run test`. ## Deploy via Fly.io + Fly.io is a hosting service similar to Heroku that simplifies the deployment process. Given Twilio Media Streams are sent and received from us-east-1, it's recommended to choose Fly's Ashburn, VA (IAD) region. > Deploying to Fly.io is not required to try the app, but can be helpful if your home internet speed is variable. @@ -279,6 +277,7 @@ Fly.io is a hosting service similar to Heroku that simplifies the deployment pro Modify the app name `fly.toml` to be a unique value (this must be globally unique). Deploy the app using the Fly.io CLI: + ```bash fly launch @@ -286,6 +285,7 @@ fly deploy ``` Import your secrets from your .env file to your deployed app: + ```bash fly secrets import < .env ``` diff --git a/config.js b/config.js index 7e10a985..4e50bdc4 100644 --- a/config.js +++ b/config.js @@ -24,5 +24,8 @@ cfg.callerId = process.env.TWILIO_CALLER_ID; cfg.apiKey = process.env.TWILIO_API_KEY; cfg.apiSecret = process.env.TWILIO_API_SECRET; +cfg.ttsProvider = process.env.TTS_PROVIDER ?? "amazon"; +cfg.voiceModel = process.env.VOICE_MODEL ?? "aura-asteria-en"; + // Export configuration object module.exports = cfg; From d1fafb9bc91210e8443117fd562a1571afe0c025 Mon Sep 17 00:00:00 2001 From: pBread <4473570+pBread@users.noreply.github.com> Date: Tue, 5 Nov 2024 12:37:24 -0700 Subject: [PATCH 17/26] changed default voice --- .env.example | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.env.example b/.env.example index 22470d33..bf494d97 100644 --- a/.env.example +++ b/.env.example @@ -19,7 +19,7 @@ OPENAI_API_KEY= # Google: en-US-Journey-D, en-US-Journey-F, en-US-Journey-O, en-IN-Journey-D, en-IN-Journey-F, en-GB-Journey-D, en-GB-Journey-F, de-DE-Journey-D, de-DE-Journey-F # Amazon: Amy-Generative, Matthew-Generative, Ruth-Generative TTS_PROVIDER='amazon' -TTS_VOICE='aura-asteria-en' +TTS_VOICE='Danielle-Neural' # Call Recording # Important: Legal implications of call recording From 2a473741c15d8331ffec1f14c1ee192179f512b1 Mon Sep 17 00:00:00 2001 From: pBread <4473570+pBread@users.noreply.github.com> Date: Tue, 5 Nov 2024 12:40:58 -0700 Subject: [PATCH 18/26] changed tts defaults --- config.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/config.js b/config.js index 4e50bdc4..429851d4 100644 --- a/config.js +++ b/config.js @@ -10,6 +10,8 @@ if (process.env.NODE_ENV !== "test") { // HTTP Port to run our web application cfg.port = process.env.PORT || 3000; +cfg.server = process.env.SERVER; + // Your Twilio account SID and auth token, both found at: // https://www.twilio.com/user/account // @@ -25,7 +27,7 @@ cfg.apiKey = process.env.TWILIO_API_KEY; cfg.apiSecret = process.env.TWILIO_API_SECRET; cfg.ttsProvider = process.env.TTS_PROVIDER ?? "amazon"; -cfg.voiceModel = process.env.VOICE_MODEL ?? "aura-asteria-en"; +cfg.voice = process.env.VOICE_MODEL ?? "Danielle-Neural"; // Export configuration object module.exports = cfg; From fc1ab49af2eb3e670e5a7140f172dfbb31998280 Mon Sep 17 00:00:00 2001 From: pBread <4473570+pBread@users.noreply.github.com> Date: Tue, 5 Nov 2024 12:42:47 -0700 Subject: [PATCH 19/26] updated config naming to match env --- config.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config.js b/config.js index 429851d4..f5c00f2b 100644 --- a/config.js +++ b/config.js @@ -27,7 +27,7 @@ cfg.apiKey = process.env.TWILIO_API_KEY; cfg.apiSecret = process.env.TWILIO_API_SECRET; cfg.ttsProvider = process.env.TTS_PROVIDER ?? "amazon"; -cfg.voice = process.env.VOICE_MODEL ?? "Danielle-Neural"; +cfg.ttsVoice = process.env.TTS_VOICE ?? "Danielle-Neural"; // Export configuration object module.exports = cfg; From 3b7c62bf342198606d2651c04ac5fac202bb3d5a Mon Sep 17 00:00:00 2001 From: pBread <4473570+pBread@users.noreply.github.com> Date: Tue, 5 Nov 2024 12:43:50 -0700 Subject: [PATCH 20/26] updated TWIML from Voxray => ConversationRelay, moved env to config --- app.js | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/app.js b/app.js index b90dd0df..8cdb724b 100644 --- a/app.js +++ b/app.js @@ -1,6 +1,7 @@ -require("dotenv").config(); require("colors"); +const cfg = require("./config"); + const express = require("express"); const ExpressWs = require("express-ws"); @@ -21,16 +22,18 @@ const { const app = express(); ExpressWs(app); -const PORT = process.env.PORT || 3000; - app.post("/incoming", (req, res) => { + console.log(`[App.js] Incoming call webhook`); + try { - // Build the response for Twilio's verb - const response = ` + // Build the response for Twilio's verb + const response = `\ + - + `; + res.type("text/xml"); res.send(response); } catch (err) { @@ -159,6 +162,6 @@ app.ws("/sockets", (ws) => { } }); -app.listen(PORT, () => { - console.log(`Server running on port ${PORT}`); +app.listen(cfg.port, () => { + console.log(`Server running on port ${cfg.port}`); }); From 5ee9010049c8677ca65f945e198dc57b4637ec61 Mon Sep 17 00:00:00 2001 From: pBread <4473570+pBread@users.noreply.github.com> Date: Tue, 5 Nov 2024 12:45:52 -0700 Subject: [PATCH 21/26] added standard urlencoded & json express middleware --- app.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app.js b/app.js index 8cdb724b..6c85ee6c 100644 --- a/app.js +++ b/app.js @@ -19,8 +19,8 @@ const { handleDtmfInput, } = require("./functions/helper-functions"); -const app = express(); -ExpressWs(app); +const { app } = ExpressWs(express()); +app.use(express.urlencoded({ extended: true })).use(express.json()); app.post("/incoming", (req, res) => { console.log(`[App.js] Incoming call webhook`); From 12948c332055ed64b13ea542472fb33bd6071874 Mon Sep 17 00:00:00 2001 From: pBread <4473570+pBread@users.noreply.github.com> Date: Tue, 5 Nov 2024 12:48:09 -0700 Subject: [PATCH 22/26] added callSid to log --- app.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app.js b/app.js index 6c85ee6c..ecedb530 100644 --- a/app.js +++ b/app.js @@ -23,7 +23,7 @@ const { app } = ExpressWs(express()); app.use(express.urlencoded({ extended: true })).use(express.json()); app.post("/incoming", (req, res) => { - console.log(`[App.js] Incoming call webhook`); + console.log(`[App.js] Incoming call webhook, callSid ${req.body?.CallSid}`); try { // Build the response for Twilio's verb From e5b72befaae2f5493cd8b6ab81c8750ddb6f7741 Mon Sep 17 00:00:00 2001 From: pBread <4473570+pBread@users.noreply.github.com> Date: Tue, 5 Nov 2024 12:49:13 -0700 Subject: [PATCH 23/26] moved env reference to cfg --- app.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app.js b/app.js index ecedb530..af8cdc0f 100644 --- a/app.js +++ b/app.js @@ -30,7 +30,7 @@ app.post("/incoming", (req, res) => { const response = `\ - + `; From 0a2de32074b04899dd94f48fadb7a0c5d5bc3f90 Mon Sep 17 00:00:00 2001 From: pBread <4473570+pBread@users.noreply.github.com> Date: Tue, 5 Nov 2024 14:27:25 -0700 Subject: [PATCH 24/26] removed deepgram sdk from dependencies --- package-lock.json | 199 ++-------------------------------------------- package.json | 1 - 2 files changed, 7 insertions(+), 193 deletions(-) diff --git a/package-lock.json b/package-lock.json index 7bd5519a..f57a0482 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,7 +9,6 @@ "version": "1.1.0", "license": "MIT", "dependencies": { - "@deepgram/sdk": "^3.3.4", "colors": "^1.4.0", "dotenv": "^16.3.1", "express": "^4.19.2", @@ -590,33 +589,6 @@ "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", "dev": true }, - "node_modules/@deepgram/captions": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@deepgram/captions/-/captions-1.2.0.tgz", - "integrity": "sha512-8B1C/oTxTxyHlSFubAhNRgCbQ2SQ5wwvtlByn8sDYZvdDtdn/VE2yEPZ4BvUnrKWmsbTQY6/ooLV+9Ka2qmDSQ==", - "dependencies": { - "dayjs": "^1.11.10" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@deepgram/sdk": { - "version": "3.3.4", - "resolved": "https://registry.npmjs.org/@deepgram/sdk/-/sdk-3.3.4.tgz", - "integrity": "sha512-DhkmQ1YhYFCUjdTzmEQZDTI1hFg+7qUtOMGgBknGESCbzEJ2Pt9bXaFk4IU8F9cz1cdGe5yUNyrVVJKQfnr/xg==", - "dependencies": { - "@deepgram/captions": "^1.1.1", - "@types/websocket": "^1.0.9", - "cross-fetch": "^3.1.5", - "deepmerge": "^4.3.1", - "events": "^3.3.0", - "websocket": "^1.0.34" - }, - "engines": { - "node": ">=18.0.0" - } - }, "node_modules/@eslint-community/eslint-utils": { "version": "4.4.0", "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz", @@ -1560,14 +1532,6 @@ "integrity": "sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==", "dev": true }, - "node_modules/@types/websocket": { - "version": "1.0.10", - "resolved": "https://registry.npmjs.org/@types/websocket/-/websocket-1.0.10.tgz", - "integrity": "sha512-svjGZvPB7EzuYS94cI7a+qhwgGU1y89wUgjT6E2wVUfmAGIvRfT7obBvRtnhXCSsoMdlG4gBFGE7MfkIXZLoww==", - "dependencies": { - "@types/node": "*" - } - }, "node_modules/@types/yargs": { "version": "17.0.32", "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.32.tgz", @@ -2053,6 +2017,8 @@ "resolved": "https://registry.npmjs.org/bufferutil/-/bufferutil-4.0.8.tgz", "integrity": "sha512-4T53u4PdgsXqKaIctwF8ifXlRTTmEPJ8iEPWFdGZvcf7sbwYo6FKFEX9eNNAnzFZ7EzJAQ3CJeOtCRA4rDp7Pw==", "hasInstallScript": true, + "optional": true, + "peer": true, "dependencies": { "node-gyp-build": "^4.3.0" }, @@ -2380,14 +2346,6 @@ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", "dev": true }, - "node_modules/cross-fetch": { - "version": "3.1.8", - "resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-3.1.8.tgz", - "integrity": "sha512-cvA+JwZoU0Xq+h6WkMvAUqPEYy92Obet6UdKLfW60qn99ftItKjB5T+BkyWOFWe2pUyfQ+IJHmpOTznqk1M6Kg==", - "dependencies": { - "node-fetch": "^2.6.12" - } - }, "node_modules/cross-spawn": { "version": "7.0.3", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", @@ -2402,18 +2360,6 @@ "node": ">= 8" } }, - "node_modules/d": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/d/-/d-1.0.2.tgz", - "integrity": "sha512-MOqHvMWF9/9MX6nza0KgvFH4HpMU0EF5uUDXqX/BtxtU8NfB0QzRtJ8Oe/6SuS4kbhyzVJwjd97EA4PKrzJ8bw==", - "dependencies": { - "es5-ext": "^0.10.64", - "type": "^2.7.2" - }, - "engines": { - "node": ">=0.12" - } - }, "node_modules/dayjs": { "version": "1.11.11", "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.11.tgz", @@ -2459,6 +2405,7 @@ "version": "4.3.1", "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", + "dev": true, "engines": { "node": ">=0.10.0" } @@ -2642,43 +2589,6 @@ "node": ">= 0.4" } }, - "node_modules/es5-ext": { - "version": "0.10.64", - "resolved": "https://registry.npmjs.org/es5-ext/-/es5-ext-0.10.64.tgz", - "integrity": "sha512-p2snDhiLaXe6dahss1LddxqEm+SkuDvV8dnIQG0MWjyHpcMNfXKPE+/Cc0y+PhxJX3A4xGNeFCj5oc0BUh6deg==", - "hasInstallScript": true, - "dependencies": { - "es6-iterator": "^2.0.3", - "es6-symbol": "^3.1.3", - "esniff": "^2.0.1", - "next-tick": "^1.1.0" - }, - "engines": { - "node": ">=0.10" - } - }, - "node_modules/es6-iterator": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/es6-iterator/-/es6-iterator-2.0.3.tgz", - "integrity": "sha512-zw4SRzoUkd+cl+ZoE15A9o1oQd920Bb0iOJMQkQhl3jNc03YqVjAhG7scf9C5KWRU/R13Orf588uCC6525o02g==", - "dependencies": { - "d": "1", - "es5-ext": "^0.10.35", - "es6-symbol": "^3.1.1" - } - }, - "node_modules/es6-symbol": { - "version": "3.1.4", - "resolved": "https://registry.npmjs.org/es6-symbol/-/es6-symbol-3.1.4.tgz", - "integrity": "sha512-U9bFFjX8tFiATgtkJ1zg25+KviIXpgRvRHS8sau3GfhVzThRQrOeksPeT0BWW2MNZs1OEWJ1DPXOQMn0KKRkvg==", - "dependencies": { - "d": "^1.0.2", - "ext": "^1.7.0" - }, - "engines": { - "node": ">=0.12" - } - }, "node_modules/escalade": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.2.tgz", @@ -2837,20 +2747,6 @@ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", "dev": true }, - "node_modules/esniff": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/esniff/-/esniff-2.0.1.tgz", - "integrity": "sha512-kTUIGKQ/mDPFoJ0oVfcmyJn4iBDRptjNVIzwIFR7tqWXdVI9xfA2RMwY/gbSpJG3lkdWNEjLap/NqVHZiJsdfg==", - "dependencies": { - "d": "^1.0.1", - "es5-ext": "^0.10.62", - "event-emitter": "^0.3.5", - "type": "^2.7.2" - }, - "engines": { - "node": ">=0.10" - } - }, "node_modules/espree": { "version": "9.6.1", "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", @@ -2931,15 +2827,6 @@ "node": ">= 0.6" } }, - "node_modules/event-emitter": { - "version": "0.3.5", - "resolved": "https://registry.npmjs.org/event-emitter/-/event-emitter-0.3.5.tgz", - "integrity": "sha512-D9rRn9y7kLPnJ+hMq7S/nhvoKwwvVJahBi2BPmx3bvbsEdK3W9ii8cBSGjP+72/LnM4n6fo3+dkCX5FeTQruXA==", - "dependencies": { - "d": "1", - "es5-ext": "~0.10.14" - } - }, "node_modules/event-target-shim": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", @@ -2948,14 +2835,6 @@ "node": ">=6" } }, - "node_modules/events": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", - "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", - "engines": { - "node": ">=0.8.x" - } - }, "node_modules/execa": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", @@ -3072,14 +2951,6 @@ "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" }, - "node_modules/ext": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/ext/-/ext-1.7.0.tgz", - "integrity": "sha512-6hxeJYaL110a9b5TEJSj0gojyHQAmA2ch5Os+ySCiA1QGdS697XWY1pzsrSjqA9LDEEgdB/KypIlR59RcLuHYw==", - "dependencies": { - "type": "^2.7.2" - } - }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", @@ -3761,11 +3632,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/is-typedarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", - "integrity": "sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA==" - }, "node_modules/isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", @@ -5544,11 +5410,6 @@ "node": ">= 0.6" } }, - "node_modules/next-tick": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/next-tick/-/next-tick-1.1.0.tgz", - "integrity": "sha512-CXdUiJembsNjuToQvxayPZF9Vqht7hewsvy2sOWafLvi2awflj9mOC6bHIg50orX8IJvWKY9wYQ/zB2kogPslQ==" - }, "node_modules/node-domexception": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz", @@ -5590,6 +5451,8 @@ "version": "4.8.1", "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.8.1.tgz", "integrity": "sha512-OSs33Z9yWr148JZcbZd5WiAXhh/n9z8TxQcdMhIOlpN9AhWpLfvVFO73+m77bBABQMaY9XSvIa+qk0jlI7Gcaw==", + "optional": true, + "peer": true, "bin": { "node-gyp-build": "bin.js", "node-gyp-build-optional": "optional.js", @@ -6704,11 +6567,6 @@ "node": ">=14.0" } }, - "node_modules/type": { - "version": "2.7.3", - "resolved": "https://registry.npmjs.org/type/-/type-2.7.3.tgz", - "integrity": "sha512-8j+1QmAbPvLZow5Qpi6NCaN8FB60p/6x8/vfNqOk/hC+HuvFZhL4+WfekuhQLiqFZXOgQdrs3B+XxEmCc6b3FQ==" - }, "node_modules/type-check": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", @@ -6754,14 +6612,6 @@ "node": ">= 0.6" } }, - "node_modules/typedarray-to-buffer": { - "version": "3.1.5", - "resolved": "https://registry.npmjs.org/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz", - "integrity": "sha512-zdu8XMNEDepKKR+XYOXAVPtWui0ly0NtohUscw+UmaHiAWT8hrV1rr//H6V+0DvJ3OQ19S979M0laLfX8rm82Q==", - "dependencies": { - "is-typedarray": "^1.0.0" - } - }, "node_modules/undefsafe": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.5.tgz", @@ -6834,6 +6684,8 @@ "resolved": "https://registry.npmjs.org/utf-8-validate/-/utf-8-validate-5.0.10.tgz", "integrity": "sha512-Z6czzLq4u8fPOyx7TU6X3dvUZVvoJmxSQ+IcrlmagKhilxlhZgxPK6C5Jqbkw1IDUmFTM+cz9QDnnLTwDz/2gQ==", "hasInstallScript": true, + "optional": true, + "peer": true, "dependencies": { "node-gyp-build": "^4.3.0" }, @@ -6916,35 +6768,6 @@ "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==" }, - "node_modules/websocket": { - "version": "1.0.35", - "resolved": "https://registry.npmjs.org/websocket/-/websocket-1.0.35.tgz", - "integrity": "sha512-/REy6amwPZl44DDzvRCkaI1q1bIiQB0mEFQLUrhz3z2EK91cp3n72rAjUlrTP0zV22HJIUOVHQGPxhFRjxjt+Q==", - "dependencies": { - "bufferutil": "^4.0.1", - "debug": "^2.2.0", - "es5-ext": "^0.10.63", - "typedarray-to-buffer": "^3.1.5", - "utf-8-validate": "^5.0.2", - "yaeti": "^0.0.6" - }, - "engines": { - "node": ">=4.0.0" - } - }, - "node_modules/websocket/node_modules/debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "dependencies": { - "ms": "2.0.0" - } - }, - "node_modules/websocket/node_modules/ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" - }, "node_modules/whatwg-url": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", @@ -7084,14 +6907,6 @@ "node": ">=10" } }, - "node_modules/yaeti": { - "version": "0.0.6", - "resolved": "https://registry.npmjs.org/yaeti/-/yaeti-0.0.6.tgz", - "integrity": "sha512-MvQa//+KcZCUkBTIC9blM+CU9J2GzuTytsOUwf2lidtvkx/6gnEp1QvJv34t9vdjhFmha/mUiNDbN0D0mJWdug==", - "engines": { - "node": ">=0.10.32" - } - }, "node_modules/yallist": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", diff --git a/package.json b/package.json index 595b0a29..cb4fd812 100644 --- a/package.json +++ b/package.json @@ -14,7 +14,6 @@ "author": "Charlie Weems", "license": "MIT", "dependencies": { - "@deepgram/sdk": "^3.3.4", "colors": "^1.4.0", "dotenv": "^16.3.1", "express": "^4.19.2", From 96bdb8d42823a7f144d7d46bd9edb32f4e18ea68 Mon Sep 17 00:00:00 2001 From: pBread <4473570+pBread@users.noreply.github.com> Date: Tue, 5 Nov 2024 14:28:19 -0700 Subject: [PATCH 25/26] removed package scripts that aren't used --- package.json | 2 -- 1 file changed, 2 deletions(-) diff --git a/package.json b/package.json index cb4fd812..0e374706 100644 --- a/package.json +++ b/package.json @@ -4,8 +4,6 @@ "description": "", "main": "index.js", "scripts": { - "inbound": "node ./scripts/inbound-call.js", - "outbound": "node ./scripts/outbound-call.js", "test": "jest", "dev": "nodemon app.js", "start": "node app.js" From 195a530d02b9a9eb7deac835b55427224a7944f4 Mon Sep 17 00:00:00 2001 From: pBread <4473570+pBread@users.noreply.github.com> Date: Tue, 5 Nov 2024 14:28:32 -0700 Subject: [PATCH 26/26] minor, sorted package scripts --- package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 0e374706..fa70d2ca 100644 --- a/package.json +++ b/package.json @@ -4,9 +4,9 @@ "description": "", "main": "index.js", "scripts": { - "test": "jest", "dev": "nodemon app.js", - "start": "node app.js" + "start": "node app.js", + "test": "jest" }, "keywords": [], "author": "Charlie Weems",