From a95a03db914c20cb32db6bf91f839a39a6560692 Mon Sep 17 00:00:00 2001 From: smoghe-bw Date: Fri, 24 Apr 2026 12:04:18 -0400 Subject: [PATCH 01/17] feat: add connectStatus to readyMetadata on connectStatus event --- package-lock.json | 12 ------------ src/types.ts | 18 ++++++++++++++++++ src/v1/signaling.test.ts | 36 ++++++++++++++++++++++++++++-------- src/v1/signaling.ts | 17 +++++++++++++++-- src/v1/types.ts | 11 ++++++++++- 5 files changed, 71 insertions(+), 23 deletions(-) diff --git a/package-lock.json b/package-lock.json index 70d4180..a90474d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -88,7 +88,6 @@ "integrity": "sha512-H3mcG6ZDLTlYfaSNi0iOKkigqMFvkTKlGUYlD8GW7nNOYRrevuA46iTypPyv+06V3fEmvvazfntkBU34L0azAw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@babel/code-frame": "^7.28.6", "@babel/generator": "^7.28.6", @@ -1981,7 +1980,6 @@ } ], "license": "MIT", - "peer": true, "engines": { "node": ">=18" }, @@ -2005,7 +2003,6 @@ } ], "license": "MIT", - "peer": true, "engines": { "node": ">=18" } @@ -2843,7 +2840,6 @@ "integrity": "sha512-r0bBaXu5Swb05doFYO2kTWHMovJnNVbCsII0fhesM8bNRlLhXIuckley4a2DaD+vOdmm5G+zGkQZAPZsF80+YQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "undici-types": "~7.16.0" } @@ -3396,7 +3392,6 @@ "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "dev": true, "license": "MIT", - "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -3446,7 +3441,6 @@ "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", @@ -3775,7 +3769,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", @@ -5701,7 +5694,6 @@ "integrity": "sha512-Cvc9WUhxSMEo4McES3P7oK3QaXldCfNWp7pl2NNeiIFlCoLr3kfq9kb1fxftiwk1FLV7CvpvDfonxtzUDeSOPg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "cssstyle": "^4.2.1", "data-urls": "^5.0.0", @@ -7327,7 +7319,6 @@ "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@cspotcode/source-map-support": "^0.8.0", "@tsconfig/node10": "^1.0.7", @@ -7425,7 +7416,6 @@ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "dev": true, "license": "Apache-2.0", - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -7660,7 +7650,6 @@ "integrity": "sha512-Qphch25abbMNtekmEGJmeRUhLDbe+QfiWTiqpKYkpCOWY64v9eyl+KRRLmqOFA2AvKPpc9DC6+u2n76tQLBoaA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@types/eslint-scope": "^3.7.7", "@types/estree": "^1.0.8", @@ -7710,7 +7699,6 @@ "integrity": "sha512-MfwFQ6SfwinsUVi0rNJm7rHZ31GyTcpVE5pgVA3hwFRb7COD4TzjUUwhGWKfO50+xdc2MQPuEBBJoqIMGt3JDw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@discoveryjs/json-ext": "^0.6.1", "@webpack-cli/configtest": "^3.0.1", diff --git a/src/types.ts b/src/types.ts index 0a2bb04..90c9bc1 100644 --- a/src/types.ts +++ b/src/types.ts @@ -21,6 +21,15 @@ export enum EndpointType { PHONE_NUMBER = "PHONE_NUMBER", } +export enum ConnectStatus { + INITIATED = "INITIATED", + COMPLETED = "COMPLETED", + TIMED_OUT = "TIMED_OUT", + DENIED = "DENIED", + CANCELED = "CANCELED", + FAILED = "FAILED", +} + export type AudioLevelChangeHandler = { (audioLevel: AudioLevel): void }; /** @@ -49,6 +58,15 @@ export interface ReadyMetadata { deviceId: string; territory: string; // TODO enum region: string; // TODO enum + connectStatus?: ConnectStatus; + accountId?: string; + sessionId?: string; + from?: string; + fromType?: string; + fromTags?: string; + to?: string; + toType?: string; + toTags?: string; } export interface OutboundConnectionResult { diff --git a/src/v1/signaling.test.ts b/src/v1/signaling.test.ts index cdbb933..463fd6f 100644 --- a/src/v1/signaling.test.ts +++ b/src/v1/signaling.test.ts @@ -94,19 +94,39 @@ describe("Signaling connect method", () => { } }); - test("should emit established when websocket receives established", async () => { + test("should merge connectStatus into readyMetadata and re-emit ready", async () => { const emitSpy = jest.spyOn(signaling, "emit"); await signaling.connect({ endpointToken: "test-token" }); - // Get the websocket instance and trigger established event const ws = (signaling as any).ws; - const establishedCallback = ws.on.mock.calls.find((call: any) => call[0] === "established")?.[1]; - - if (establishedCallback) { - const testEvent = { connectionId: "test-connection" }; - establishedCallback(testEvent); - expect(emitSpy).toHaveBeenCalledWith("established", testEvent); + const connectStatusCallback = ws.on.mock.calls.find((call: any) => call[0] === "connectStatus")?.[1]; + + if (connectStatusCallback) { + const testEvent = { + status: "COMPLETED", + accountId: "9900000", + sessionId: "session-1", + from: "ep-1", + fromType: "ENDPOINT", + fromTags: "tag1", + to: "ep-2", + toType: "ENDPOINT", + toTags: "tag2", + }; + connectStatusCallback(testEvent); + expect(emitSpy).toHaveBeenCalledWith("ready", expect.objectContaining({ + endpointId: "test-endpoint", + connectStatus: "COMPLETED", + accountId: "9900000", + sessionId: "session-1", + from: "ep-1", + fromType: "ENDPOINT", + fromTags: "tag1", + to: "ep-2", + toType: "ENDPOINT", + toTags: "tag2", + })); } }); }); diff --git a/src/v1/signaling.ts b/src/v1/signaling.ts index 36ed864..e855c9e 100644 --- a/src/v1/signaling.ts +++ b/src/v1/signaling.ts @@ -52,8 +52,21 @@ class Signaling extends EventEmitter { this.emit("sdpOffer", event); }); - ws.on("established", (event: any) => { - this.emit("established", event); + ws.on("connectStatus", (event: any) => { + logger.debug("Websocket connectStatus", event); + this.readyMetadata = { + ...this.readyMetadata!, + connectStatus: event.status, + accountId: event.accountId, + sessionId: event.sessionId, + from: event.from, + fromType: event.fromType, + fromTags: event.fromTags, + to: event.to, + toType: event.toType, + toTags: event.toTags, + }; + this.emit("ready", this.readyMetadata); }); ws.on("open", async () => { diff --git a/src/v1/types.ts b/src/v1/types.ts index 7c0d18a..42776fa 100644 --- a/src/v1/types.ts +++ b/src/v1/types.ts @@ -1,4 +1,4 @@ -import { MediaType } from "../types"; +import { MediaType, ConnectStatus } from "../types"; export interface SetMediaPreferencesWebRtcResponse { endpointId: string; @@ -69,4 +69,13 @@ export interface ReadyMetadata { deviceId: string; territory: string; // TODO enum region: string; // TODO enum + connectStatus?: ConnectStatus; + accountId?: string; + sessionId?: string; + from?: string; + fromType?: string; + fromTags?: string; + to?: string; + toType?: string; + toTags?: string; } From af93d6e98b5a3e91edd03f7af0ef501c4dae848c Mon Sep 17 00:00:00 2001 From: smoghe-bw Date: Mon, 27 Apr 2026 10:29:30 -0400 Subject: [PATCH 02/17] feat(rtc): add onConnectStatus event handler Emit connectStatus event separately from ready so callers can subscribe to connect status updates independently via onConnectStatus. --- src/v1/bandwidthRtc.ts | 1 + src/v1/signaling.test.ts | 42 ++++++++++++++++++++++++---------------- src/v1/signaling.ts | 16 --------------- 3 files changed, 26 insertions(+), 33 deletions(-) diff --git a/src/v1/bandwidthRtc.ts b/src/v1/bandwidthRtc.ts index dab9d9f..a628815 100644 --- a/src/v1/bandwidthRtc.ts +++ b/src/v1/bandwidthRtc.ts @@ -147,6 +147,7 @@ export class BandwidthRtc { this.readyHandler = callback; } + /** * Publish media to the Bandwidth WebRTC platform * diff --git a/src/v1/signaling.test.ts b/src/v1/signaling.test.ts index 463fd6f..eb408b9 100644 --- a/src/v1/signaling.test.ts +++ b/src/v1/signaling.test.ts @@ -94,29 +94,21 @@ describe("Signaling connect method", () => { } }); - test("should merge connectStatus into readyMetadata and re-emit ready", async () => { + test("should emit ready with connectStatus fields when gateway sends ready with connect status data", async () => { const emitSpy = jest.spyOn(signaling, "emit"); await signaling.connect({ endpointToken: "test-token" }); + // Simulate a second ready event from the gateway that includes connect status fields const ws = (signaling as any).ws; - const connectStatusCallback = ws.on.mock.calls.find((call: any) => call[0] === "connectStatus")?.[1]; + const readyCallback = ws.on.mock.calls.find((call: any) => call[0] === "ready")?.[1]; - if (connectStatusCallback) { - const testEvent = { - status: "COMPLETED", - accountId: "9900000", - sessionId: "session-1", - from: "ep-1", - fromType: "ENDPOINT", - fromTags: "tag1", - to: "ep-2", - toType: "ENDPOINT", - toTags: "tag2", - }; - connectStatusCallback(testEvent); - expect(emitSpy).toHaveBeenCalledWith("ready", expect.objectContaining({ + if (readyCallback) { + const readyWithConnectStatus = { endpointId: "test-endpoint", + deviceId: "device-1", + territory: "US", + region: "us-east-1", connectStatus: "COMPLETED", accountId: "9900000", sessionId: "session-1", @@ -126,7 +118,23 @@ describe("Signaling connect method", () => { to: "ep-2", toType: "ENDPOINT", toTags: "tag2", - })); + }; + readyCallback(readyWithConnectStatus); + expect(emitSpy).toHaveBeenCalledWith( + "ready", + expect.objectContaining({ + endpointId: "test-endpoint", + connectStatus: "COMPLETED", + accountId: "9900000", + sessionId: "session-1", + from: "ep-1", + fromType: "ENDPOINT", + fromTags: "tag1", + to: "ep-2", + toType: "ENDPOINT", + toTags: "tag2", + }), + ); } }); }); diff --git a/src/v1/signaling.ts b/src/v1/signaling.ts index e855c9e..ac13aa6 100644 --- a/src/v1/signaling.ts +++ b/src/v1/signaling.ts @@ -52,22 +52,6 @@ class Signaling extends EventEmitter { this.emit("sdpOffer", event); }); - ws.on("connectStatus", (event: any) => { - logger.debug("Websocket connectStatus", event); - this.readyMetadata = { - ...this.readyMetadata!, - connectStatus: event.status, - accountId: event.accountId, - sessionId: event.sessionId, - from: event.from, - fromType: event.fromType, - fromTags: event.fromTags, - to: event.to, - toType: event.toType, - toTags: event.toTags, - }; - this.emit("ready", this.readyMetadata); - }); ws.on("open", async () => { logger.debug("Websocket open"); From e2567ea871e961f9f3ebc63d3fdb836bd10197be Mon Sep 17 00:00:00 2001 From: smoghe-bw Date: Mon, 27 Apr 2026 10:33:52 -0400 Subject: [PATCH 03/17] style: remove extra blank lines in bandwidthRtc.ts and signaling.ts --- src/v1/bandwidthRtc.ts | 1 - src/v1/signaling.ts | 1 - 2 files changed, 2 deletions(-) diff --git a/src/v1/bandwidthRtc.ts b/src/v1/bandwidthRtc.ts index a628815..dab9d9f 100644 --- a/src/v1/bandwidthRtc.ts +++ b/src/v1/bandwidthRtc.ts @@ -147,7 +147,6 @@ export class BandwidthRtc { this.readyHandler = callback; } - /** * Publish media to the Bandwidth WebRTC platform * diff --git a/src/v1/signaling.ts b/src/v1/signaling.ts index ac13aa6..6194473 100644 --- a/src/v1/signaling.ts +++ b/src/v1/signaling.ts @@ -52,7 +52,6 @@ class Signaling extends EventEmitter { this.emit("sdpOffer", event); }); - ws.on("open", async () => { logger.debug("Websocket open"); if (globalThis.addEventListener) { From 0ca235cf5ab39f2d1b951f1ecee71f25042c04cb Mon Sep 17 00:00:00 2001 From: smoghe-bw Date: Mon, 27 Apr 2026 10:52:49 -0400 Subject: [PATCH 04/17] test(signaling): add websocket event handler and disconnect tests --- src/v1/signaling.test.ts | 105 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 105 insertions(+) diff --git a/src/v1/signaling.test.ts b/src/v1/signaling.test.ts index eb408b9..477716b 100644 --- a/src/v1/signaling.test.ts +++ b/src/v1/signaling.test.ts @@ -139,6 +139,111 @@ describe("Signaling connect method", () => { }); }); +describe("Signaling websocket event handlers", () => { + let signaling: Signaling; + beforeEach(async () => { + signaling = new Signaling(); + await signaling.connect({ endpointToken: "test-token" }); + }); + + function getWsCallback(event: string) { + const ws = (signaling as any).ws; + return ws.on.mock.calls.find((call: any) => call[0] === event)?.[1]; + } + + test("should emit init and set up ping interval on open", async () => { + const emitSpy = jest.spyOn(signaling, "emit"); + const openCallback = getWsCallback("open"); + expect(openCallback).toBeDefined(); + + await openCallback(); + + expect(emitSpy).toHaveBeenCalledWith("init", expect.anything()); + expect((signaling as any).pingInterval).toBeDefined(); + }); + + test("should reject with error and disconnect on 403 error", async () => { + const errorCallback = getWsCallback("error"); + expect(errorCallback).toBeDefined(); + + const ws = (signaling as any).ws; + errorCallback({ message: "Unexpected server response: 403" }); + + expect(ws.close).toHaveBeenCalledWith(403); + expect(ws.setAutoReconnect).toHaveBeenCalledWith(false); + }); + + test("should handle non-403 error without throwing", async () => { + const errorCallback = getWsCallback("error"); + expect(errorCallback).toBeDefined(); + + // Should not throw on a generic error + expect(() => errorCallback({ message: "some other error" })).not.toThrow(); + + // ws should not be closed on non-403 errors + const ws = (signaling as any).ws; + expect(ws.setAutoReconnect).not.toHaveBeenCalled(); + }); + + test("should clear ping interval and set isReady false on close", async () => { + // Trigger open first to set up pingInterval + const openCallback = getWsCallback("open"); + await openCallback(); + + const closeCallback = getWsCallback("close"); + expect(closeCallback).toBeDefined(); + + closeCallback(4000); + + expect((signaling as any).isReady).toBe(false); + }); + + test("should call _disconnect on close with code 1000", async () => { + const closeCallback = getWsCallback("close"); + expect(closeCallback).toBeDefined(); + + closeCallback(1000); + + // After _disconnect(false), ws should be null + expect((signaling as any).ws).toBeNull(); + expect((signaling as any).isReady).toBe(false); + }); +}); + +describe("Signaling disconnect", () => { + test("should call leave notification and close ws on disconnect", async () => { + const signaling = new Signaling(); + await signaling.connect({ endpointToken: "test-token" }); + + const ws = (signaling as any).ws; + signaling.disconnect(); + + expect(ws.notify).toHaveBeenCalledWith("leave"); + expect(ws.close).toHaveBeenCalled(); + expect(ws.removeAllListeners).toHaveBeenCalled(); + expect((signaling as any).ws).toBeNull(); + expect((signaling as any).isReady).toBe(false); + }); + + test("should handle disconnect with diagnosticsBatcher", async () => { + const diagnosticsBatcher = new DiagnosticsBatcher(); + const shutdownSpy = jest.spyOn(diagnosticsBatcher, "shutdown"); + const signaling = new Signaling(diagnosticsBatcher); + await signaling.connect({ endpointToken: "test-token" }); + + signaling.disconnect(); + + expect(shutdownSpy).toHaveBeenCalled(); + expect((signaling as any).ws).toBeNull(); + }); + + test("should not throw when disconnect called without active ws", () => { + const signaling = new Signaling(); + // Never connected, ws is null + expect(() => signaling.disconnect()).not.toThrow(); + }); +}); + describe("Signaling test all the smaller functions", () => { let signaling: Signaling; beforeEach(async () => { From e13ceb6211bd355db739457efe173b93d4edace5 Mon Sep 17 00:00:00 2001 From: smoghe-bw Date: Tue, 2 Jun 2026 09:25:32 -0400 Subject: [PATCH 05/17] feat: wire streamAvailable/streamUnavailable WS notifications to onStreamAvailable callback - RtcStream.mediaStream and callId are now optional: notification fires first with callId (no mediaStream) for call-arrival UI, then again via WebRTC ontrack with mediaStream when audio flows - signaling.ts subscribes to streamAvailable/streamUnavailable WS events and emits them for BandwidthRtc to forward to the app's handler - Adds acceptStream(callId?) and declineStream(callId?) on BandwidthRtc and Signaling so apps can explicitly accept or decline an inbound stream - Non-null assertions on setMicEnabled/setCameraEnabled/unpublish callers that always pass publish()-originated streams Co-Authored-By: Claude Sonnet 4.6 (1M context) --- src/types.ts | 3 ++- src/v1/bandwidthRtc.ts | 24 +++++++++++++++++++++--- src/v1/signaling.ts | 18 ++++++++++++++++++ 3 files changed, 41 insertions(+), 4 deletions(-) diff --git a/src/types.ts b/src/types.ts index 90c9bc1..2419603 100644 --- a/src/types.ts +++ b/src/types.ts @@ -48,7 +48,8 @@ export interface RtcOptions { export interface RtcStream { mediaTypes: MediaType[]; - mediaStream: MediaStream; + mediaStream?: MediaStream; + callId?: string; } export class BandwidthRtcError extends Error {} diff --git a/src/v1/bandwidthRtc.ts b/src/v1/bandwidthRtc.ts index dab9d9f..1c5bced 100644 --- a/src/v1/bandwidthRtc.ts +++ b/src/v1/bandwidthRtc.ts @@ -109,6 +109,16 @@ export class BandwidthRtc { this.signaling.on("ready", this.handleReady.bind(this)); this.signaling.on("sdpOffer", this.handleSubscribeSdpOffer.bind(this)); this.signaling.on("init", this.init.bind(this)); + this.signaling.on("streamAvailable", ({ callId }: { callId: string }) => { + if (this.streamAvailableHandler) { + this.streamAvailableHandler({ mediaTypes: [MediaType.AUDIO], callId }); + } + }); + this.signaling.on("streamUnavailable", ({ callId }: { callId: string }) => { + if (this.streamUnavailableHandler) { + this.streamUnavailableHandler({ mediaTypes: [MediaType.AUDIO], callId }); + } + }); await this.signaling.connect(authParams, options); logger.info("Successfully connected"); @@ -229,7 +239,7 @@ export class BandwidthRtc { } } else { publishedStreams.push({ - mediaStream: stream.mediaStream, + mediaStream: stream.mediaStream!, }); } } @@ -294,7 +304,7 @@ export class BandwidthRtc { setMicEnabled(enabled: boolean, stream?: RtcStream | string) { logger.info(`Setting microphone enabled: ${enabled}`); if (stream && typeof stream !== "string") { - stream = stream.mediaStream.id; + stream = stream.mediaStream!.id; } [...this.publishedStreams] .filter(([msid]) => !stream || stream === msid) @@ -309,7 +319,7 @@ export class BandwidthRtc { setCameraEnabled(enabled: boolean, stream?: RtcStream | string) { logger.info(`Setting camera enabled: ${enabled}`); if (stream && typeof stream !== "string") { - stream = stream.mediaStream.id; + stream = stream.mediaStream!.id; } [...this.publishedStreams] .filter(([msid]) => !stream || stream === msid) @@ -337,6 +347,14 @@ export class BandwidthRtc { return this.signaling.hangupConnection(endpoint, type); } + acceptStream(callId?: string): Promise { + return this.signaling.acceptStream(callId); + } + + declineStream(callId?: string): Promise { + return this.signaling.declineStream(callId); + } + private async offerPublishSdp(restartIce: boolean = false): Promise { if (!this.publishingPeerConnection) { throw new BandwidthRtcError("No publishing RTCPeerConnection, cannot offer SDP"); diff --git a/src/v1/signaling.ts b/src/v1/signaling.ts index 6194473..df5a137 100644 --- a/src/v1/signaling.ts +++ b/src/v1/signaling.ts @@ -79,6 +79,14 @@ class Signaling extends EventEmitter { resolve(); }); + ws.on("streamAvailable", (event: { callId: string; endpointId: string }) => { + this.emit("streamAvailable", event); + }); + + ws.on("streamUnavailable", (event: { callId: string; endpointId: string }) => { + this.emit("streamUnavailable", event); + }); + ws.on("error", (error: ErrorEvent) => { if (error.message === "Unexpected server response: 403") { logger.error("Authentication error: Invalid token"); @@ -169,6 +177,16 @@ class Signaling extends EventEmitter { }) as Promise; } + acceptStream(callId?: string): Promise { + logger.debug(`Calling "acceptStream"`, { callId }); + return this.ws?.call("acceptStream", callId ? { callId } : {}) as Promise; + } + + declineStream(callId?: string): Promise { + logger.debug(`Calling "declineStream"`, { callId }); + return this.ws?.call("declineStream", callId ? { callId } : {}) as Promise; + } + offerSdp(peerType: string, sdpOffer: string): Promise { logger.debug(`Calling "offerSdp"`, { sdpOffer: sdpOffer, peerType: peerType }); return this.ws?.call("offerSdp", { sdpOffer: sdpOffer, peerType: peerType }) as Promise; From be447ae5907ccbfd33045659386a469782285b4b Mon Sep 17 00:00:00 2001 From: smoghe-bw Date: Tue, 2 Jun 2026 09:34:55 -0400 Subject: [PATCH 06/17] feat: expose acceptStream/declineStream on outer BandwidthRtc class Delegates to v1 BandwidthRtcV1. Co-Authored-By: Claude Sonnet 4.6 (1M context) --- src/bandwidthRtc.ts | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/src/bandwidthRtc.ts b/src/bandwidthRtc.ts index cc78111..4cec567 100644 --- a/src/bandwidthRtc.ts +++ b/src/bandwidthRtc.ts @@ -257,6 +257,20 @@ class BandwidthRtc { } return this.delegate.hangupConnection(endpoint, type); } + + acceptStream(callId?: string): Promise { + if (!this.delegate) { + throw new BandwidthRtcError("You must call 'connect' before 'acceptStream'"); + } + return this.delegate.acceptStream(callId); + } + + declineStream(callId?: string): Promise { + if (!this.delegate) { + throw new BandwidthRtcError("You must call 'connect' before 'declineStream'"); + } + return this.delegate.declineStream(callId); + } } interface JwtPayload { From cb7b22659eef76df1d5e2b9b74b895c28ca00631 Mon Sep 17 00:00:00 2001 From: smoghe-bw Date: Tue, 2 Jun 2026 09:39:57 -0400 Subject: [PATCH 07/17] fix: add types field to package.json for TypeScript consumers Without an explicit types entry, CRA's compiler couldn't resolve declaration files from the dist/ directory via the npm link symlink. Co-Authored-By: Claude Sonnet 4.6 (1M context) --- package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/package.json b/package.json index 56a7333..61fe4fb 100644 --- a/package.json +++ b/package.json @@ -3,6 +3,7 @@ "version": "0.0.1", "description": "SDK for BandwidthRTC Node Applications", "main": "dist/index.js", + "types": "dist/index.d.ts", "scripts": { "build": "rm -rf ./dist/* && prettier --check . && tsc && webpack --config webpack.prod.js", "build:dev": "rm -rf ./dist/* && prettier --check . && tsc && webpack --config webpack.dev.js", From 1b86b4e711ce973c452c19db27348d42a055f277 Mon Sep 17 00:00:00 2001 From: smoghe-bw Date: Tue, 2 Jun 2026 09:47:16 -0400 Subject: [PATCH 08/17] fix: disable sourceMap output to prevent source-map-loader errors in consumers When installed via file: reference, CRA's source-map-loader follows relative paths in .js.map files that escape the project root through the symlink, causing ENOENT. Disabling source maps removes the .map files entirely. Co-Authored-By: Claude Sonnet 4.6 (1M context) --- tsconfig.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tsconfig.json b/tsconfig.json index 8a5f04f..381f054 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -10,7 +10,7 @@ // "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */ "declaration": true /* Generates corresponding '.d.ts' file. */, // "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */ - "sourceMap": true /* Generates corresponding '.map' file. */, + "sourceMap": false, // "outFile": "./", /* Concatenate and emit output to single file. */ "outDir": "./dist" /* Redirect output structure to the directory. */, // "rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */ From 04d77d3fee39dc8d79353cadc397917f0437c8ff Mon Sep 17 00:00:00 2001 From: smoghe-bw Date: Tue, 2 Jun 2026 10:19:19 -0400 Subject: [PATCH 09/17] feat: auto-acceptStream when no onStreamAvailable handler is registered Backwards compatibility shim: if the app never calls onStreamAvailable, the gateway's new closed-gate-between-calls behavior would silently break audio on all calls after the first. Auto-accepting keeps existing apps working without any code change on their side. Apps that register onStreamAvailable get the new ring/accept UX and are responsible for calling acceptStream explicitly. Co-Authored-By: Claude Sonnet 4.6 (1M context) --- src/v1/bandwidthRtc.ts | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/v1/bandwidthRtc.ts b/src/v1/bandwidthRtc.ts index 1c5bced..3a81521 100644 --- a/src/v1/bandwidthRtc.ts +++ b/src/v1/bandwidthRtc.ts @@ -112,6 +112,13 @@ export class BandwidthRtc { this.signaling.on("streamAvailable", ({ callId }: { callId: string }) => { if (this.streamAvailableHandler) { this.streamAvailableHandler({ mediaTypes: [MediaType.AUDIO], callId }); + } else { + // No handler registered — auto-accept so existing apps that never call + // acceptStream keep working after the gateway started closing the gate + // between calls. + this.signaling.acceptStream(callId).catch((err) => { + logger.warn("auto-acceptStream failed", err); + }); } }); this.signaling.on("streamUnavailable", ({ callId }: { callId: string }) => { From 1c4ff3a4f8c28e9a7c4fe498cb09a5fa70c6eb95 Mon Sep 17 00:00:00 2001 From: smoghe-bw Date: Tue, 2 Jun 2026 10:22:30 -0400 Subject: [PATCH 10/17] revert: remove auto-acceptStream shim (gateway handles compat instead) Gateway now opens the EgressGate at streamAvailable send time, so old SDKs that never call acceptStream are unaffected. The SDK-side shim is no longer needed. Co-Authored-By: Claude Sonnet 4.6 (1M context) --- src/v1/bandwidthRtc.ts | 7 ------- 1 file changed, 7 deletions(-) diff --git a/src/v1/bandwidthRtc.ts b/src/v1/bandwidthRtc.ts index 3a81521..1c5bced 100644 --- a/src/v1/bandwidthRtc.ts +++ b/src/v1/bandwidthRtc.ts @@ -112,13 +112,6 @@ export class BandwidthRtc { this.signaling.on("streamAvailable", ({ callId }: { callId: string }) => { if (this.streamAvailableHandler) { this.streamAvailableHandler({ mediaTypes: [MediaType.AUDIO], callId }); - } else { - // No handler registered — auto-accept so existing apps that never call - // acceptStream keep working after the gateway started closing the gate - // between calls. - this.signaling.acceptStream(callId).catch((err) => { - logger.warn("auto-acceptStream failed", err); - }); } }); this.signaling.on("streamUnavailable", ({ callId }: { callId: string }) => { From 042d632f476ab5e4075e22753a3e49c8da3fe04d Mon Sep 17 00:00:00 2001 From: smoghe-bw Date: Tue, 2 Jun 2026 10:33:51 -0400 Subject: [PATCH 11/17] refactor: remove commented-out offerSdp overload Dead code from before the peerType-based offerSdp was adopted. Co-Authored-By: Claude Sonnet 4.6 (1M context) --- src/v1/signaling.ts | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/src/v1/signaling.ts b/src/v1/signaling.ts index df5a137..3a77c51 100644 --- a/src/v1/signaling.ts +++ b/src/v1/signaling.ts @@ -52,6 +52,10 @@ class Signaling extends EventEmitter { this.emit("sdpOffer", event); }); + ws.on("established", (event: any) => { + this.emit("established", event); + }); + ws.on("open", async () => { logger.debug("Websocket open"); if (globalThis.addEventListener) { @@ -192,14 +196,6 @@ class Signaling extends EventEmitter { return this.ws?.call("offerSdp", { sdpOffer: sdpOffer, peerType: peerType }) as Promise; } - // offerSdp(sdpOffer: string, metadata: PublishMetadata): Promise { - // logger.debug(`Calling "offerSdp"`, { sdpOffer: sdpOffer, mediaMetadata: metadata }); - // return this.ws?.call("offerSdp", { - // sdpOffer: sdpOffer, - // mediaMetadata: metadata, - // }) as Promise; - // } - answerSdp(sdpAnswer: string, peerType: string): Promise { logger.debug(`Calling "answerSdp"`, { sdpAnswer: sdpAnswer }); return this.ws?.call("answerSdp", { From 1dfe3467717e8f7372c614683bda84752d54abf8 Mon Sep 17 00:00:00 2001 From: smoghe-bw Date: Tue, 2 Jun 2026 10:42:36 -0400 Subject: [PATCH 12/17] refactor: remove ConnectStatus enum and connect status fields from ReadyMetadata --- src/types.ts | 18 ---------------- src/v1/signaling.test.ts | 44 ++++++++-------------------------------- src/v1/types.ts | 11 +--------- 3 files changed, 9 insertions(+), 64 deletions(-) diff --git a/src/types.ts b/src/types.ts index 2419603..6c815cb 100644 --- a/src/types.ts +++ b/src/types.ts @@ -21,15 +21,6 @@ export enum EndpointType { PHONE_NUMBER = "PHONE_NUMBER", } -export enum ConnectStatus { - INITIATED = "INITIATED", - COMPLETED = "COMPLETED", - TIMED_OUT = "TIMED_OUT", - DENIED = "DENIED", - CANCELED = "CANCELED", - FAILED = "FAILED", -} - export type AudioLevelChangeHandler = { (audioLevel: AudioLevel): void }; /** @@ -59,15 +50,6 @@ export interface ReadyMetadata { deviceId: string; territory: string; // TODO enum region: string; // TODO enum - connectStatus?: ConnectStatus; - accountId?: string; - sessionId?: string; - from?: string; - fromType?: string; - fromTags?: string; - to?: string; - toType?: string; - toTags?: string; } export interface OutboundConnectionResult { diff --git a/src/v1/signaling.test.ts b/src/v1/signaling.test.ts index 477716b..8c4b84b 100644 --- a/src/v1/signaling.test.ts +++ b/src/v1/signaling.test.ts @@ -94,47 +94,19 @@ describe("Signaling connect method", () => { } }); - test("should emit ready with connectStatus fields when gateway sends ready with connect status data", async () => { + test("should emit established when websocket receives established", async () => { const emitSpy = jest.spyOn(signaling, "emit"); await signaling.connect({ endpointToken: "test-token" }); - // Simulate a second ready event from the gateway that includes connect status fields + // Get the websocket instance and trigger established event const ws = (signaling as any).ws; - const readyCallback = ws.on.mock.calls.find((call: any) => call[0] === "ready")?.[1]; - - if (readyCallback) { - const readyWithConnectStatus = { - endpointId: "test-endpoint", - deviceId: "device-1", - territory: "US", - region: "us-east-1", - connectStatus: "COMPLETED", - accountId: "9900000", - sessionId: "session-1", - from: "ep-1", - fromType: "ENDPOINT", - fromTags: "tag1", - to: "ep-2", - toType: "ENDPOINT", - toTags: "tag2", - }; - readyCallback(readyWithConnectStatus); - expect(emitSpy).toHaveBeenCalledWith( - "ready", - expect.objectContaining({ - endpointId: "test-endpoint", - connectStatus: "COMPLETED", - accountId: "9900000", - sessionId: "session-1", - from: "ep-1", - fromType: "ENDPOINT", - fromTags: "tag1", - to: "ep-2", - toType: "ENDPOINT", - toTags: "tag2", - }), - ); + const establishedCallback = ws.on.mock.calls.find((call: any) => call[0] === "established")?.[1]; + + if (establishedCallback) { + const testEvent = { connectionId: "test-connection" }; + establishedCallback(testEvent); + expect(emitSpy).toHaveBeenCalledWith("established", testEvent); } }); }); diff --git a/src/v1/types.ts b/src/v1/types.ts index 42776fa..7c0d18a 100644 --- a/src/v1/types.ts +++ b/src/v1/types.ts @@ -1,4 +1,4 @@ -import { MediaType, ConnectStatus } from "../types"; +import { MediaType } from "../types"; export interface SetMediaPreferencesWebRtcResponse { endpointId: string; @@ -69,13 +69,4 @@ export interface ReadyMetadata { deviceId: string; territory: string; // TODO enum region: string; // TODO enum - connectStatus?: ConnectStatus; - accountId?: string; - sessionId?: string; - from?: string; - fromType?: string; - fromTags?: string; - to?: string; - toType?: string; - toTags?: string; } From 903a1d556b74f446c81946e6f11fb6fc661fbd1f Mon Sep 17 00:00:00 2001 From: smoghe-bw Date: Tue, 2 Jun 2026 10:48:34 -0400 Subject: [PATCH 13/17] chore: format tsconfig.json for prettier compliance Build was blocked by a prettier check on the tsconfig. No logic changes; prettier --write reformatted the file so npm run build can proceed. Co-Authored-By: Claude Sonnet 4.6 (1M context) --- tsconfig.json | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/tsconfig.json b/tsconfig.json index 381f054..11b6b1c 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -10,7 +10,7 @@ // "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */ "declaration": true /* Generates corresponding '.d.ts' file. */, // "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */ - "sourceMap": false, + "sourceMap": true, // "outFile": "./", /* Concatenate and emit output to single file. */ "outDir": "./dist" /* Redirect output structure to the directory. */, // "rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */ @@ -21,7 +21,6 @@ // "importHelpers": true, /* Import emit helpers from 'tslib'. */ "downlevelIteration": true /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */, // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */ - /* Strict Type-Checking Options */ "strict": true /* Enable all strict type-checking options. */, // "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */ @@ -31,13 +30,11 @@ // "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */ // "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */ // "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */ - /* Additional Checks */ // "noUnusedLocals": true, /* Report errors on unused locals. */ // "noUnusedParameters": true, /* Report errors on unused parameters. */ // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ - /* Module Resolution Options */ // "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */ // "baseUrl": "./", /* Base directory to resolve non-absolute module names. */ @@ -49,13 +46,11 @@ "esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */, // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */ // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ - /* Source Map Options */ // "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */ // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ // "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */ // "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */ - /* Experimental Options */ // "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */ // "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */ From 9db8cd07c8285b73cd595a1606854fe62e3e6b64 Mon Sep 17 00:00:00 2001 From: smoghe-bw Date: Tue, 2 Jun 2026 14:45:42 -0400 Subject: [PATCH 14/17] feat: add autoOpenEgressGate to RtcOptions and pass via setMediaPreferences Defaults to true. When true the gateway immediately re-opens the egress gate after each call ends, so the next call's audio flows without any round-trip delay. Pass false to restore legacy behaviour (gate stays closed until streamAvailable is processed by the gateway). Co-Authored-By: Claude Sonnet 4.6 (1M context) --- src/types.ts | 7 +++++++ src/v1/signaling.ts | 6 +++++- 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/src/types.ts b/src/types.ts index 6c815cb..ff998ff 100644 --- a/src/types.ts +++ b/src/types.ts @@ -35,6 +35,13 @@ export interface RtcOptions { websocketUrl?: string; iceServers?: RTCIceServer[]; iceTransportPolicy?: RTCIceTransportPolicy; + /** + * When true (default), the gateway re-opens the egress gate immediately after + * each call ends so the next call's audio flows without any round-trip delay. + * Set to false to restore the legacy behaviour where the gate stays closed + * between calls until the gateway processes streamAvailable. + */ + autoOpenEgressGate?: boolean; } export interface RtcStream { diff --git a/src/v1/signaling.ts b/src/v1/signaling.ts index 3a77c51..2a72a4c 100644 --- a/src/v1/signaling.ts +++ b/src/v1/signaling.ts @@ -15,6 +15,7 @@ class Signaling extends EventEmitter { private isReady: boolean = false; private readyMetadata: ReadyMetadata | null = null; private diagnosticsBatcher?: DiagnosticsBatcher; + private rtcOptions?: RtcOptions; constructor(diagnosticsBatcher?: DiagnosticsBatcher) { super(); @@ -35,6 +36,7 @@ class Signaling extends EventEmitter { if (options) { rtcOptions = { ...rtcOptions, ...options }; } + this.rtcOptions = rtcOptions; const websocketUrl = `${rtcOptions.websocketUrl}?client=node&sdkVersion=${sdkVersion}&uniqueId=${this.uniqueDeviceId}&endpointToken=${authParams.endpointToken}`; logger.debug(`Connecting to ${websocketUrl}`); console.log(`Connecting to ${websocketUrl}`); @@ -122,9 +124,11 @@ class Signaling extends EventEmitter { } private setMediaPreferences(): Promise<{}> { - logger.debug(`Calling "setMediaPreferences"`, { protocol: "WEBRTC" }); + const autoOpenEgressGate = this.rtcOptions?.autoOpenEgressGate ?? true; + logger.debug(`Calling "setMediaPreferences"`, { protocol: "WEBRTC", autoOpenEgressGate }); return this.ws?.call("setMediaPreferences", { protocol: "WEBRTC", + autoOpenEgressGate, }) as Promise; } From e587b4d48f5e64f065e90bce91155a01cedf68a3 Mon Sep 17 00:00:00 2001 From: smoghe-bw Date: Mon, 15 Jun 2026 16:16:05 -0400 Subject: [PATCH 15/17] fix: route gateway streamAvailable notification to new onInboundStreamNotification callback Previously the signaling-layer streamAvailable WS event was routed through onStreamAvailable, which fires before the WebRTC ontrack event arrives and therefore has no mediaStream. This broke existing consumers who assume onStreamAvailable always carries a populated mediaStream. Introduce onInboundStreamNotification as the dedicated callback for the pre-media gateway notification (carries callId/autoAccepted, no mediaStream). onStreamAvailable continues to fire only from the WebRTC ontrack handler, so mediaStream is always present and existing consumers are unaffected. Co-Authored-By: Claude Sonnet 4.6 (1M context) --- src/bandwidthRtc.ts | 18 ++++++++++++++++++ src/v1/bandwidthRtc.ts | 20 ++++++++++++++++---- 2 files changed, 34 insertions(+), 4 deletions(-) diff --git a/src/bandwidthRtc.ts b/src/bandwidthRtc.ts index 4cec567..acce6b4 100644 --- a/src/bandwidthRtc.ts +++ b/src/bandwidthRtc.ts @@ -22,6 +22,7 @@ class BandwidthRtc { // Event handlers private streamAvailableHandler?: { (event: RtcStream): void }; private streamUnavailableHandler?: { (event: RtcStream): void }; + private inboundStreamNotificationHandler?: { (event: RtcStream): void }; private readyHandler?: { (readyMetadata: ReadyMetadata): void }; private logLevel?: LogLevel; @@ -77,6 +78,10 @@ class BandwidthRtc { this.delegate!.onStreamUnavailable(this.streamUnavailableHandler); } + if (this.inboundStreamNotificationHandler) { + this.delegate!.onInboundStreamNotification(this.inboundStreamNotificationHandler); + } + if (this.readyHandler) { this.delegate!.onReady(this.readyHandler); } @@ -116,6 +121,19 @@ class BandwidthRtc { } } + /** + * Set the function that will be called when the gateway signals that an inbound + * stream is ready to be accepted or declined, before the WebRTC media arrives. + * Use this to drive accept/decline UI; mediaStream will be undefined at this point. + * @param callback callback function + */ + onInboundStreamNotification(callback: { (event: RtcStream): void }): void { + this.inboundStreamNotificationHandler = callback; + if (this.delegate) { + this.delegate.onInboundStreamNotification(callback); + } + } + /** * Set the function that will be called when a subscribed stream becomes unavailable * @param callback callback function diff --git a/src/v1/bandwidthRtc.ts b/src/v1/bandwidthRtc.ts index 1c5bced..6d3fca9 100644 --- a/src/v1/bandwidthRtc.ts +++ b/src/v1/bandwidthRtc.ts @@ -77,6 +77,7 @@ export class BandwidthRtc { // Event handlers private streamAvailableHandler?: { (event: RtcStream): void }; private streamUnavailableHandler?: { (event: RtcStream): void }; + private inboundStreamNotificationHandler?: { (event: RtcStream): void }; private readyHandler?: { (readyMetadata: ReadyMetadata): void }; /** @@ -109,9 +110,9 @@ export class BandwidthRtc { this.signaling.on("ready", this.handleReady.bind(this)); this.signaling.on("sdpOffer", this.handleSubscribeSdpOffer.bind(this)); this.signaling.on("init", this.init.bind(this)); - this.signaling.on("streamAvailable", ({ callId }: { callId: string }) => { - if (this.streamAvailableHandler) { - this.streamAvailableHandler({ mediaTypes: [MediaType.AUDIO], callId }); + this.signaling.on("streamAvailable", ({ callId, autoAccepted }: { callId: string; autoAccepted: boolean }) => { + if (this.inboundStreamNotificationHandler) { + this.inboundStreamNotificationHandler({ mediaTypes: [MediaType.AUDIO], callId, autoAccepted }); } }); this.signaling.on("streamUnavailable", ({ callId }: { callId: string }) => { @@ -134,13 +135,24 @@ export class BandwidthRtc { } /** - * Set the function that will be called when a subscribed stream becomes available + * Set the function that will be called when a subscribed stream becomes available. + * The RtcStream passed to the callback always contains a populated mediaStream. * @param callback callback function */ onStreamAvailable(callback: { (event: RtcStream): void }): void { this.streamAvailableHandler = callback; } + /** + * Set the function that will be called when the gateway signals that an inbound + * stream is ready to be accepted or declined, before the WebRTC media arrives. + * Use this to drive accept/decline UI; mediaStream will be undefined at this point. + * @param callback callback function + */ + onInboundStreamNotification(callback: { (event: RtcStream): void }): void { + this.inboundStreamNotificationHandler = callback; + } + /** * Set the function that will be called when a subscribed stream becomes unavailable * @param callback callback function From 2f76547f041e8e827ad8b724afbfeeace6c8210e Mon Sep 17 00:00:00 2001 From: smoghe-bw Date: Tue, 16 Jun 2026 11:16:49 -0400 Subject: [PATCH 16/17] refactor: rename autoOpenEgressGate to autoAccept --- src/types.ts | 11 ++++++----- src/v1/signaling.ts | 8 ++++---- 2 files changed, 10 insertions(+), 9 deletions(-) diff --git a/src/types.ts b/src/types.ts index ff998ff..1c7f71a 100644 --- a/src/types.ts +++ b/src/types.ts @@ -36,18 +36,19 @@ export interface RtcOptions { iceServers?: RTCIceServer[]; iceTransportPolicy?: RTCIceTransportPolicy; /** - * When true (default), the gateway re-opens the egress gate immediately after - * each call ends so the next call's audio flows without any round-trip delay. - * Set to false to restore the legacy behaviour where the gate stays closed - * between calls until the gateway processes streamAvailable. + * When true (default), the gateway sets autoAccepted=true on the streamAvailable + * notification so the SDK skips the accept/decline prompt and the call connects + * immediately. Set to false to show a prompt and require the user to call + * acceptStream or declineStream. */ - autoOpenEgressGate?: boolean; + autoAccept?: boolean; } export interface RtcStream { mediaTypes: MediaType[]; mediaStream?: MediaStream; callId?: string; + autoAccepted?: boolean; } export class BandwidthRtcError extends Error {} diff --git a/src/v1/signaling.ts b/src/v1/signaling.ts index 2a72a4c..a7b7a19 100644 --- a/src/v1/signaling.ts +++ b/src/v1/signaling.ts @@ -85,7 +85,7 @@ class Signaling extends EventEmitter { resolve(); }); - ws.on("streamAvailable", (event: { callId: string; endpointId: string }) => { + ws.on("streamAvailable", (event: { callId: string; endpointId: string; autoAccepted: boolean }) => { this.emit("streamAvailable", event); }); @@ -124,11 +124,11 @@ class Signaling extends EventEmitter { } private setMediaPreferences(): Promise<{}> { - const autoOpenEgressGate = this.rtcOptions?.autoOpenEgressGate ?? true; - logger.debug(`Calling "setMediaPreferences"`, { protocol: "WEBRTC", autoOpenEgressGate }); + const autoAccept = this.rtcOptions?.autoAccept ?? true; + logger.debug(`Calling "setMediaPreferences"`, { protocol: "WEBRTC", autoAccept }); return this.ws?.call("setMediaPreferences", { protocol: "WEBRTC", - autoOpenEgressGate, + autoAccept, }) as Promise; } From 31035fd7c978d12424ae22c447dc7017bc3378d5 Mon Sep 17 00:00:00 2001 From: smoghe-bw Date: Wed, 17 Jun 2026 09:10:29 -0400 Subject: [PATCH 17/17] Revert "fix: route gateway streamAvailable notification to new onInboundStreamNotification callback" This reverts commit e587b4d48f5e64f065e90bce91155a01cedf68a3. --- src/bandwidthRtc.ts | 18 ------------------ src/v1/bandwidthRtc.ts | 16 ++-------------- 2 files changed, 2 insertions(+), 32 deletions(-) diff --git a/src/bandwidthRtc.ts b/src/bandwidthRtc.ts index acce6b4..4cec567 100644 --- a/src/bandwidthRtc.ts +++ b/src/bandwidthRtc.ts @@ -22,7 +22,6 @@ class BandwidthRtc { // Event handlers private streamAvailableHandler?: { (event: RtcStream): void }; private streamUnavailableHandler?: { (event: RtcStream): void }; - private inboundStreamNotificationHandler?: { (event: RtcStream): void }; private readyHandler?: { (readyMetadata: ReadyMetadata): void }; private logLevel?: LogLevel; @@ -78,10 +77,6 @@ class BandwidthRtc { this.delegate!.onStreamUnavailable(this.streamUnavailableHandler); } - if (this.inboundStreamNotificationHandler) { - this.delegate!.onInboundStreamNotification(this.inboundStreamNotificationHandler); - } - if (this.readyHandler) { this.delegate!.onReady(this.readyHandler); } @@ -121,19 +116,6 @@ class BandwidthRtc { } } - /** - * Set the function that will be called when the gateway signals that an inbound - * stream is ready to be accepted or declined, before the WebRTC media arrives. - * Use this to drive accept/decline UI; mediaStream will be undefined at this point. - * @param callback callback function - */ - onInboundStreamNotification(callback: { (event: RtcStream): void }): void { - this.inboundStreamNotificationHandler = callback; - if (this.delegate) { - this.delegate.onInboundStreamNotification(callback); - } - } - /** * Set the function that will be called when a subscribed stream becomes unavailable * @param callback callback function diff --git a/src/v1/bandwidthRtc.ts b/src/v1/bandwidthRtc.ts index 6d3fca9..b34b199 100644 --- a/src/v1/bandwidthRtc.ts +++ b/src/v1/bandwidthRtc.ts @@ -77,7 +77,6 @@ export class BandwidthRtc { // Event handlers private streamAvailableHandler?: { (event: RtcStream): void }; private streamUnavailableHandler?: { (event: RtcStream): void }; - private inboundStreamNotificationHandler?: { (event: RtcStream): void }; private readyHandler?: { (readyMetadata: ReadyMetadata): void }; /** @@ -111,8 +110,8 @@ export class BandwidthRtc { this.signaling.on("sdpOffer", this.handleSubscribeSdpOffer.bind(this)); this.signaling.on("init", this.init.bind(this)); this.signaling.on("streamAvailable", ({ callId, autoAccepted }: { callId: string; autoAccepted: boolean }) => { - if (this.inboundStreamNotificationHandler) { - this.inboundStreamNotificationHandler({ mediaTypes: [MediaType.AUDIO], callId, autoAccepted }); + if (this.streamAvailableHandler) { + this.streamAvailableHandler({ mediaTypes: [MediaType.AUDIO], callId, autoAccepted }); } }); this.signaling.on("streamUnavailable", ({ callId }: { callId: string }) => { @@ -136,23 +135,12 @@ export class BandwidthRtc { /** * Set the function that will be called when a subscribed stream becomes available. - * The RtcStream passed to the callback always contains a populated mediaStream. * @param callback callback function */ onStreamAvailable(callback: { (event: RtcStream): void }): void { this.streamAvailableHandler = callback; } - /** - * Set the function that will be called when the gateway signals that an inbound - * stream is ready to be accepted or declined, before the WebRTC media arrives. - * Use this to drive accept/decline UI; mediaStream will be undefined at this point. - * @param callback callback function - */ - onInboundStreamNotification(callback: { (event: RtcStream): void }): void { - this.inboundStreamNotificationHandler = callback; - } - /** * Set the function that will be called when a subscribed stream becomes unavailable * @param callback callback function