Skip to content
Merged
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
51 changes: 37 additions & 14 deletions src/v1/bandwidthRtc.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -184,58 +184,81 @@ describe("bandwidthRtcV1 addStreamToPublishingPeerConnection", () => {
expect((brtc as any).localDtmfSenders.has("stream-1")).toBe(false);
});

test("appends telephone-event codec when missing from audio preferences", () => {
test("appends the clock-matched telephone-event 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 te8000 = { mimeType: "audio/telephone-event", clockRate: 8000 };
const te48000 = { mimeType: "audio/telephone-event", clockRate: 48000 };
(global as any).RTCRtpSender = { getCapabilities: jest.fn().mockReturnValue({ codecs: [te8000, te48000] }) };

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

expect(transceiver.setCodecPreferences).toHaveBeenCalledWith([opusCodec, telephoneEventCodec]);
// Must pair with telephone-event/48000 (Opus clock), not the 8000 variant.
expect(transceiver.setCodecPreferences).toHaveBeenCalledWith([opusCodec, te48000]);
});

test("does not duplicate telephone-event when already in preferences", () => {
test("replaces a clock-mismatched telephone-event 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] });
const te8000 = { mimeType: "audio/telephone-event", clockRate: 8000 };
const te48000 = { mimeType: "audio/telephone-event", clockRate: 48000 };
(global as any).RTCRtpSender = { getCapabilities: jest.fn().mockReturnValue({ codecs: [te48000] }) };

expect(transceiver.setCodecPreferences).toHaveBeenCalledWith([opusCodec, telephoneEventCodec]);
(brtc as any).addStreamToPublishingPeerConnection(makeMockStream("stream-1", "audio"), { audio: [opusCodec, te8000] });

// The mismatched 8000 event is dropped and replaced with the 48000 one from capabilities.
expect(transceiver.setCodecPreferences).toHaveBeenCalledWith([opusCodec, te48000]);
expect(transceiver.setCodecPreferences).toHaveBeenCalledTimes(1);
});

test("keeps the clock-matched telephone-event already in preferences", () => {
const brtc = new BandwidthRtc();
const transceiver = makeTransceiver();
withPublishingPeerConnection(brtc, transceiver);

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

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

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

(global as any).RTCRtpSender = { getCapabilities: jest.fn().mockReturnValue({ codecs: [] }) };
// Only a mismatched (8000) telephone-event exists; better to omit it than offer a dropped codec.
const te8000 = { mimeType: "audio/telephone-event", clockRate: 8000 };
(global as any).RTCRtpSender = { getCapabilities: jest.fn().mockReturnValue({ codecs: [te8000] }) };

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

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

test("forces telephone-event into codec preferences even without explicit codecPreferences", () => {
test("forces the clock-matched telephone-event even without explicit codecPreferences", () => {
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 };
(global as any).RTCRtpSender = { getCapabilities: jest.fn().mockReturnValue({ codecs: [opusCodec, telephoneEventCodec] }) };
const te8000 = { mimeType: "audio/telephone-event", clockRate: 8000 };
const te48000 = { mimeType: "audio/telephone-event", clockRate: 48000 };
(global as any).RTCRtpSender = { getCapabilities: jest.fn().mockReturnValue({ codecs: [opusCodec, te8000, te48000] }) };

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

expect(transceiver.setCodecPreferences).toHaveBeenCalledWith([opusCodec, telephoneEventCodec]);
// Full capabilities collapse to the primary codec + its matching-clock telephone-event.
expect(transceiver.setCodecPreferences).toHaveBeenCalledWith([opusCodec, te48000]);
});

test("skips setCodecPreferences when RTCRtpSender is unavailable (e.g. non-browser environment)", () => {
Expand Down
23 changes: 16 additions & 7 deletions src/v1/bandwidthRtc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -703,16 +703,25 @@ export class BandwidthRtc {
// dropped from the SDP offer. Apply it unconditionally (not just when the caller
// passes codecPreferences) so telephone-event is always present and RTCDTMFSender
// can send RFC 4733 DTMF packets, regardless of the browser's default codec offer.
//
// RFC 4733: telephone-event shares the primary audio codec's RTP clock, so it MUST
// be offered at that codec's clock rate (e.g. 48000 alongside Opus). A mismatched
// telephone-event (e.g. 8000 next to Opus/48000) is silently dropped by the peer and
// DTMF never reaches the far end — so drop every telephone-event and re-add only the
// one whose clock rate matches, sourced from prefs or full capabilities.
const audioCapabilities = typeof RTCRtpSender !== "undefined" ? RTCRtpSender.getCapabilities(TRACK_KIND_AUDIO) : undefined;
const audioCodecs = codecPreferences?.audio ?? audioCapabilities?.codecs;
if (audioCodecs) {
const hasTelephoneEvent = audioCodecs.some((c) => c.mimeType.toLowerCase() === TELEPHONE_EVENT_MIME_TYPE);
if (!hasTelephoneEvent) {
const telephoneEventCodec = audioCapabilities?.codecs.find((c) => c.mimeType.toLowerCase() === TELEPHONE_EVENT_MIME_TYPE);
transceiver.setCodecPreferences(telephoneEventCodec ? [...audioCodecs, telephoneEventCodec] : audioCodecs);
} else {
transceiver.setCodecPreferences(audioCodecs);
}
// Strip every telephone-event, then re-add only the one whose clock rate matches the
// primary (first) media codec, preferring one already in the caller's list and falling
// back to full capabilities. If none matches, leave the codecs untouched rather than
// offer a telephone-event the peer will drop.
const isTelephoneEvent = (c: RTCRtpCodec) => c.mimeType.toLowerCase() === TELEPHONE_EVENT_MIME_TYPE;
const mediaCodecs = audioCodecs.filter((c) => !isTelephoneEvent(c));
const primaryClockRate = mediaCodecs[0]?.clockRate;
const matchesPrimary = (c: RTCRtpCodec) => isTelephoneEvent(c) && c.clockRate === primaryClockRate;
const telephoneEventCodec = audioCodecs.find(matchesPrimary) ?? audioCapabilities?.codecs.find(matchesPrimary);
transceiver.setCodecPreferences(telephoneEventCodec ? [...mediaCodecs, telephoneEventCodec] : audioCodecs);
}
} else if (track.kind === TRACK_KIND_VIDEO && codecPreferences?.video) {
transceiver.setCodecPreferences(codecPreferences.video);
Expand Down