Skip to content

Commit a750236

Browse files
authored
Merge pull request #7 from Bandwidth/feat/dtmf-duration-and-null-safety
VAPI-3354 feat(dtmf): expose duration/interToneGap and fix null safety on RTCDTMFSender
1 parent 22921b4 commit a750236

6 files changed

Lines changed: 234 additions & 211 deletions

File tree

README.md

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -83,14 +83,17 @@ Please see the following resources for more information on MediaStreamConstraint
8383

8484
### DTMF
8585

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

9193
```javascript
9294
bandwidthRtc.sendDtmf("3");
9395
bandwidthRtc.sendDtmf("313,3211*#");
96+
bandwidthRtc.sendDtmf("5", undefined, 200, 100); // 200ms tone, 100ms gap
9497
```
9598

9699
## Event Listeners

src/bandwidthRtc.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -211,12 +211,12 @@ class BandwidthRtc {
211211
return devices;
212212
}
213213

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

219-
return this.delegate.sendDtmf(tone, streamId);
219+
return this.delegate.sendDtmf(tone, streamId, duration, interToneGap);
220220
}
221221

222222
/**

src/dtmfSender.test.ts

Lines changed: 0 additions & 33 deletions
This file was deleted.

src/dtmfSender.ts

Lines changed: 0 additions & 127 deletions
This file was deleted.

src/v1/bandwidthRtc.test.ts

Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,144 @@ describe("bandwidhthRtcV1 constructor", () => {
5555
});
5656
});
5757

58+
describe("bandwidthRtcV1 sendDtmf", () => {
59+
beforeAll(() => {
60+
setupNavigatorMocks();
61+
setupMocks();
62+
});
63+
64+
function makeDtmfSender() {
65+
return { insertDTMF: jest.fn() };
66+
}
67+
68+
test("calls insertDTMF on all registered senders when no streamId given", () => {
69+
const brtc = new BandwidthRtc();
70+
const sender1 = makeDtmfSender();
71+
const sender2 = makeDtmfSender();
72+
(brtc as any).localDtmfSenders.set("stream-1", sender1);
73+
(brtc as any).localDtmfSenders.set("stream-2", sender2);
74+
75+
brtc.sendDtmf("5");
76+
77+
expect(sender1.insertDTMF).toHaveBeenCalledTimes(1);
78+
expect(sender1.insertDTMF).toHaveBeenCalledWith("5", 100, 70);
79+
expect(sender2.insertDTMF).toHaveBeenCalledTimes(1);
80+
expect(sender2.insertDTMF).toHaveBeenCalledWith("5", 100, 70);
81+
});
82+
83+
test("calls insertDTMF only on the specified stream when streamId given", () => {
84+
const brtc = new BandwidthRtc();
85+
const sender1 = makeDtmfSender();
86+
const sender2 = makeDtmfSender();
87+
(brtc as any).localDtmfSenders.set("stream-1", sender1);
88+
(brtc as any).localDtmfSenders.set("stream-2", sender2);
89+
90+
brtc.sendDtmf("9", "stream-1");
91+
92+
expect(sender1.insertDTMF).toHaveBeenCalledTimes(1);
93+
expect(sender2.insertDTMF).not.toHaveBeenCalled();
94+
});
95+
96+
test("forwards duration and interToneGap to insertDTMF", () => {
97+
const brtc = new BandwidthRtc();
98+
const sender = makeDtmfSender();
99+
(brtc as any).localDtmfSenders.set("stream-1", sender);
100+
101+
brtc.sendDtmf("1", undefined, 200, 80);
102+
103+
expect(sender.insertDTMF).toHaveBeenCalledWith("1", 200, 80);
104+
});
105+
106+
test("does not throw when no senders are registered", () => {
107+
const brtc = new BandwidthRtc();
108+
expect(() => brtc.sendDtmf("5")).not.toThrow();
109+
});
110+
111+
test("does nothing for an unknown streamId", () => {
112+
const brtc = new BandwidthRtc();
113+
const sender = makeDtmfSender();
114+
(brtc as any).localDtmfSenders.set("stream-1", sender);
115+
116+
brtc.sendDtmf("5", "nonexistent");
117+
118+
expect(sender.insertDTMF).not.toHaveBeenCalled();
119+
});
120+
});
121+
122+
describe("bandwidthRtcV1 addStreamToPublishingPeerConnection", () => {
123+
function makeTransceiver(dtmf: RTCDTMFSender | null = { insertDTMF: jest.fn() } as any) {
124+
return { sender: { dtmf }, setCodecPreferences: jest.fn() };
125+
}
126+
127+
function makeMockStream(id: string, trackKind: string) {
128+
return { id, getTracks: () => [{ kind: trackKind, id: "track-1" }] };
129+
}
130+
131+
function withPublishingPeerConnection(brtc: BandwidthRtc, transceiver: ReturnType<typeof makeTransceiver>) {
132+
(brtc as any).publishingPeerConnection = { addTransceiver: jest.fn().mockReturnValue(transceiver) };
133+
}
134+
135+
test("stores dtmf sender for audio track", () => {
136+
const brtc = new BandwidthRtc();
137+
const dtmfSender = { insertDTMF: jest.fn() };
138+
const transceiver = makeTransceiver(dtmfSender as any);
139+
withPublishingPeerConnection(brtc, transceiver);
140+
141+
(brtc as any).addStreamToPublishingPeerConnection(makeMockStream("stream-1", "audio"));
142+
143+
expect((brtc as any).localDtmfSenders.get("stream-1")).toBe(dtmfSender);
144+
});
145+
146+
test("does not store dtmf sender when sender.dtmf is null", () => {
147+
const brtc = new BandwidthRtc();
148+
withPublishingPeerConnection(brtc, makeTransceiver(null));
149+
150+
(brtc as any).addStreamToPublishingPeerConnection(makeMockStream("stream-1", "audio"));
151+
152+
expect((brtc as any).localDtmfSenders.has("stream-1")).toBe(false);
153+
});
154+
155+
test("appends telephone-event codec when missing from audio preferences", () => {
156+
const brtc = new BandwidthRtc();
157+
const transceiver = makeTransceiver();
158+
withPublishingPeerConnection(brtc, transceiver);
159+
160+
const telephoneEventCodec = { mimeType: "audio/telephone-event", clockRate: 8000 };
161+
(global as any).RTCRtpSender = { getCapabilities: jest.fn().mockReturnValue({ codecs: [telephoneEventCodec] }) };
162+
163+
const opusCodec = { mimeType: "audio/opus", clockRate: 48000 };
164+
(brtc as any).addStreamToPublishingPeerConnection(makeMockStream("stream-1", "audio"), { audio: [opusCodec] });
165+
166+
expect(transceiver.setCodecPreferences).toHaveBeenCalledWith([opusCodec, telephoneEventCodec]);
167+
});
168+
169+
test("does not duplicate telephone-event when already in preferences", () => {
170+
const brtc = new BandwidthRtc();
171+
const transceiver = makeTransceiver();
172+
withPublishingPeerConnection(brtc, transceiver);
173+
174+
const opusCodec = { mimeType: "audio/opus", clockRate: 48000 };
175+
const telephoneEventCodec = { mimeType: "audio/telephone-event", clockRate: 8000 };
176+
(brtc as any).addStreamToPublishingPeerConnection(makeMockStream("stream-1", "audio"), { audio: [opusCodec, telephoneEventCodec] });
177+
178+
expect(transceiver.setCodecPreferences).toHaveBeenCalledWith([opusCodec, telephoneEventCodec]);
179+
expect(transceiver.setCodecPreferences).toHaveBeenCalledTimes(1);
180+
});
181+
182+
test("falls back to original preferences when telephone-event not found in capabilities", () => {
183+
const brtc = new BandwidthRtc();
184+
const transceiver = makeTransceiver();
185+
withPublishingPeerConnection(brtc, transceiver);
186+
187+
(global as any).RTCRtpSender = { getCapabilities: jest.fn().mockReturnValue({ codecs: [] }) };
188+
189+
const opusCodec = { mimeType: "audio/opus", clockRate: 48000 };
190+
(brtc as any).addStreamToPublishingPeerConnection(makeMockStream("stream-1", "audio"), { audio: [opusCodec] });
191+
192+
expect(transceiver.setCodecPreferences).toHaveBeenCalledWith([opusCodec]);
193+
});
194+
});
195+
58196
describe("bandwidthRtcV1 connect method", () => {
59197
beforeAll(() => {
60198
setupNavigatorMocks();

0 commit comments

Comments
 (0)