From e11212b45b549f59fa84450c573861df23b69814 Mon Sep 17 00:00:00 2001 From: Denys Smirnov Date: Thu, 27 Aug 2026 19:46:25 +0200 Subject: [PATCH] Match audio sample rate for DTMF. --- dtmf/dtmf.go | 30 ++++----- dtmf/dtmf_test.go | 157 ++++++++++++++++++++++++---------------------- resample_test.go | 4 +- sdp/offer.go | 105 ++++++++++++++++++++++++------- sdp/offer_test.go | 147 ++++++++++++++++++++++++++++--------------- 5 files changed, 279 insertions(+), 164 deletions(-) diff --git a/dtmf/dtmf.go b/dtmf/dtmf.go index 093ceb6..d9e6412 100644 --- a/dtmf/dtmf.go +++ b/dtmf/dtmf.go @@ -17,6 +17,7 @@ package dtmf import ( "context" "encoding/binary" + "fmt" "io" "math" "time" @@ -27,19 +28,20 @@ import ( ) const ( - SDPNameOnly = "telephone-event" - SDPNameAndRate = SDPNameOnly + "/8000" - SDPName = SDPNameAndRate // Deprecated: use SDPNameOnly or SDPNameAndRate + SDPNameOnly = "telephone-event" ) -const SampleRate = 8000 func init() { - media.RegisterCodec(media.NewCodec(media.CodecInfo{ - SDPName: SDPNameAndRate, - SampleRate: SampleRate, - RTPIsStatic: false, - Priority: -100, // let it be last in SDP - })) + for i, rate := range []int{ + 8000, 16000, 48000, + } { + media.RegisterCodec(media.NewCodec(media.CodecInfo{ + SDPName: fmt.Sprintf("%s/%d", SDPNameOnly, rate), + SampleRate: rate, + RTPIsStatic: false, + Priority: -100 - i, // let it be last in SDP + })) + } } const ( @@ -219,7 +221,7 @@ func Encode(out []byte, ev Event) (int, error) { // Write in-band (analog) and off-band (digital) DTMF tones to audio and RTP streams respectively. // // Digits may contain a special character 'w' which adds a 0.5 sec delay. -func Write(ctx context.Context, audio media.Writer[media.PCM16Sample], events *rtp.Stream, startTs uint32, digits string) error { +func Write(ctx context.Context, audio media.Writer[media.PCM16Sample], events *rtp.Stream, eventsRate int, startTs uint32, digits string) error { const framesPerSec = int(time.Second / rtp.DefFrameDur) var ( buf [4]byte @@ -247,7 +249,7 @@ func Write(ctx context.Context, audio media.Writer[media.PCM16Sample], events *r totalDur = dt nextDelay = 0 if events != nil { - events.Delay(uint32(dt / (time.Second / SampleRate))) + events.Delay(uint32(dt / (time.Second / time.Duration(eventsRate)))) } } @@ -303,7 +305,7 @@ func Write(ctx context.Context, audio media.Writer[media.PCM16Sample], events *r n, err := Encode(buf[:], Event{ Code: code, Volume: eventVolume, - Dur: uint16(dur / (time.Second / SampleRate)), + Dur: uint16(dur / (time.Second / time.Duration(eventsRate))), End: end, }) if err != nil { @@ -323,7 +325,7 @@ func Write(ctx context.Context, audio media.Writer[media.PCM16Sample], events *r return err } // advance the timestamp now - events.Delay(uint32(totalDur / (time.Second / SampleRate))) + events.Delay(uint32(totalDur / (time.Second / time.Duration(eventsRate)))) } } remaining -= step diff --git a/dtmf/dtmf_test.go b/dtmf/dtmf_test.go index f4dc93d..5c6f577 100644 --- a/dtmf/dtmf_test.go +++ b/dtmf/dtmf_test.go @@ -17,6 +17,7 @@ package dtmf import ( "context" "encoding/hex" + "strconv" "testing" "time" @@ -132,83 +133,87 @@ func TestDecodeRTPWithEnd(t *testing.T) { func TestDTMFDelay(t *testing.T) { const startTime = 1242 - var buf rtp.Buffer - w := rtp.NewSeqWriter(&buf).NewStream(101, SampleRate) - err := Write(context.Background(), nil, w, startTime, "1w23") - require.NoError(t, err) + for _, eventsRate := range []int{ + 8000, 16000, + } { + t.Run(strconv.Itoa(eventsRate), func(t *testing.T) { + var buf rtp.Buffer + w := rtp.NewSeqWriter(&buf).NewStream(101, eventsRate) + err := Write(context.Background(), nil, w, eventsRate, startTime, "1w23") + require.NoError(t, err) - type packet struct { - SequenceNumber uint16 - Timestamp uint32 - Marker bool - Event - } - var ( - exp []packet - seq uint16 - ts uint32 - ) - const ( - packetDur = uint32(SampleRate / int(time.Second/rtp.DefFrameDur)) - ) - - ts = startTime - - expectDigit := func(code byte, digit byte) { - start := ts - const n = 13 - for i := 0; i < n-1; i++ { - exp = append(exp, packet{ - SequenceNumber: seq, - Timestamp: start, // should be the same for all events - Marker: i == 0, - Event: Event{ - Code: code, - Digit: digit, - Volume: eventVolume, - Dur: uint16(i+1) * uint16(packetDur), - End: false, - }, - }) - ts += packetDur - seq++ - } - // end event must be sent 3 times with the same duration - for i := 0; i < 3; i++ { - exp = append(exp, packet{ - SequenceNumber: seq, - Timestamp: start, // should be the same for all events - Marker: false, - Event: Event{ - Code: code, - Digit: digit, - Volume: eventVolume, - Dur: uint16(n) * uint16(packetDur), - End: true, - }, - }) - seq++ - } - ts += packetDur - // delay between digits - ts += uint32(eventDur / (time.Second / SampleRate)) - // rounding error (12.5 events in a sec) - ts -= packetDur / 2 - } - expectDigit(1, '1') - ts += SampleRate / 2 // 500ms delay - expectDigit(2, '2') - expectDigit(3, '3') - var got []packet - for _, p := range buf { - e, err := Decode(p.Payload) - require.NoError(t, err) - got = append(got, packet{ - SequenceNumber: p.SequenceNumber, - Timestamp: p.Timestamp, - Marker: p.Marker, - Event: e, + type packet struct { + SequenceNumber uint16 + Timestamp uint32 + Marker bool + Event + } + var ( + exp []packet + seq uint16 + ts uint32 + ) + packetDur := uint32(eventsRate / int(time.Second/rtp.DefFrameDur)) + + ts = startTime + + expectDigit := func(code byte, digit byte) { + start := ts + const n = 13 + for i := 0; i < n-1; i++ { + exp = append(exp, packet{ + SequenceNumber: seq, + Timestamp: start, // should be the same for all events + Marker: i == 0, + Event: Event{ + Code: code, + Digit: digit, + Volume: eventVolume, + Dur: uint16(i+1) * uint16(packetDur), + End: false, + }, + }) + ts += packetDur + seq++ + } + // end event must be sent 3 times with the same duration + for i := 0; i < 3; i++ { + exp = append(exp, packet{ + SequenceNumber: seq, + Timestamp: start, // should be the same for all events + Marker: false, + Event: Event{ + Code: code, + Digit: digit, + Volume: eventVolume, + Dur: uint16(n) * uint16(packetDur), + End: true, + }, + }) + seq++ + } + ts += packetDur + // delay between digits + ts += uint32(eventDur / (time.Second / time.Duration(eventsRate))) + // rounding error (12.5 events in a sec) + ts -= packetDur / 2 + } + expectDigit(1, '1') + ts += uint32(eventsRate) / 2 // 500ms delay + expectDigit(2, '2') + expectDigit(3, '3') + var got []packet + for _, p := range buf { + e, err := Decode(p.Payload) + require.NoError(t, err) + got = append(got, packet{ + SequenceNumber: p.SequenceNumber, + Timestamp: p.Timestamp, + Marker: p.Marker, + Event: e, + }) + } + require.Equal(t, exp, got) }) } - require.Equal(t, exp, got) } diff --git a/resample_test.go b/resample_test.go index e71b5d6..0ae4138 100644 --- a/resample_test.go +++ b/resample_test.go @@ -195,8 +195,8 @@ func memstats(pid int) int64 { } func TestResampleLeak(t *testing.T) { - if runtime.GOOS == "windows" { - t.Skip("windows is not supported for this test") + if runtime.GOOS != "linux" { + t.Skip("only linux supported for this test") } pid := os.Getpid() diff --git a/sdp/offer.go b/sdp/offer.go index 5bd0fb3..4c574f8 100644 --- a/sdp/offer.go +++ b/sdp/offer.go @@ -95,9 +95,14 @@ func OfferCodecs() []CodecInfo { return OfferCodecsWith(media.GlobalCodecs()) } +type DTMFInfo struct { + Type byte + Rate int +} + type MediaDesc struct { Codecs []CodecInfo - DTMFType byte // set to 0 if there's no DTMF + DTMF []DTMFInfo CryptoProfiles []srtp.Profile Direction sdp.Direction } @@ -130,11 +135,23 @@ func OfferMediaWith(s *media.CodecSet, rtpListenerPort int, encrypted Encryption codecs := OfferCodecsWith(s) attrs := make([]sdp.Attribute, 0, len(codecs)+4) formats := make([]string, 0, len(codecs)) - dtmfType := byte(0) + var dtmfTypes = make(map[int]byte) + // Index supported DTMF rates first. for _, codec := range codecs { ci := codec.Codec.Info() - if ci.SDPName == dtmf.SDPNameAndRate { - dtmfType = codec.Type + if strings.HasPrefix(ci.SDPName, dtmf.SDPNameOnly+"/") { + dtmfTypes[ci.RTPClockRate] = codec.Type + } + } + var ratesForDTMF []int + for _, codec := range codecs { + ci := codec.Codec.Info() + if strings.HasPrefix(ci.SDPName, dtmf.SDPNameOnly+"/") { + continue + } + dtmfRate := ci.RTPClockRate + if _, ok := dtmfTypes[dtmfRate]; ok && !slices.Contains(ratesForDTMF, dtmfRate) { + ratesForDTMF = append(ratesForDTMF, dtmfRate) } styp := strconv.Itoa(int(codec.Type)) formats = append(formats, styp) @@ -149,9 +166,21 @@ func OfferMediaWith(s *media.CodecSet, rtpListenerPort int, encrypted Encryption }) } } - if dtmfType > 0 { + slices.Sort(ratesForDTMF) + var dtmfList []DTMFInfo + for _, rate := range ratesForDTMF { + typ, ok := dtmfTypes[rate] + if !ok { + continue + } + dtmfList = append(dtmfList, DTMFInfo{Type: typ, Rate: rate}) + styp := strconv.Itoa(int(typ)) + formats = append(formats, styp) attrs = append(attrs, sdp.Attribute{ - Key: "fmtp", Value: fmt.Sprintf("%d 0-16", dtmfType), + Key: "rtpmap", + Value: fmt.Sprintf("%s %s/%d", styp, dtmf.SDPNameOnly, rate), + }, sdp.Attribute{ + Key: "fmtp", Value: fmt.Sprintf("%d 0-16", typ), }) } var cryptoProfiles []srtp.Profile @@ -173,10 +202,9 @@ func OfferMediaWith(s *media.CodecSet, rtpListenerPort int, encrypted Encryption if encrypted != EncryptionNone { proto = "SAVP" } - return MediaDesc{ Codecs: codecs, - DTMFType: dtmfType, + DTMF: dtmfList, CryptoProfiles: cryptoProfiles, }, &sdp.MediaDescription{ MediaName: sdp.MediaName{ @@ -213,11 +241,11 @@ func AnswerMedia(rtpListenerPort int, audio *AudioConfig, crypt *srtp.Profile) * } formats := make([]string, 0, 2) formats = append(formats, strconv.Itoa(int(audio.Type))) - if audio.DTMFType != 0 { - formats = append(formats, strconv.Itoa(int(audio.DTMFType))) + if d := audio.DTMF; d != nil { + formats = append(formats, strconv.Itoa(int(d.Type))) attrs = append(attrs, []sdp.Attribute{ - {Key: "rtpmap", Value: fmt.Sprintf("%d %s", audio.DTMFType, dtmf.SDPNameAndRate)}, - {Key: "fmtp", Value: fmt.Sprintf("%d 0-16", audio.DTMFType)}, + {Key: "rtpmap", Value: fmt.Sprintf("%d %d", d.Type, d.Rate)}, + {Key: "fmtp", Value: fmt.Sprintf("%d 0-16", d.Type)}, }...) } proto := "AVP" @@ -369,6 +397,10 @@ func (d *Offer) Answer(publicIp netip.Addr, rtpListenerPort int, enc Encryption, MediaDescriptions: []*sdp.MediaDescription{mediaDesc}, } src := netip.AddrPortFrom(publicIp, uint16(rtpListenerPort)) + var dtmfList []DTMFInfo + if d := audio.DTMF; d != nil { + dtmfList = []DTMFInfo{*d} + } return &Answer{ SDP: answer, Addr: src, @@ -376,7 +408,7 @@ func (d *Offer) Answer(publicIp netip.Addr, rtpListenerPort int, enc Encryption, Codecs: []CodecInfo{ {Type: audio.Type, Codec: audio.Codec}, }, - DTMFType: audio.DTMFType, + DTMF: dtmfList, }, }, &MediaConfig{ Local: src, @@ -710,8 +742,16 @@ func ParseMediaWith(s *media.CodecSet, d *sdp.MediaDescription) (*MediaDesc, err continue } name := sub[1] - if name == dtmf.SDPNameAndRate || name == dtmf.SDPNameAndRate+"/1" { - out.DTMFType = byte(typ) + if par, ok := strings.CutPrefix(name, dtmf.SDPNameOnly+"/"); ok { + srate := par + if i := strings.IndexByte(srate, '/'); i != -1 { + srate = srate[:i] + } + rate, err := strconv.Atoi(srate) + if err != nil { + continue + } + out.DTMF = append(out.DTMF, DTMFInfo{Type: byte(typ), Rate: rate}) continue } c := getCodec(typ) @@ -793,9 +833,9 @@ type MediaConfig struct { } type AudioConfig struct { - Codec media.AudioCodec - Type byte - DTMFType byte + Codec media.AudioCodec + Type byte + DTMF *DTMFInfo } func SelectAudio(desc MediaDesc, answer bool) (*AudioConfig, error) { @@ -821,11 +861,30 @@ func SelectAudio(desc MediaDesc, answer bool) (*AudioConfig, error) { if audioCodec == nil { return nil, ErrNoCommonMedia } - return &AudioConfig{ - Codec: audioCodec, - Type: audioType, - DTMFType: desc.DTMFType, - }, nil + audioInfo := audioCodec.Info() + c := &AudioConfig{ + Codec: audioCodec, + Type: audioType, + } + di := slices.IndexFunc(desc.DTMF, func(d DTMFInfo) bool { + return d.Rate == audioInfo.RTPClockRate + }) + if di < 0 { + maxRate := -1 + for i, d := range desc.DTMF { + if d.Rate > audioInfo.RTPClockRate { + continue + } + if maxRate < 0 || d.Rate > maxRate { + maxRate = d.Rate + di = i + } + } + } + if di >= 0 { + c.DTMF = new(desc.DTMF[di]) + } + return c, nil } func SelectCrypto(offer, answer []srtp.Profile, swap bool) (*srtp.Config, *srtp.Profile, error) { diff --git a/sdp/offer_test.go b/sdp/offer_test.go index 273264f..78090ed 100644 --- a/sdp/offer_test.go +++ b/sdp/offer_test.go @@ -63,7 +63,7 @@ func TestSDPMediaOffer(t *testing.T) { Media: "audio", Port: sdp.RangedPort{Value: port}, Protos: []string{"RTP", "AVP"}, - Formats: []string{"101", "9", "0", "8", "102"}, + Formats: []string{"101", "9", "0", "8", "102", "103"}, }, Attributes: []sdp.Attribute{ {Key: "rtpmap", Value: "101 AMR-WB/16000"}, @@ -73,6 +73,8 @@ func TestSDPMediaOffer(t *testing.T) { {Key: "rtpmap", Value: "8 PCMA/8000"}, {Key: "rtpmap", Value: "102 telephone-event/8000"}, {Key: "fmtp", Value: "102 0-16"}, + {Key: "rtpmap", Value: "103 telephone-event/16000"}, + {Key: "fmtp", Value: "103 0-16"}, {Key: "ptime", Value: "20"}, {Key: "sendrecv"}, }, @@ -89,7 +91,7 @@ func TestSDPMediaOffer(t *testing.T) { Media: "audio", Port: sdp.RangedPort{Value: port}, Protos: []string{"RTP", "SAVP"}, - Formats: []string{"101", "9", "0", "8", "102"}, + Formats: []string{"101", "9", "0", "8", "102", "103"}, }, Attributes: []sdp.Attribute{ {Key: "rtpmap", Value: "101 AMR-WB/16000"}, @@ -99,6 +101,8 @@ func TestSDPMediaOffer(t *testing.T) { {Key: "rtpmap", Value: "8 PCMA/8000"}, {Key: "rtpmap", Value: "102 telephone-event/8000"}, {Key: "fmtp", Value: "102 0-16"}, + {Key: "rtpmap", Value: "103 telephone-event/16000"}, + {Key: "fmtp", Value: "103 0-16"}, {Key: "crypto", Value: "1 AES_CM_128_HMAC_SHA1_80 inline:" + getInline(offer.Attributes[i+0].Value)}, {Key: "crypto", Value: "2 AES_CM_128_HMAC_SHA1_32 inline:" + getInline(offer.Attributes[i+1].Value)}, {Key: "crypto", Value: "3 AES_256_CM_HMAC_SHA1_80 inline:" + getInline(offer.Attributes[i+2].Value)}, @@ -157,9 +161,9 @@ func TestSDPMediaAnswer(t *testing.T) { }, }, exp: &AudioConfig{ - Codec: getCodec(g, g722.SDPNameAndRate), - Type: 9, - DTMFType: 101, + Codec: getCodec(g, g722.SDPNameAndRate), + Type: 9, + DTMF: &DTMFInfo{Type: 101, Rate: 8000}, }, }, { @@ -175,9 +179,9 @@ func TestSDPMediaAnswer(t *testing.T) { }, }, exp: &AudioConfig{ - Codec: getCodec(g, g722.SDPNameAndRate), - Type: 9, - DTMFType: 101, + Codec: getCodec(g, g722.SDPNameAndRate), + Type: 9, + DTMF: &DTMFInfo{Type: 101, Rate: 8000}, }, }, { @@ -209,43 +213,45 @@ func TestSDPMediaAnswer(t *testing.T) { }, }, exp: &AudioConfig{ - Codec: getCodec(g, g722.SDPNameAndRate), - Type: 9, - DTMFType: 103, + Codec: getCodec(g, g722.SDPNameAndRate), + Type: 9, + DTMF: &DTMFInfo{Type: 103, Rate: 8000}, }, }, { name: "only ulaw", offer: sdp.MediaDescription{ MediaName: sdp.MediaName{ - Formats: []string{"0", "101"}, + Formats: []string{"0", "101", "102"}, }, Attributes: []sdp.Attribute{ {Key: "rtpmap", Value: "0 PCMU/8000"}, {Key: "rtpmap", Value: "101 telephone-event/8000"}, + {Key: "rtpmap", Value: "102 telephone-event/16000"}, }, }, exp: &AudioConfig{ - Codec: getCodec(g, g711.ULawSDPNameAndRate), - Type: 0, - DTMFType: 101, + Codec: getCodec(g, g711.ULawSDPNameAndRate), + Type: 0, + DTMF: &DTMFInfo{Type: 101, Rate: 8000}, }, }, { name: "only g722", offer: sdp.MediaDescription{ MediaName: sdp.MediaName{ - Formats: []string{"9", "101"}, + Formats: []string{"9", "101", "102"}, }, Attributes: []sdp.Attribute{ {Key: "rtpmap", Value: "9 G722/8000"}, {Key: "rtpmap", Value: "101 telephone-event/8000"}, + {Key: "rtpmap", Value: "102 telephone-event/16000"}, }, }, exp: &AudioConfig{ - Codec: getCodec(g, g722.SDPNameAndRate), - Type: 9, - DTMFType: 101, + Codec: getCodec(g, g722.SDPNameAndRate), + Type: 9, + DTMF: &DTMFInfo{Type: 101, Rate: 8000}, }, }, { @@ -272,9 +278,9 @@ func TestSDPMediaAnswer(t *testing.T) { }, }, exp: &AudioConfig{ - Codec: getCodec(g, g711.ULawSDPNameAndRate), - Type: 0, - DTMFType: 101, + Codec: getCodec(g, g711.ULawSDPNameAndRate), + Type: 0, + DTMF: &DTMFInfo{Type: 101, Rate: 8000}, }, }, { @@ -286,9 +292,9 @@ func TestSDPMediaAnswer(t *testing.T) { }, }, exp: &AudioConfig{ - Codec: getCodec(g, g711.ULawSDPNameAndRate), - Type: 0, - DTMFType: 101, + Codec: getCodec(g, g711.ULawSDPNameAndRate), + Type: 0, + DTMF: &DTMFInfo{Type: 101, Rate: 8000}, }, }, { @@ -340,6 +346,45 @@ func TestSDPMediaAnswer(t *testing.T) { Type: 101, }, }, + { + name: "amrwb dtmf", + offer: sdp.MediaDescription{ + MediaName: sdp.MediaName{ + Formats: []string{"101", "0", "103", "104"}, + }, + Attributes: []sdp.Attribute{ + {Key: "rtpmap", Value: "101 AMR-WB/16000"}, + {Key: "rtpmap", Value: "0 PCMU/8000"}, + {Key: "rtpmap", Value: "103 telephone-event/8000"}, + {Key: "rtpmap", Value: "104 telephone-event/16000"}, + }, + }, + exp: &AudioConfig{ + // Pick AMR-WB, assume missing parameter matches ours. + Codec: getCodec(g, amrwb.SDPNameAndRate, media.CodecParam{"octet-align", "0"}), + Type: 101, + DTMF: &DTMFInfo{Type: 104, Rate: 16000}, + }, + }, + { + name: "amrwb low", + offer: sdp.MediaDescription{ + MediaName: sdp.MediaName{ + Formats: []string{"101", "0", "103"}, + }, + Attributes: []sdp.Attribute{ + {Key: "rtpmap", Value: "101 AMR-WB/16000"}, + {Key: "rtpmap", Value: "0 PCMU/8000"}, + {Key: "rtpmap", Value: "103 telephone-event/8000"}, + }, + }, + exp: &AudioConfig{ + // Pick AMR-WB, assume missing parameter matches ours. + Codec: getCodec(g, amrwb.SDPNameAndRate, media.CodecParam{"octet-align", "0"}), + Type: 101, + DTMF: &DTMFInfo{Type: 103, Rate: 8000}, + }, + }, { name: "amrwb bandwidth efficient", offer: sdp.MediaDescription{ @@ -392,27 +437,31 @@ func TestSDPMediaAnswer(t *testing.T) { require.Equal(t, c.exp, got) }) } - _, offer, err := OfferMediaWith(g, port, EncryptionNone) - require.NoError(t, err) - require.Equal(t, &sdp.MediaDescription{ - MediaName: sdp.MediaName{ - Media: "audio", - Port: sdp.RangedPort{Value: port}, - Protos: []string{"RTP", "AVP"}, - Formats: []string{"101", "9", "0", "8", "102"}, - }, - Attributes: []sdp.Attribute{ - {Key: "rtpmap", Value: "101 AMR-WB/16000"}, - {Key: "fmtp", Value: "101 octet-align=0"}, - {Key: "rtpmap", Value: "9 G722/8000"}, - {Key: "rtpmap", Value: "0 PCMU/8000"}, - {Key: "rtpmap", Value: "8 PCMA/8000"}, - {Key: "rtpmap", Value: "102 telephone-event/8000"}, - {Key: "fmtp", Value: "102 0-16"}, - {Key: "ptime", Value: "20"}, - {Key: "sendrecv"}, - }, - }, offer) + t.Run("default offer", func(t *testing.T) { + _, offer, err := OfferMediaWith(g, port, EncryptionNone) + require.NoError(t, err) + require.Equal(t, &sdp.MediaDescription{ + MediaName: sdp.MediaName{ + Media: "audio", + Port: sdp.RangedPort{Value: port}, + Protos: []string{"RTP", "AVP"}, + Formats: []string{"101", "9", "0", "8", "102", "103"}, + }, + Attributes: []sdp.Attribute{ + {Key: "rtpmap", Value: "101 AMR-WB/16000"}, + {Key: "fmtp", Value: "101 octet-align=0"}, + {Key: "rtpmap", Value: "9 G722/8000"}, + {Key: "rtpmap", Value: "0 PCMU/8000"}, + {Key: "rtpmap", Value: "8 PCMA/8000"}, + {Key: "rtpmap", Value: "102 telephone-event/8000"}, + {Key: "fmtp", Value: "102 0-16"}, + {Key: "rtpmap", Value: "103 telephone-event/16000"}, + {Key: "fmtp", Value: "103 0-16"}, + {Key: "ptime", Value: "20"}, + {Key: "sendrecv"}, + }, + }, offer) + }) } func TestSDPMediaAnswerOneDisabled(t *testing.T) { @@ -429,9 +478,9 @@ func TestSDPMediaAnswerOneDisabled(t *testing.T) { }, } exp := &AudioConfig{ - Codec: getCodec(g, g711.ULawSDPNameAndRate), - Type: 0, - DTMFType: 101, + Codec: getCodec(g, g711.ULawSDPNameAndRate), + Type: 0, + DTMF: &DTMFInfo{Type: 101, Rate: 8000}, } noG722 := g.NewSet()