Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
a95a03d
feat: add connectStatus to readyMetadata on connectStatus event
Apr 24, 2026
af93d6e
feat(rtc): add onConnectStatus event handler
Apr 27, 2026
e2567ea
style: remove extra blank lines in bandwidthRtc.ts and signaling.ts
Apr 27, 2026
0ca235c
test(signaling): add websocket event handler and disconnect tests
Apr 27, 2026
e13ceb6
feat: wire streamAvailable/streamUnavailable WS notifications to onSt…
Jun 2, 2026
be447ae
feat: expose acceptStream/declineStream on outer BandwidthRtc class
Jun 2, 2026
cb7b226
fix: add types field to package.json for TypeScript consumers
Jun 2, 2026
1b86b4e
fix: disable sourceMap output to prevent source-map-loader errors in …
Jun 2, 2026
04d77d3
feat: auto-acceptStream when no onStreamAvailable handler is registered
Jun 2, 2026
1c4ff3a
revert: remove auto-acceptStream shim (gateway handles compat instead)
Jun 2, 2026
042d632
refactor: remove commented-out offerSdp overload
Jun 2, 2026
1dfe346
refactor: remove ConnectStatus enum and connect status fields from Re…
Jun 2, 2026
903a1d5
chore: format tsconfig.json for prettier compliance
Jun 2, 2026
9db8cd0
feat: add autoOpenEgressGate to RtcOptions and pass via setMediaPrefe…
Jun 2, 2026
e587b4d
fix: route gateway streamAvailable notification to new onInboundStrea…
Jun 15, 2026
2f76547
refactor: rename autoOpenEgressGate to autoAccept
Jun 16, 2026
31035fd
Revert "fix: route gateway streamAvailable notification to new onInbo…
Jun 17, 2026
f829bd0
feat: correlate ontrack with WS streamAvailable for backwards-compati…
Jun 25, 2026
166837e
merge: resolve conflict between stream correlation and ICE restart ch…
Jun 25, 2026
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
12 changes: 0 additions & 12 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
14 changes: 14 additions & 0 deletions src/bandwidthRtc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -257,6 +257,20 @@ class BandwidthRtc {
}
return this.delegate.hangupConnection(endpoint, type);
}

acceptStream(callId?: string): Promise<void> {
if (!this.delegate) {
throw new BandwidthRtcError("You must call 'connect' before 'acceptStream'");
}
return this.delegate.acceptStream(callId);
}

declineStream(callId?: string): Promise<void> {
if (!this.delegate) {
throw new BandwidthRtcError("You must call 'connect' before 'declineStream'");
}
return this.delegate.declineStream(callId);
}
}

interface JwtPayload {
Expand Down
11 changes: 10 additions & 1 deletion src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,11 +35,20 @@ export interface RtcOptions {
websocketUrl?: string;
iceServers?: RTCIceServer[];
iceTransportPolicy?: RTCIceTransportPolicy;
/**
* 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.
*/
autoAccept?: boolean;
}

export interface RtcStream {
mediaTypes: MediaType[];
mediaStream: MediaStream;
mediaStream?: MediaStream;
callId?: string;
autoAccepted?: boolean;
}

export class BandwidthRtcError extends Error {}
Expand Down
102 changes: 80 additions & 22 deletions src/v1/bandwidthRtc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,14 @@ export class BandwidthRtc {
private streamUnavailableHandler?: { (event: RtcStream): void };
private readyHandler?: { (readyMetadata: ReadyMetadata): void };

// Pending state for correlating WebRTC ontrack with WS streamAvailable notification.
// ontrack fires at connect time; the WS notification fires when a call actually arrives.
// We hold both until we have each, then fire a single combined onStreamAvailable event.
private pendingSubscribeStream: MediaStream | undefined;
private pendingCallInfo: { callId: string; autoAccepted: boolean } | undefined;
// Prevents double-firing onStreamUnavailable when both WS and onremovetrack fire.
private callIsActive = false;

/**
* Construct a new instance of BandwidthRtc
* @param logLevel desired log level for logs that will appear in the browser's console, optional
Expand Down Expand Up @@ -121,6 +129,33 @@ 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, autoAccepted }: { callId: string; autoAccepted: boolean }) => {
if (this.pendingSubscribeStream) {
this.callIsActive = true;
this.streamAvailableHandler?.({
mediaTypes: [MediaType.AUDIO],
mediaStream: this.pendingSubscribeStream,
callId,
autoAccepted,
});
} else {
// ontrack hasn't fired yet — store and fire once it does
this.pendingCallInfo = { callId, autoAccepted };
}
});
this.signaling.on("streamUnavailable", ({ callId }: { callId: string }) => {
if (!this.callIsActive) {
return;
}
this.callIsActive = false;
this.pendingCallInfo = undefined;
this.streamUnavailableHandler?.({
mediaTypes: [MediaType.AUDIO],
mediaStream: this.pendingSubscribeStream,
callId,
});
});

await this.signaling.connect(authParams, options);
logger.info("Successfully connected");
}
Expand All @@ -135,7 +170,7 @@ 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.
* @param callback callback function
*/
onStreamAvailable(callback: { (event: RtcStream): void }): void {
Expand Down Expand Up @@ -240,7 +275,7 @@ export class BandwidthRtc {
}
} else {
publishedStreams.push({
mediaStream: stream.mediaStream,
mediaStream: stream.mediaStream!,
});
}
}
Expand Down Expand Up @@ -307,7 +342,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)
Expand All @@ -322,7 +357,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)
Expand Down Expand Up @@ -350,6 +385,14 @@ export class BandwidthRtc {
return this.signaling.hangupConnection(endpoint, type);
}

