Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions server/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -339,6 +347,13 @@ app.post('/callbacks/bandwidth', async (req: Request, res: Response) => {
}
return res.type('application/xml').send(`<?xml version="1.0" encoding="UTF-8"?><Response/>`);
}
if (toType === 'ENDPOINT' && to === PLAY_AUDIO_TARGET) {
console.log(`Playing demo audio for endpoint ${endpointId}`);
return res.type('application/xml').send(`<?xml version="1.0" encoding="UTF-8"?>
<Response>
<PlayAudio>${DEFAULT_PLAY_AUDIO_URL}</PlayAudio>
</Response>`);
}
if (toType === 'ENDPOINT') {
// Endpoint-to-endpoint has no PSTN leg to place. We answer the
// request with BXML containing a <Connect> verb targeting the
Expand Down
51 changes: 41 additions & 10 deletions src/components/CallController.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 <PlayAudio>
// 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");
Expand All @@ -78,7 +87,7 @@ function CallController({bandwidthRtcClient, readyMetadata, inCall, setInCall, c
const [destNumber, setDestNumber] = useState<string>('');
const [callStatus, setCallStatus] = useState('Select Connection Target');
const [endpointTarget, setEndpointTarget] = useState('');
const [endpointType, setEndpointType] = useState<EndpointType>(EndpointType.PHONE_NUMBER);
const [endpointType, setEndpointType] = useState<DestinationType>(EndpointType.PHONE_NUMBER);
const [dtmfDuration, setDtmfDuration] = useState<number>(300);
const [dtmfSequence, setDtmfSequence] = useState<string>('');

Expand Down Expand Up @@ -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 '+'
Expand Down Expand Up @@ -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',
Expand Down Expand Up @@ -204,12 +231,13 @@ function CallController({bandwidthRtcClient, readyMetadata, inCall, setInCall, c
<select
disabled={inCall}
value={endpointType}
onChange={e => setEndpointType(e.target.value as EndpointType)}
onChange={e => setEndpointType(e.target.value as DestinationType)}
style={{ marginBottom: '10px' }}
>
{Object.values(EndpointType).map(type => (
<option key={type} value={type}>{type}</option>
))}
<option value={PLAY_AUDIO_TYPE}>PLAY_AUDIO</option>
</select>

<h2>{!inCall && callStatus}{inCall && !connected && "Ringing"}{inCall && connected && "In Call"}</h2>
Expand All @@ -221,6 +249,9 @@ function CallController({bandwidthRtcClient, readyMetadata, inCall, setInCall, c
: <div className='dialed-number'>{endpointTarget}</div>}
</>
)}
{endpointType == PLAY_AUDIO_TYPE && (
<div className='play-audio-note'>Will play a 30 second sample audio file</div>
)}
{endpointType == EndpointType.CALL_ID && (
<>
TODO
Expand Down
7 changes: 7 additions & 0 deletions src/css/CallController.scss
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down