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