Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
9 changes: 6 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -83,14 +83,17 @@ Please see the following resources for more information on MediaStreamConstraint

### DTMF

- Description: send a set of VoIP-network-friendly DTMF tones. The tone amplitude and duration can not be controlled
- Description: send DTMF tones via the browser's native `RTCDTMFSender` (RFC 4733). Tones are forwarded as telephone-event RTP packets by the Bandwidth gateway.
- Params:
- tone: the digits to send, as a string, chosen from the set of valid DTMF characters [0-9,*,#,\,]
- streamId (optional): the stream to 'play' the tone on
- tone: the digits to send, as a string, chosen from the set of valid DTMF characters [0-9,*,#,A-D,\,]
- streamId (optional): the stream to send the tone on; defaults to all published streams
- duration (optional): tone duration in milliseconds, between 40 and 6000 (default: 100)
- interToneGap (optional): gap between tones in milliseconds, minimum 30 (default: 70)

```javascript
bandwidthRtc.sendDtmf("3");
bandwidthRtc.sendDtmf("313,3211*#");
bandwidthRtc.sendDtmf("5", undefined, 200, 100); // 200ms tone, 100ms gap
```

## Event Listeners
Expand Down
4 changes: 2 additions & 2 deletions src/bandwidthRtc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -211,12 +211,12 @@ class BandwidthRtc {
return devices;
}

sendDtmf(tone: string, streamId?: string) {
sendDtmf(tone: string, streamId?: string, duration: number = 100, interToneGap: number = 70) {
if (!this.delegate) {
throw new BandwidthRtcError("You must call 'connect' before 'sendDtmf'");
}

return this.delegate.sendDtmf(tone, streamId);
return this.delegate.sendDtmf(tone, streamId, duration, interToneGap);
}

/**
Expand Down
33 changes: 0 additions & 33 deletions src/dtmfSender.test.ts

This file was deleted.

127 changes: 0 additions & 127 deletions src/dtmfSender.ts

This file was deleted.

138 changes: 138 additions & 0 deletions src/v1/bandwidthRtc.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,144 @@ describe("bandwidhthRtcV1 constructor", () => {
});
});

describe("bandwidthRtcV1 sendDtmf", () => {
beforeAll(() => {
setupNavigatorMocks();
setupMocks();
});

function makeDtmfSender() {
return { insertDTMF: jest.fn() };
}

test("calls insertDTMF on all registered senders when no streamId given", () => {
const brtc = new BandwidthRtc();
const sender1 = makeDtmfSender();
const sender2 = makeDtmfSender();
(brtc as any).localDtmfSenders.set("stream-1", sender1);
(brtc as any).localDtmfSenders.set("stream-2", sender2);

brtc.sendDtmf("5");

expect(sender1.insertDTMF).toHaveBeenCalledTimes(1);
expect(sender1.insertDTMF).toHaveBeenCalledWith("5", 100, 70);
expect(sender2.insertDTMF).toHaveBeenCalledTimes(1);
expect(sender2.insertDTMF).toHaveBeenCalledWith("5", 100, 70);
});

test("calls insertDTMF only on the specified stream when streamId given", () => {
const brtc = new BandwidthRtc();
const sender1 = makeDtmfSender();
const sender2 = makeDtmfSender();
(brtc as any).localDtmfSenders.set("stream-1", sender1);
(brtc as any).localDtmfSenders.set("stream-2", sender2);

brtc.sendDtmf("9", "stream-1");

expect(sender1.insertDTMF).toHaveBeenCalledTimes(1);
expect(sender2.insertDTMF).not.toHaveBeenCalled();
});

test("forwards duration and interToneGap to insertDTMF", () => {
const brtc = new BandwidthRtc();
const sender = makeDtmfSender();
(brtc as any).localDtmfSenders.set("stream-1", sender);

brtc.sendDtmf("1", undefined, 200, 80);

expect(sender.insertDTMF).toHaveBeenCalledWith("1", 200, 80);
});

test("does not throw when no senders are registered", () => {
const brtc = new BandwidthRtc();
expect(() => brtc.sendDtmf("5")).not.toThrow();
});

test("does nothing for an unknown streamId", () => {
const brtc = new BandwidthRtc();
const sender = makeDtmfSender();
(brtc as any).localDtmfSenders.set("stream-1", sender);

brtc.sendDtmf("5", "nonexistent");

expect(sender.insertDTMF).not.toHaveBeenCalled();
});
});

describe("bandwidthRtcV1 addStreamToPublishingPeerConnection", () => {
function makeTransceiver(dtmf: RTCDTMFSender | null = { insertDTMF: jest.fn() } as any) {
return { sender: { dtmf }, setCodecPreferences: jest.fn() };
}

function makeMockStream(id: string, trackKind: string) {
return { id, getTracks: () => [{ kind: trackKind, id: "track-1" }] };
}

function withPublishingPeerConnection(brtc: BandwidthRtc, transceiver: ReturnType<typeof makeTransceiver>) {
(brtc as any).publishingPeerConnection = { addTransceiver: jest.fn().mockReturnValue(transceiver) };
}

test("stores dtmf sender for audio track", () => {
const brtc = new BandwidthRtc();
const dtmfSender = { insertDTMF: jest.fn() };
const transceiver = makeTransceiver(dtmfSender as any);
withPublishingPeerConnection(brtc, transceiver);

(brtc as any).addStreamToPublishingPeerConnection(makeMockStream("stream-1", "audio"));

expect((brtc as any).localDtmfSenders.get("stream-1")).toBe(dtmfSender);
});

test("does not store dtmf sender when sender.dtmf is null", () => {
const brtc = new BandwidthRtc();
withPublishingPeerConnection(brtc, makeTransceiver(null));

(brtc as any).addStreamToPublishingPeerConnection(makeMockStream("stream-1", "audio"));

expect((brtc as any).localDtmfSenders.has("stream-1")).toBe(false);
});

test("appends telephone-event codec when missing from audio preferences", () => {
const brtc = new BandwidthRtc();
const transceiver = makeTransceiver();
withPublishingPeerConnection(brtc, transceiver);

const telephoneEventCodec = { mimeType: "audio/telephone-event", clockRate: 8000 };
(global as any).RTCRtpSender = { getCapabilities: jest.fn().mockReturnValue({ codecs: [telephoneEventCodec] }) };

const opusCodec = { mimeType: "audio/opus", clockRate: 48000 };
(brtc as any).addStreamToPublishingPeerConnection(makeMockStream("stream-1", "audio"), { audio: [opusCodec] });

expect(transceiver.setCodecPreferences).toHaveBeenCalledWith([opusCodec, telephoneEventCodec]);
});

test("does not duplicate telephone-event when already in preferences", () => {
const brtc = new BandwidthRtc();
const transceiver = makeTransceiver();
withPublishingPeerConnection(brtc, transceiver);

const opusCodec = { mimeType: "audio/opus", clockRate: 48000 };
const telephoneEventCodec = { mimeType: "audio/telephone-event", clockRate: 8000 };
(brtc as any).addStreamToPublishingPeerConnection(makeMockStream("stream-1", "audio"), { audio: [opusCodec, telephoneEventCodec] });

expect(transceiver.setCodecPreferences).toHaveBeenCalledWith([opusCodec, telephoneEventCodec]);
expect(transceiver.setCodecPreferences).toHaveBeenCalledTimes(1);
});

test("falls back to original preferences when telephone-event not found in capabilities", () => {
const brtc = new BandwidthRtc();
const transceiver = makeTransceiver();
withPublishingPeerConnection(brtc, transceiver);

(global as any).RTCRtpSender = { getCapabilities: jest.fn().mockReturnValue({ codecs: [] }) };

const opusCodec = { mimeType: "audio/opus", clockRate: 48000 };
(brtc as any).addStreamToPublishingPeerConnection(makeMockStream("stream-1", "audio"), { audio: [opusCodec] });

expect(transceiver.setCodecPreferences).toHaveBeenCalledWith([opusCodec]);
});
});

describe("bandwidthRtcV1 connect method", () => {
beforeAll(() => {
setupNavigatorMocks();
Expand Down
Loading