Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
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
26 changes: 22 additions & 4 deletions src/v1/bandwidthRtc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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, autoAccepted }: { callId: string; autoAccepted: boolean }) => {
if (this.streamAvailableHandler) {
this.streamAvailableHandler({ mediaTypes: [MediaType.AUDIO], callId, autoAccepted });
}
});
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");
Expand All @@ -124,7 +134,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 @@ -229,7 +239,7 @@ export class BandwidthRtc {
}
} else {
publishedStreams.push({
mediaStream: stream.mediaStream,
mediaStream: stream.mediaStream!,
});
}
}
Expand Down Expand Up @@ -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)
Expand All @@ -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)
Expand Down Expand Up @@ -337,6 +347,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);
}

private async offerPublishSdp(restartIce: boolean = false): Promise<SdpAnswer> {
if (!this.publishingPeerConnection) {
throw new BandwidthRtcError("No publishing RTCPeerConnection, cannot offer SDP");
Expand Down
105 changes: 105 additions & 0 deletions src/v1/signaling.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,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 () => {
Expand Down
Loading