diff --git a/package.json b/package.json index c6323df..89a70a4 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.5.0", + "bandwidth-rtc": "^0.6.0", "bandwidth-sdk": "^7.3.0", "cors": "^2.8.5", "dotenv": "^16.4.7", diff --git a/src/App.tsx b/src/App.tsx index 95f76ba..64709d6 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -3,9 +3,10 @@ import './css/App.scss'; import Navbar from "./components/Navbar"; import EndpointHandler from "./components/EndpointHandler"; import MediaCapture from "./components/MediaCapture"; -import BandwidthRtc, {ReadyMetadata} from "bandwidth-rtc"; +import BandwidthRtc, {ReadyMetadata, RtcStream} from "bandwidth-rtc"; import MediaPlayer from "./components/MediaPlayer"; import CallController from "./components/CallController"; +import IncomingCall from "./components/IncomingCall"; function App() { @@ -14,6 +15,12 @@ function App() { const [readyMetadata, setReadyMetadata] = useState(null); const [inCall, setInCall] = useState(false); const [inboundStream, setInboundStream] = useState(null); + // When autoAccept is off, an incoming stream that the gateway did not + // auto-accept lands here so we can prompt the user with Accept/Decline. + const [incomingCall, setIncomingCall] = useState(null); + // Sent to the gateway at connect time via setMediaPreferences. The gateway + // echoes its decision back on each stream (RtcStream.autoAccepted). + const [autoAccept, setAutoAccept] = useState(false); const prepBrtcClient= async (reset: boolean) => { console.log("Prepping Bandwidth RTC Client") @@ -24,6 +31,7 @@ function App() { setBrtcClient(null) setInCall(false) setInboundStream(null) + setIncomingCall(null) } if (!brtcClient || reset) { console.log("Creating Bandwidth RTC Client") @@ -33,17 +41,23 @@ function App() { // 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); + // The gateway rides its accept decision on the stream itself. When it + // auto-accepted, connect straight through; otherwise prompt the user + // and wait for acceptStream/declineStream before playing any audio. + if (s.autoAccepted) { + setInboundStream(s.mediaStream); + setInCall(true); + } else { + setIncomingCall(s); + } }) brtcClient.onStreamUnavailable((s) => { console.log("Stream unavailable:", s); + // The far side (or gateway) ended/declined the call; reset the UI out + // of the in-call/ringing state. setInboundStream(null); - // The far side (or gateway) ended the call; reset the UI out of - // the in-call/ringing state. setInCall(false); + setIncomingCall(null); }) brtcClient.onReady((readyMetadata: ReadyMetadata) => { console.log("Ready Metadata:", readyMetadata); @@ -62,6 +76,20 @@ function App() { await prepBrtcClient(true) } + const handleAccept = async () => { + if (!brtcClient || !incomingCall) return; + await brtcClient.acceptStream(); + setInboundStream(incomingCall.mediaStream); + setInCall(true); + setIncomingCall(null); + } + + const handleDecline = async () => { + if (!brtcClient) return; + await brtcClient.declineStream(); + setIncomingCall(null); + } + // Fetch the WebSocket URL from env const gatewayUrl = process.env.REACT_APP_WSS_URL; @@ -70,9 +98,12 @@ function App() { {brtcClient && ( <> - +
{brtcClientReady} + {incomingCall && ( + + )} {readyMetadata && ( <>

Bandwidth RTC Agent Sample

diff --git a/src/components/EndpointHandler.tsx b/src/components/EndpointHandler.tsx index 3ca6c44..4ae1757 100644 --- a/src/components/EndpointHandler.tsx +++ b/src/components/EndpointHandler.tsx @@ -3,7 +3,7 @@ import BandwidthRtc from "bandwidth-rtc"; import {Endpoint} from "../../server/types" import '../css/EndpointHandler.scss'; -function EndpointHandler({bandwidthRtcClient, resetClient, gatewayUrl}: {bandwidthRtcClient: BandwidthRtc, resetClient: () => void, gatewayUrl?: string }) { +function EndpointHandler({bandwidthRtcClient, resetClient, gatewayUrl, autoAccept, setAutoAccept}: {bandwidthRtcClient: BandwidthRtc, resetClient: () => void, gatewayUrl?: string, autoAccept: boolean, setAutoAccept: (autoAccept: boolean) => void }) { const [endpoint, setEndpoint] = useState(null); const [banner, setBanner] = useState<{ message: string; isError: boolean } | null>(null); @@ -23,7 +23,8 @@ function EndpointHandler({bandwidthRtcClient, resetClient, gatewayUrl}: {bandwid await bandwidthRtcClient.connect({ endpointToken: endpointData.token }, { - websocketUrl: gatewayUrl + websocketUrl: gatewayUrl, + autoAccept }).then(() => { console.log("WebRTC Client Connected"); }).catch((error) => { @@ -88,6 +89,15 @@ function EndpointHandler({bandwidthRtcClient, resetClient, gatewayUrl}: {bandwid + {endpoint !== null && (

{endpoint.endpointId}

)} diff --git a/src/components/IncomingCall.tsx b/src/components/IncomingCall.tsx new file mode 100644 index 0000000..54cef86 --- /dev/null +++ b/src/components/IncomingCall.tsx @@ -0,0 +1,24 @@ +import React from "react"; +import {RtcStream} from "bandwidth-rtc"; +import '../css/IncomingCall.scss'; + +function IncomingCall({stream, onAccept, onDecline}: {stream: RtcStream, onAccept: () => void, onDecline: () => void}) { + const caller = stream.from || "Unknown"; + + return ( +
+
+ Incoming call + + {caller}{stream.fromType ? ` (${stream.fromType})` : ""} + +
+
+ + +
+
+ ); +} + +export default IncomingCall; diff --git a/src/css/EndpointHandler.scss b/src/css/EndpointHandler.scss index 8fa66b9..967d779 100644 --- a/src/css/EndpointHandler.scss +++ b/src/css/EndpointHandler.scss @@ -11,3 +11,12 @@ word-break: break-all; padding: 0 8px; } + +.auto-accept-toggle { + display: flex; + align-items: center; + justify-content: center; + gap: 6px; + margin-top: 8px; + cursor: pointer; +} diff --git a/src/css/IncomingCall.scss b/src/css/IncomingCall.scss new file mode 100644 index 0000000..33cfcbb --- /dev/null +++ b/src/css/IncomingCall.scss @@ -0,0 +1,53 @@ +// IncomingCall.scss + +.incoming-call { + display: flex; + flex-wrap: wrap; + align-items: center; + justify-content: space-between; + gap: 16px; + max-width: 480px; + margin: 8px auto; + padding: 12px 24px; + background: #e3f2fd; + border: 2px solid #1976d2; + border-radius: 8px; + + .incoming-call-info { + display: flex; + flex-direction: column; + align-items: flex-start; + } + + .incoming-call-label { + font-weight: 600; + } + + .incoming-call-from { + font-size: 0.9rem; + color: #555; + word-break: break-all; + } + + .incoming-call-actions { + display: flex; + gap: 8px; + } + + button { + border: none; + padding: 8px 20px; + border-radius: 4px; + cursor: pointer; + font-weight: 600; + color: white; + + &.accept { + background: #4caf50; + } + + &.decline { + background: #f44336; + } + } +}