Skip to content

Commit

Permalink
Voice API with new interface (#855)
Browse files Browse the repository at this point in the history
* Task namespace with new interface

* taskworker include

* extend task from applyeventlisteners

* base namespace class to handle the listen method

* topic attach to event name

* type update

* remove older Task api

* refactor and e2e test case

* Voice API with new interface

* handle call.playback listeners with all the methods

* run workers through methods

* playback events with e2e test cases

* remove old call playback class

* fix test file names

* improve playback tests

* rename voice playback tests

* voice call record events with e2e test cases

* fix playback and record types

* implement call.prompt with playback

* test utility add

* e2e test cases for call prompt

* call collect with e2e test cases

* Call tap with e2e test cases

* Call Detect API with e2e test cases

* remove old voice detect test

* voice call connect api

* update voice pass test with new interface

* improve base and listener class for instances

* include unit test cases for call apis

* voice stack test update

* call connect implement with e2e test case

* enable ws logs for task test

* update voice playground with the new interface

* minimize race condition in playback and recording e2e test cases

* minimize race condition for collect and detect e2e

* improve call state events logic

* fix voice unit test

* enable ws logs for voice test

* fix call connect bug

* remove unused voice calling worker

* enable ws logs for voice call collect

* improve collect and detect e2e test cases

* include changeset

* Update packages/realtime-api/src/BaseNamespace.ts

Co-authored-by: Edoardo Gallo <edoardo@signalwire.com>

* Update packages/realtime-api/src/ListenSubscriber.ts

Co-authored-by: Edoardo Gallo <edoardo@signalwire.com>

* Update packages/realtime-api/src/task/Task.ts

Co-authored-by: Edoardo Gallo <edoardo@signalwire.com>

* add addToListenerMap method for consistency

* Revert "Update packages/realtime-api/src/ListenSubscriber.ts"

This reverts commit 69df536.

* update payload set and extends base calls with EventEmitter

* protect event emitter methods

* improve call collect test

* improve voice record e2e test

---------

Co-authored-by: Edoardo Gallo <edoardo@signalwire.com>
  • Loading branch information
iAmmar7 and Edoardo Gallo committed Dec 5, 2023
1 parent 2ff0247 commit 84385a7
Show file tree
Hide file tree
Showing 89 changed files with 7,037 additions and 3,678 deletions.
95 changes: 95 additions & 0 deletions .changeset/cuddly-carrots-bathe.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
---
'@signalwire/realtime-api': major
'@signalwire/core': major
---

New interface for Voice APIs

The new interface contains a single SW client with Chat and PubSub namespaces
```javascript
import { SignalWire } from '@signalwire/realtime-api'

(async () => {
const client = await SignalWire({
host: process.env.HOST,
project: process.env.PROJECT,
token: process.env.TOKEN,
})

const unsubVoiceOffice = await client.voice.listen({
topics: ['office'],
onCallReceived: async (call) => {
try {
await call.answer()

const unsubCall = await call.listen({
onStateChanged: (call) => {},
onPlaybackUpdated: (playback) => {},
onRecordingStarted: (recording) => {},
onCollectInputStarted: (collect) => {},
onDetectStarted: (detect) => {},
onTapStarted: (tap) => {},
onPromptEnded: (prompt) => {}
// ... more call listeners can be attached here
})

// ...

await unsubCall()
} catch (error) {
console.error('Error answering inbound call', error)
}
}
})

const call = await client.voice.dialPhone({
to: process.env.VOICE_DIAL_TO_NUMBER as string,
from: process.env.VOICE_DIAL_FROM_NUMBER as string,
timeout: 30,
listen: {
onStateChanged: async (call) => {
// When call ends; unsubscribe all listeners and disconnect the client
if (call.state === 'ended') {
await unsubVoiceOffice()

await unsubVoiceHome()

await unsubPlay()

client.disconnect()
}
},
onPlaybackStarted: (playback) => {},
},
})

const unsubCall = await call.listen({
onPlaybackStarted: (playback) => {},
onPlaybackEnded: (playback) => {
// This will never run since we unsubscribe this listener before the playback stops
},
})

// Play an audio
const play = await call.playAudio({
url: 'https://cdn.signalwire.com/default-music/welcome.mp3',
listen: {
onStarted: async (playback) => {
await unsubCall()

await play.stop()
},
},
})

const unsubPlay = await play.listen({
onStarted: (playback) => {
// This will never run since this listener is attached after the call.play has started
},
onEnded: async (playback) => {
await call.hangup()
},
})

})
```
82 changes: 44 additions & 38 deletions internal/e2e-realtime-api/src/task.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { randomUUID } from 'node:crypto'
import tap from 'tap'
import { SignalWire } from '@signalwire/realtime-api'
import { createTestRunner } from './utils'

Expand All @@ -9,57 +10,76 @@ const handler = () => {
host: process.env.RELAY_HOST || 'relay.swire.io',
project: process.env.RELAY_PROJECT as string,
token: process.env.RELAY_TOKEN as string,
debug: {
logWsTraffic: true,
},
})

const homeTopic = `home-${randomUUID()}`
const officeTopic = `office-${randomUUID()}`

