diff --git a/package-lock.json b/package-lock.json index c48e1de..99c7d08 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16,7 +16,7 @@ "@types/node": "^16.18.126", "@types/react": "^19.2.2", "@types/react-dom": "^19.2.2", - "bandwidth-rtc": "^0.3.0", + "bandwidth-rtc": "^0.4.0", "bandwidth-sdk": "^7.3.0", "cors": "^2.8.5", "dotenv": "^16.4.7", @@ -5683,7 +5683,9 @@ "license": "MIT" }, "node_modules/bandwidth-rtc": { - "version": "0.3.0", + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/bandwidth-rtc/-/bandwidth-rtc-0.4.0.tgz", + "integrity": "sha512-BZDWvjSci6C1m1a3aLzndMwOnfSzfqF4vJiyp0ukD69ljXU1tU1fgQHoYPgFK9Xdn48HI/z9cXVp8DZhm2rZIw==", "license": "MIT", "dependencies": { "@types/uuid": "^10.0.0", diff --git a/package.json b/package.json index 21ffbfd..dc41316 100644 --- a/package.json +++ b/package.json @@ -13,7 +13,7 @@ "@types/node": "^16.18.126", "@types/react": "^19.2.2", "@types/react-dom": "^19.2.2", - "bandwidth-rtc": "^0.3.0", + "bandwidth-rtc": "^0.4.0", "bandwidth-sdk": "^7.3.0", "cors": "^2.8.5", "dotenv": "^16.4.7", diff --git a/server/index.ts b/server/index.ts index 6606feb..dfd44ac 100644 --- a/server/index.ts +++ b/server/index.ts @@ -183,6 +183,7 @@ function processInboundCall(callId: string): string { } return ` + Connecting ${endpointId} @@ -191,6 +192,7 @@ function processInboundCall(callId: string): string { } return ` + Connecting ${requestingEndpointId} @@ -335,6 +337,20 @@ app.post('/callbacks/bandwidth', async (req: Request, res: Response) => { } catch (error: any) { console.error('Error placing outbound call:', error.message); } + return res.type('application/xml').send(``); + } + if (toType === 'ENDPOINT') { + // Endpoint-to-endpoint has no PSTN leg to place. We answer the + // request with BXML containing a verb targeting the + // destination endpoint; the callback proxy executes it against the + // requesting endpoint's leg, bridging the two WebRTC endpoints. + console.log(`Bridging endpoints via : ${event.from} -> ${to}`); + return res.type('application/xml').send(` + + + ${to} + +`); } return res.type('application/xml').send(``); } @@ -400,6 +416,13 @@ app.post('/calls/status', async (req: Request, res: Response) => { } }); +// POST /connectstatus - Connect bridge lifecycle events (PSTN and +// endpoint-to-endpoint connect). Logged for observability; no BXML response needed. +app.post('/connectstatus', (req: Request, res: Response) => { + console.log('Connect status event:', JSON.stringify(req.body, null, 2)); + res.sendStatus(200); +}); + // GET /api/endpoint/:endpointId/call-status - Get current PSTN call status for an endpoint app.get('/api/endpoint/:endpointId/call-status', (req: Request, res: Response) => { const endpointId = req.params.endpointId; diff --git a/src/App.tsx b/src/App.tsx index 5461f54..95f76ba 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -13,6 +13,7 @@ function App() { const [brtcClientReady, setBrtcClientReady] = useState(false); const [readyMetadata, setReadyMetadata] = useState(null); const [inCall, setInCall] = useState(false); + const [inboundStream, setInboundStream] = useState(null); const prepBrtcClient= async (reset: boolean) => { console.log("Prepping Bandwidth RTC Client") @@ -21,10 +22,29 @@ function App() { setBrtcClientReady(false) setReadyMetadata(null) setBrtcClient(null) + setInCall(false) + setInboundStream(null) } if (!brtcClient || reset) { console.log("Creating Bandwidth RTC Client") let brtcClient = new BandwidthRtc('debug') + // Registered before connect() runs: the subscribing peer connection's + // ontrack fires once during initial connection setup, before + // readyMetadata (and therefore MediaPlayer) exists. + brtcClient.onStreamAvailable((s) => { + console.log("Stream available:", s); + setInboundStream(s.mediaStream); + // The stream arriving means the call actually connected (answered), + // so we're truly in-call now — not just ringing. + setInCall(true); + }) + brtcClient.onStreamUnavailable((s) => { + console.log("Stream unavailable:", s); + setInboundStream(null); + // The far side (or gateway) ended the call; reset the UI out of + // the in-call/ringing state. + setInCall(false); + }) brtcClient.onReady((readyMetadata: ReadyMetadata) => { console.log("Ready Metadata:", readyMetadata); setBrtcClientReady(true) @@ -56,12 +76,12 @@ function App() { {readyMetadata && ( <>

Bandwidth RTC Agent Sample

-
+
- +
-
- +
+
)} diff --git a/src/components/CallController.tsx b/src/components/CallController.tsx index 0d62757..66c756f 100644 --- a/src/components/CallController.tsx +++ b/src/components/CallController.tsx @@ -3,6 +3,7 @@ import React, {ChangeEventHandler, useState} from "react"; import '../css/DigitGrid.scss'; import '../css/CallControlButton.scss'; import '../css/NumberInput.scss'; +import '../css/CallController.scss'; import CallIcon from '@mui/icons-material/Call'; import CallEndIcon from '@mui/icons-material/CallEnd'; import ShortcutOutlinedIcon from '@mui/icons-material/ShortcutOutlined'; @@ -66,11 +67,7 @@ function NumberInput ({ onChange, value }: {onChange: ChangeEventHandler, value: } -function isDestinationValid(destination: string): boolean { - return true -} - -function CallController({bandwidthRtcClient, readyMetadata, inCall, setInCall}: {bandwidthRtcClient: BandwidthRtc, readyMetadata: ReadyMetadata, inCall: boolean, setInCall: (inCall: boolean) => void} ) { +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"); } @@ -106,27 +103,38 @@ function CallController({bandwidthRtcClient, readyMetadata, inCall, setInCall}: let result = await bandwidthRtcClient.requestOutboundConnection(e164Number, EndpointType.PHONE_NUMBER) if (result.accepted) { setCallStatus('Ringing'); + setInCall(true); } else { setCallStatus('Declined'); } } const handleHangUp = async () => { setCallStatus('Hanging Up...'); - // Ensure E.164 format: clean digits and add '+' - const e164Number = '+' + destNumber.replace(/[^\d]/g, ''); - let result = await bandwidthRtcClient.hangupConnection(e164Number, EndpointType.PHONE_NUMBER) + // Hang up whichever destination the active call is using. endpointType is + // frozen during a call (the selector is disabled), so it still reflects + // how this call was dialed. + let result; + if (endpointType === EndpointType.ENDPOINT) { + result = await bandwidthRtcClient.hangupConnection(endpointTarget, EndpointType.ENDPOINT) + } else { + // Ensure E.164 format: clean digits and add '+' + const e164Number = '+' + destNumber.replace(/[^\d]/g, ''); + result = await bandwidthRtcClient.hangupConnection(e164Number, EndpointType.PHONE_NUMBER) + } setCallStatus(result.result) + setInCall(false); } const handleDigitClick = (value: string) => { - if (inCall) { + if (connected) { + // DTMF only once the call is answered; during ringing, digits do nothing. console.log(`Sending DTMF: ${value}, duration: ${dtmfDuration}ms`); try { bandwidthRtcClient.sendDtmf(value, undefined, dtmfDuration); } catch (err) { console.error('sendDtmf failed:', err); } - } else { + } else if (!inCall) { setDestNumber((destNumber) => destNumber.concat(value)); } } @@ -135,7 +143,14 @@ function CallController({bandwidthRtcClient, readyMetadata, inCall, setInCall}: } const handleDialEndpoint = async () => { - await bandwidthRtcClient.requestOutboundConnection(endpointTarget, EndpointType.ENDPOINT) + setCallStatus('Calling...'); + let result = await bandwidthRtcClient.requestOutboundConnection(endpointTarget, EndpointType.ENDPOINT) + if (result.accepted) { + setCallStatus('Ringing'); + setInCall(true); + } else { + setCallStatus('Declined'); + } } const handleDial = async () => { @@ -147,6 +162,15 @@ function CallController({bandwidthRtcClient, readyMetadata, inCall, setInCall}: } + // 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; + const endCallButtonProps = { type: 'end-call', onClick: handleHangUp, @@ -157,7 +181,7 @@ function CallController({bandwidthRtcClient, readyMetadata, inCall, setInCall}: const startCallButtonProps = { type: 'start-call', onClick: handleDial, - disabled: !isDestinationValid(destNumber) || inCall, + disabled: !hasDestination || inCall, Icon: CallIcon }; @@ -188,11 +212,13 @@ function CallController({bandwidthRtcClient, readyMetadata, inCall, setInCall}: ))} -

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

+

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

{endpointType == EndpointType.ENDPOINT && ( <> - setEndpointTarget(value.target.value)} /> + {!inCall + ? setEndpointTarget(value.target.value)} /> + :
{endpointTarget}
} )} {endpointType == EndpointType.CALL_ID && ( @@ -206,7 +232,7 @@ function CallController({bandwidthRtcClient, readyMetadata, inCall, setInCall}:
- {inCall && ( + {connected && (
)} - {inCall && ( + {connected && (
void, gatewayUrl?: string }) { const [endpoint, setEndpoint] = useState(null); @@ -82,11 +83,13 @@ function EndpointHandler({bandwidthRtcClient, resetClient, gatewayUrl}: {bandwid
)} - - - +
+ + + +
{endpoint !== null && ( -

{endpoint.endpointId}

+

{endpoint.endpointId}

)}
); diff --git a/src/components/MediaCapture.tsx b/src/components/MediaCapture.tsx index cba49ce..2d8d764 100644 --- a/src/components/MediaCapture.tsx +++ b/src/components/MediaCapture.tsx @@ -244,10 +244,10 @@ function MediaCapture({bandwidthRtcClient}: {bandwidthRtcClient: BandwidthRtc} ) }; return ( -
+