diff --git a/loadtest-example.html b/loadtest-example.html
new file mode 100644
index 0000000..d55c3c7
--- /dev/null
+++ b/loadtest-example.html
@@ -0,0 +1,49 @@
+
+
+
+ WebRTC Reference Client
+
+
+
+
+
+
+
+
+
+ VIER Cognitive Voice Gateway
+ WebRTC Reference Client
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/common-example.ts b/src/common-example.ts
index 490d1e7..22d3e69 100644
--- a/src/common-example.ts
+++ b/src/common-example.ts
@@ -130,4 +130,71 @@ export function concurrencyLimitedWorkQueue(maxConcurrency: number): WorkQueu
return new Promise(resolve => emptyPromises.push(resolve))
}
}
+}
+
+export function getDialogId(headers: HeaderList): string | undefined {
+ const dialogIds = headers
+ .filter(([name,]) => name.toLowerCase() == "x-vier-dialogid")
+ .map(([, value]) => value)
+ if (dialogIds.length > 0) {
+ return dialogIds[0]
+ }
+ return undefined
+}
+
+export interface BaseDialogDataEntry {
+ type: string,
+ timestamp: number
+}
+
+export interface StartDialogDataEntry extends BaseDialogDataEntry {
+ type: "Start"
+ customSipHeaders: {[name: string]: Array}
+}
+
+export interface SynthesisDialogDataEntry extends BaseDialogDataEntry {
+ type: "Synthesis"
+ text: string
+ plainText: string
+ vendor: string
+ language: string
+}
+
+export interface ToneDialogDataEntry extends BaseDialogDataEntry {
+ type: "Tone"
+ tone: string
+ triggeredBargeIn: boolean
+}
+
+export interface TranscriptionDialogDataEntry extends BaseDialogDataEntry {
+ type: "Transcription"
+ text: string
+ confidence: number
+ vendor: string
+ language: string
+ triggeredBargeIn: boolean
+}
+
+export interface EndDialogDataEntry extends BaseDialogDataEntry {
+ type: "End"
+ reason: string
+}
+
+export type DialogDataEntry = StartDialogDataEntry | SynthesisDialogDataEntry | ToneDialogDataEntry | TranscriptionDialogDataEntry | EndDialogDataEntry | BaseDialogDataEntry
+
+export interface DialogDataResponse {
+ dialogId: string
+ callId?: string
+ data: Array
+}
+
+export async function fetchDialogData(environment: string, resellerToken: string, dialogId: string) {
+ const response = await fetch(`${environment}/v1/dialog/${resellerToken}/${dialogId}`)
+ return await response.json() as DialogDataResponse
+}
+
+export function delay(millis: number): Promise {
+ return new Promise((resolve) => {
+ setTimeout(resolve, millis)
+ })
}
\ No newline at end of file
diff --git a/src/loadtest-example.ts b/src/loadtest-example.ts
new file mode 100644
index 0000000..c368e4e
--- /dev/null
+++ b/src/loadtest-example.ts
@@ -0,0 +1,133 @@
+import {
+ CreateCallOptions,
+ DEFAULT_ICE_GATHERING_TIMEOUT,
+ fetchWebRtcAuthDetails,
+ setupSipClient,
+} from './client'
+import {
+ DEFAULT_TIMEOUT,
+ enableMediaStreamAudioInChrome,
+} from './controls'
+import {
+ concurrencyLimitedWorkQueue,
+ delay,
+ fetchDialogData,
+ getAndDisplayEnvironmentFromQuery,
+ getDialogId,
+ updateQueryParameter,
+} from './common-example'
+
+async function performCall(
+ environment: string,
+ resellerToken: string,
+ destination: string,
+ audioContext: AudioContext,
+ waitTimeBeforeDrop: number,
+ waitTimeAfterDrop: number,
+): Promise {
+ const details = await fetchWebRtcAuthDetails(environment, resellerToken)
+ const telephony = await setupSipClient(details)
+ const virtualMic = audioContext.createMediaStreamDestination()
+ const options: CreateCallOptions = {
+ timeout: DEFAULT_TIMEOUT,
+ iceGatheringTimeout: DEFAULT_ICE_GATHERING_TIMEOUT,
+ mediaStream: virtualMic.stream,
+ }
+ const callApi = await telephony.createCall(destination, options)
+ enableMediaStreamAudioInChrome(callApi.media)
+ const remoteAudio = audioContext.createMediaStreamSource(callApi.media)
+ const virtualSpeaker = audioContext.createMediaStreamDestination()
+ remoteAudio.connect(virtualSpeaker)
+ const dialogId = getDialogId(callApi.acceptHeaders)!
+
+ await delay(waitTimeBeforeDrop)
+ callApi.drop()
+
+ await callApi.callCompletion
+
+ await delay(waitTimeAfterDrop)
+ const dialog = await fetchDialogData(environment, resellerToken, dialogId)
+ let synthesisCount = 0
+ for (let datum of dialog.data) {
+ if (datum.type === "Synthesis") {
+ synthesisCount++
+ }
+ }
+ return synthesisCount
+}
+
+async function performAllCalls(
+ environment: string,
+ resellerToken: string,
+ destination: string,
+ audioContext: AudioContext,
+ numberOfCalls: number,
+ maxParallelism: number,
+ waitTimeBeforeDrop: number,
+ waitTimeAfterDrop: number,
+): Promise<[number, number, number, number]> {
+ let noGreeting: number = 0
+ let singleGreeting: number = 0
+ let multiGreeting: number = 0
+ let failedCalls: number = 0
+
+ const workQueue = concurrencyLimitedWorkQueue(maxParallelism)
+
+ for (let i = 0; i < numberOfCalls; i++) {
+ workQueue.submit(async () => {
+ try {
+ const greetingCount = await performCall(environment, resellerToken, destination, audioContext, waitTimeBeforeDrop, waitTimeAfterDrop)
+ if (greetingCount === 0) {
+ noGreeting++
+ } else if (greetingCount === 1) {
+ singleGreeting++
+ } else {
+ multiGreeting++
+ }
+ } catch (e) {
+ failedCalls++
+ console.error("Call failed!", e)
+ }
+ })
+ }
+
+ await workQueue.awaitEmpty()
+ return [noGreeting, singleGreeting, multiGreeting, failedCalls]
+}
+
+window.addEventListener('DOMContentLoaded', () => {
+ const environment = getAndDisplayEnvironmentFromQuery()
+
+ const query = new URLSearchParams(location.search)
+ document.querySelectorAll('input[name]').forEach(element => {
+ const key = `form.${element.name}`
+ const queryValue = query.get(element.name)
+ const existingValue = localStorage.getItem(key)
+ element.addEventListener('change', () => {
+ localStorage.setItem(key, element.value)
+ updateQueryParameter(element.name, element.value)
+ })
+ if (queryValue) {
+ element.value = queryValue
+ } else if (existingValue) {
+ element.value = existingValue
+ updateQueryParameter(element.name, existingValue)
+ }
+ })
+
+ const startCallsButton = document.getElementById('start-calls')! as HTMLButtonElement
+
+ startCallsButton.addEventListener('click', e => {
+ e.preventDefault()
+ const audioContext = new AudioContext()
+
+ const resellerToken = document.querySelector("input#reseller-token")!!.value
+ const destination = document.querySelector("input#destination")!!.value
+
+ performAllCalls(environment, resellerToken, destination, audioContext, 6, 2, 5000, 2000)
+ .then(([noGreeting, singleGreeting, multiGreeting, failedCalls]) => {
+ alert(`Calls without greeting: ${noGreeting}\nCalls with a single greeting: ${singleGreeting}\nCalls with multiple greetings: ${multiGreeting}\nFailed calls: ${failedCalls}`)
+ })
+
+ })
+})
\ No newline at end of file
diff --git a/src/web-call-example.ts b/src/web-call-example.ts
index 312d823..fcdb841 100644
--- a/src/web-call-example.ts
+++ b/src/web-call-example.ts
@@ -7,6 +7,7 @@ import {
import {
getAndDisplayEnvironmentFromQuery,
getCustomSipHeadersFromQuery,
+ getDialogId,
updateQueryParameter,
} from './common-example'
@@ -38,11 +39,9 @@ window.addEventListener('DOMContentLoaded', () => {
connectButton.addEventListener('call_accepted', (e) => {
const headers = e.detail.acceptHeaders
console.log("Headers received from accept:", headers)
- const dialogIds = headers
- .filter(([name,]) => name.toLowerCase() == "x-cvg-dialogid")
- .map(([, value]) => value)
- if (dialogIds.length > 0) {
- console.log(`DialogId: ${dialogIds[0]}`)
+ const dialogId = getDialogId(headers)
+ if (dialogId) {
+ console.log(`DialogId: ${dialogId}`)
}
})
diff --git a/src/webaudio-example.ts b/src/webaudio-example.ts
index ae73147..aa9b117 100644
--- a/src/webaudio-example.ts
+++ b/src/webaudio-example.ts
@@ -131,7 +131,7 @@ function performCall(
})
}
-function performAllCalls(
+async function performAllCalls(
environment: string,
resellerToken: string,
destination: string,
@@ -144,22 +144,19 @@ function performAllCalls(
const failed: Array<[DecodedAudioFile, any]> = []
const workQueue = concurrencyLimitedWorkQueue(maxParallelism)
for (let file of files) {
- workQueue.submit(() => {
- return performCall(environment, resellerToken, destination, extraCustomSipHeaders, audioContext, file)
- .then(() => {
- console.info(`Call completed for: ${file.toString()}`)
- completed.push(file)
- })
- .catch(e => {
- console.info(`Call failed for: ${file.toString()}`, e)
- failed.push([file, e])
- })
+ workQueue.submit(async () => {
+ try {
+ await performCall(environment, resellerToken, destination, extraCustomSipHeaders, audioContext, file)
+ console.info(`Call completed for: ${file.toString()}`)
+ completed.push(file)
+ } catch (e) {
+ console.info(`Call failed for: ${file.toString()}`, e)
+ failed.push([file, e])
+ }
})
}
-
- return workQueue.awaitEmpty().then(() => {
- return [completed, failed]
- })
+ await workQueue.awaitEmpty()
+ return [completed, failed]
}
function filesDropped(files: FileList): Promise> {
diff --git a/webpack.config.js b/webpack.config.js
index 83c24ed..f859d67 100644
--- a/webpack.config.js
+++ b/webpack.config.js
@@ -6,6 +6,7 @@ module.exports = {
entry: {
"web-call-example": "./src/web-call-example.ts",
"webaudio-example": "./src/webaudio-example.ts",
+ "loadtest-example": "./src/loadtest-example.ts",
"webcomponent": "./src/webcomponent.ts",
},
output: {