const firstPayload = {
id: Date.now(),
id: 1,
topic: homeTopic,
}
const secondPayload = {
id: Date.now(),
id: 2,
topic: homeTopic,
}
const thirdPayload = {
id: Date.now(),
id: 3,
topic: officeTopic,
}

let counter = 0
let unsubHomeOfficeCount = 0

const unsubHomeOffice = await client.task.listen({
topics: [homeTopic, officeTopic],
onTaskReceived: (payload) => {
onTaskReceived: async (payload) => {
if (
payload.topic !== homeTopic ||
payload.id !== firstPayload.id ||
payload.id !== secondPayload.id ||
counter > 3
(payload.id !== firstPayload.id && payload.id !== secondPayload.id)
) {
console.error('Invalid payload on `home` context', payload)
return reject(4)
tap.notOk(
payload,
"Message received on wrong ['home', 'office'] listener"
)
}

tap.ok(payload, 'Message received on ["home", "office"] topics')
unsubHomeOfficeCount++

if (unsubHomeOfficeCount === 2) {
await unsubHomeOffice()

// This message should not reach the listener since we have unsubscribed
await client.task.send({
topic: homeTopic,
message: secondPayload,
})

await client.task.send({
topic: officeTopic,
message: thirdPayload,
})
}
counter++
},
})

const unsubOffice = await client.task.listen({
topics: [officeTopic],
onTaskReceived: (payload) => {
if (
payload.topic !== officeTopic ||
payload.id !== thirdPayload.id ||
counter > 3
) {
console.error('Invalid payload on `home` context', payload)
return reject(4)
onTaskReceived: async (payload) => {
if (payload.topic !== officeTopic || payload.id !== thirdPayload.id) {
tap.notOk(payload, "Message received on wrong ['office'] listener")
}
counter++

if (counter === 3) {
return resolve(0)
}
tap.ok(payload, 'Message received on ["office"] topics')

await unsubOffice()

client.disconnect()

return resolve(0)
},
})

Expand All @@ -72,21 +92,6 @@ const handler = () => {
topic: homeTopic,
message: secondPayload,
})

await unsubHomeOffice()

// This message should not reach the listener
await client.task.send({
topic: homeTopic,
message: secondPayload,
})

await client.task.send({
topic: officeTopic,
message: thirdPayload,
})

await unsubOffice()
} catch (error) {
console.log('Task test error', error)
reject(error)
Expand All @@ -98,6 +103,7 @@ async function main() {
const runner = createTestRunner({
name: 'Task E2E',
testHandler: handler,
executionTime: 30_000,
})

await runner.run()
Expand Down
116 changes: 114 additions & 2 deletions internal/e2e-realtime-api/src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ export const createTestRunner = ({
if (exitCode === 0) {
console.log(`Test Runner ${name} Passed!`)
if (!exitOnSuccess) {
return;
return
}
} else {
console.log(`Test Runner ${name} finished with exitCode: ${exitCode}`)
Expand All @@ -87,7 +87,7 @@ export const createTestRunner = ({
name: `d-app-${uuid}`,
identifier: uuid,
call_handler: 'relay_context',
call_relay_context:`d-app-ctx-${uuid}`,
call_relay_context: `d-app-ctx-${uuid}`,
})
}
const exitCode = await testHandler(params)
Expand Down Expand Up @@ -261,3 +261,115 @@ const deleteDomainApp = ({ id }: DeleteDomainAppParams): Promise<void> => {
req.end()
})
}

export const CALL_PROPS = [
'id',
'callId',
'nodeId',
'state',
'callState',
// 'tag', // Inbound calls does not have tags
'device',
'type',
'from',
'to',
'headers',
'active',
'connected',
'direction',
// 'context', // Outbound calls do not have context
// 'connectState', // Undefined unless peer call
// 'peer', // Undefined unless peer call
'hangup',
'pass',
'answer',
'play',
'playAudio',
'playSilence',
'playRingtone',
'playTTS',
'record',
'recordAudio',
'prompt',
'promptAudio',
'promptRingtone',
'promptTTS',
'sendDigits',
'tap',
'tapAudio',
'connect',
'connectPhone',
'connectSip',
'disconnect',
'waitForDisconnected',
'disconnected',
'detect',
'amd',
'detectFax',
'detectDigit',
'collect',
'waitFor',
]

export const CALL_PLAYBACK_PROPS = [
'id',
'callId',
'nodeId',
'controlId',
'state',
'pause',
'resume',
'stop',
'setVolume',
'ended',
]

export const CALL_RECORD_PROPS = [
'id',
'callId',
'nodeId',
'controlId',
'state',
// 'url', // Sometimes server does not return it
'record',
'stop',
'ended',
]

export const CALL_PROMPT_PROPS = [
'id',
'callId',
'nodeId',
'controlId',
'stop',
'setVolume',
'ended',
]

export const CALL_COLLECT_PROPS = [
'id',
'callId',
'nodeId',
'controlId',
'stop',
'startInputTimers',
'ended',
]

export const CALL_TAP_PROPS = [
'id',
'callId',
'nodeId',
'controlId',
'stop',
'ended',
]

export const CALL_DETECT_PROPS = [
'id',
'callId',
'nodeId',
'controlId',
'stop',
'ended',
]
Loading

0 comments on commit 84385a7

Please sign in to comment.