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: {