acceptStream(callId?: string): Promise<void> {
return this.signaling.acceptStream(callId);
}

declineStream(callId?: string): Promise<void> {
return this.signaling.declineStream(callId);
}

// Re-publishes the SDP with iceRestart=true to trigger ICE renegotiation after a connection failure.
private async retryIceOnFailed(pc: RTCPeerConnection, shouldRetry: boolean): Promise<void> {
if (!shouldRetry) {
Expand Down Expand Up @@ -502,31 +545,46 @@ export class BandwidthRtc {

stream.onremovetrack = (event) => {
logger.debug("onremovetrack", event);
if (this.streamUnavailableHandler) {
let removedTrack = event.track;
let deleteResult = availableTracks?.delete(removedTrack);
if (deleteResult) {
if (availableTracks?.size === 0) {
logger.debug("onStreamUnavailable", stream.id);
this.streamUnavailableHandler({
mediaTypes: [MediaType.AUDIO],
mediaStream: stream,
});
streamTracks.delete(stream);
} else {
logger.debug("Waiting on tracks to end", availableTracks);
}
// Guard against double-fire: WS streamUnavailable fires first on new gateway.
if (!this.callIsActive) {
return;
}
let removedTrack = event.track;
let deleteResult = availableTracks?.delete(removedTrack);
if (deleteResult) {
if (availableTracks?.size === 0) {
logger.debug("onStreamUnavailable", stream.id);
this.callIsActive = false;
this.pendingCallInfo = undefined;
this.streamUnavailableHandler?.({
mediaTypes: [MediaType.AUDIO],
mediaStream: stream,
});
streamTracks.delete(stream);
} else {
logger.debug("Waiting on tracks to end", availableTracks);
}
}
};
if (this.streamAvailableHandler) {
logger.debug("onStreamAvailable", stream.id);
this.streamAvailableHandler({

// Store the subscribe stream. We fire onStreamAvailable only when the WS
// streamAvailable notification arrives (which carries callId/autoAccepted), so
// that customers always receive a single event with mediaStream set. If the WS
// notification already arrived before ontrack, fire the combined event now.
this.pendingSubscribeStream = stream;
if (this.pendingCallInfo) {
const { callId, autoAccepted } = this.pendingCallInfo;
this.pendingCallInfo = undefined;
this.callIsActive = true;
logger.debug("onStreamAvailable (deferred)", stream.id);
this.streamAvailableHandler?.({
mediaTypes: [MediaType.AUDIO],
mediaStream: stream,
callId,
autoAccepted,
});
} else {
logger.debug("Waiting on additional tracks");
logger.debug("ontrack: stored subscribe stream, waiting for WS streamAvailable", stream.id);
}
}
};
Expand Down
Loading