Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 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