diff --git a/server/index.ts b/server/index.ts index dfd44ac..18259ea 100644 --- a/server/index.ts +++ b/server/index.ts @@ -28,6 +28,14 @@ app.use((req, res, next) => { const PROD_VOICE_URL = 'https://voice.bandwidth.com/api/v2'; +// Sentinel "to" value that signals a PlayAudio request rather than a real +// endpoint-to-endpoint bridge. Sent from the frontend with a dummy toType of +// ENDPOINT, since PlayAudio has no bridge peer (see pv-media-server-sidecar#196, +// which uses a fixed dummy resourceId of "playAudio" with resourceType ENDPOINT +// for the same reason). +const PLAY_AUDIO_TARGET = 'playAudio'; +const DEFAULT_PLAY_AUDIO_URL = 'https://download.samplelib.com/mp3/sample-30s.mp3'; + function getEnvVars() { const env = process.env; const hasUserPass = !!env.BW_USERNAME && !!env.BW_PASSWORD; @@ -339,6 +347,13 @@ app.post('/callbacks/bandwidth', async (req: Request, res: Response) => { } return res.type('application/xml').send(``); } + if (toType === 'ENDPOINT' && to === PLAY_AUDIO_TARGET) { + console.log(`Playing demo audio for endpoint ${endpointId}`); + return res.type('application/xml').send(` + + ${DEFAULT_PLAY_AUDIO_URL} +`); + } if (toType === 'ENDPOINT') { // Endpoint-to-endpoint has no PSTN leg to place. We answer the // request with BXML containing a verb targeting the diff --git a/src/components/CallController.tsx b/src/components/CallController.tsx index 66c756f..9d0bf85 100644 --- a/src/components/CallController.tsx +++ b/src/components/CallController.tsx @@ -67,6 +67,15 @@ function NumberInput ({ onChange, value }: {onChange: ChangeEventHandler, value: } +// "Play Audio" isn't a real EndpointType from the SDK - there's no bridge peer to +// dial. It's sent to the server as a dummy EndpointType.ENDPOINT request with a +// fixed sentinel target, which tells the server to respond with a +// BXML verb instead of bridging endpoints (see server/index.ts, and +// pv-media-server-sidecar#196 which uses the same dummy-ENDPOINT convention). +const PLAY_AUDIO_TYPE = 'PLAY_AUDIO'; +const PLAY_AUDIO_TARGET = 'playAudio'; +type DestinationType = EndpointType | typeof PLAY_AUDIO_TYPE; + function CallController({bandwidthRtcClient, readyMetadata, inCall, setInCall, connected}: {bandwidthRtcClient: BandwidthRtc, readyMetadata: ReadyMetadata, inCall: boolean, setInCall: (inCall: boolean) => void, connected: boolean} ) { if (!bandwidthRtcClient) { throw new Error("webrtcClient is required"); @@ -78,7 +87,7 @@ function CallController({bandwidthRtcClient, readyMetadata, inCall, setInCall, c const [destNumber, setDestNumber] = useState(''); const [callStatus, setCallStatus] = useState('Select Connection Target'); const [endpointTarget, setEndpointTarget] = useState(''); - const [endpointType, setEndpointType] = useState(EndpointType.PHONE_NUMBER); + const [endpointType, setEndpointType] = useState(EndpointType.PHONE_NUMBER); const [dtmfDuration, setDtmfDuration] = useState(300); const [dtmfSequence, setDtmfSequence] = useState(''); @@ -114,7 +123,9 @@ function CallController({bandwidthRtcClient, readyMetadata, inCall, setInCall, c // frozen during a call (the selector is disabled), so it still reflects // how this call was dialed. let result; - if (endpointType === EndpointType.ENDPOINT) { + if (endpointType === PLAY_AUDIO_TYPE) { + result = await bandwidthRtcClient.hangupConnection(PLAY_AUDIO_TARGET, EndpointType.ENDPOINT) + } else if (endpointType === EndpointType.ENDPOINT) { result = await bandwidthRtcClient.hangupConnection(endpointTarget, EndpointType.ENDPOINT) } else { // Ensure E.164 format: clean digits and add '+' @@ -153,23 +164,39 @@ function CallController({bandwidthRtcClient, readyMetadata, inCall, setInCall, c } } + const handleDialPlayAudio = async () => { + setCallStatus('Calling...'); + let result = await bandwidthRtcClient.requestOutboundConnection(PLAY_AUDIO_TARGET, EndpointType.ENDPOINT) + if (result.accepted) { + setCallStatus('Ringing'); + setInCall(true); + } else { + setCallStatus('Declined'); + } + } + const handleDial = async () => { if (endpointType === EndpointType.PHONE_NUMBER) { await handleDialPhone() } else if (endpointType === EndpointType.ENDPOINT) { await handleDialEndpoint() + } else if (endpointType === PLAY_AUDIO_TYPE) { + await handleDialPlayAudio() } } // A dial is allowed only when the destination for the selected type is filled - // in. CALL_ID is not implemented yet, so it can never be dialed. - const hasDestination = - endpointType === EndpointType.PHONE_NUMBER - ? destNumber.replace(/[^\d]/g, '').length > 0 - : endpointType === EndpointType.ENDPOINT - ? endpointTarget.trim().length > 0 - : false; + // in. CALL_ID is not implemented yet, so it can never be dialed. Play Audio + // has no destination to fill in - it always has a default audio file ready. + let hasDestination = false; + if (endpointType === EndpointType.PHONE_NUMBER) { + hasDestination = destNumber.replace(/[^\d]/g, '').length > 0; + } else if (endpointType === EndpointType.ENDPOINT) { + hasDestination = endpointTarget.trim().length > 0; + } else if (endpointType === PLAY_AUDIO_TYPE) { + hasDestination = true; + } const endCallButtonProps = { type: 'end-call', @@ -204,12 +231,13 @@ function CallController({bandwidthRtcClient, readyMetadata, inCall, setInCall, c

{!inCall && callStatus}{inCall && !connected && "Ringing"}{inCall && connected && "In Call"}

@@ -221,6 +249,9 @@ function CallController({bandwidthRtcClient, readyMetadata, inCall, setInCall, c :
{endpointTarget}
} )} + {endpointType == PLAY_AUDIO_TYPE && ( +
Will play a 30 second sample audio file
+ )} {endpointType == EndpointType.CALL_ID && ( <> TODO diff --git a/src/css/CallController.scss b/src/css/CallController.scss index 7750aeb..0cd41d4 100644 --- a/src/css/CallController.scss +++ b/src/css/CallController.scss @@ -15,6 +15,13 @@ word-break: break-all; } +.play-audio-note { + font-size: 1rem; + text-align: center; + color: var(--grey55); + margin-bottom: 10px; +} + .call-controls { display: flex; justify-content: center;