Skip to content

Commit 947bb9f

Browse files
authored
Updating DTMF detection to work based off of timestamp rather than first marker (#742)
1 parent 4c199b0 commit 947bb9f

2 files changed

Lines changed: 94 additions & 7 deletions

File tree

pkg/sip/media_port.go

Lines changed: 20 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -415,6 +415,7 @@ func NewMediaPortWith(tid traceid.ID, log logger.Logger, mon *stats.CallMonitor,
415415
audioIn: msdk.NewSwitchWriter(inSampleRate),
416416
stats: opts.Stats,
417417
}
418+
p.lastDTMFTimestamp.Store(math.MaxUint32)
418419
if p.opts.IgnorePreanswerData {
419420
p.port.startDiscarding()
420421
}
@@ -457,11 +458,12 @@ type MediaPort struct {
457458
dtmfOutRTP *rtp.Stream
458459
dtmfOutAudio msdk.PCM16Writer
459460

460-
audioOutRTP *rtp.Stream
461-
audioOut *msdk.SwitchWriter // LK PCM -> SIP RTP
462-
audioIn *msdk.SwitchWriter // SIP RTP -> LK PCM
463-
audioInHandler rtp.Handler // for debug only
464-
dtmfIn atomic.Pointer[func(ev dtmf.Event)]
461+
audioOutRTP *rtp.Stream
462+
audioOut *msdk.SwitchWriter // LK PCM -> SIP RTP
463+
audioIn *msdk.SwitchWriter // SIP RTP -> LK PCM
464+
audioInHandler rtp.Handler // for debug only
465+
dtmfIn atomic.Pointer[func(ev dtmf.Event)]
466+
lastDTMFTimestamp atomic.Uint32 // rtp timestamp of last DTMF packet seen
465467
}
466468

467469
func (p *MediaPort) DisableOut() {
@@ -1015,9 +1017,20 @@ func (p *MediaPort) dtmfHandler(h *rtp.Header, payload []byte) error {
10151017
return nil
10161018
}
10171019
fnc := *ptr
1018-
if ev, ok := dtmf.DecodeRTP(h, payload); ok && fnc != nil {
1019-
fnc(ev)
1020+
if fnc == nil {
1021+
return nil
1022+
}
1023+
// RFC 4733 requires all packets of a given digit to share identical timestamps.
1024+
// The marker bit could be used instead, but it is prone to occasional loss.
1025+
if h.Timestamp == p.lastDTMFTimestamp.Load() {
1026+
return nil
1027+
}
1028+
ev, err := dtmf.Decode(payload)
1029+
if err != nil {
1030+
return nil
10201031
}
1032+
p.lastDTMFTimestamp.Store(h.Timestamp)
1033+
fnc(ev)
10211034
return nil
10221035
}
10231036

pkg/sip/media_port_test.go

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
package sip
1616

1717
import (
18+
"context"
1819
"fmt"
1920
"io"
2021
"math"
@@ -32,6 +33,7 @@ import (
3233
"github.com/stretchr/testify/require"
3334

3435
msdk "github.com/livekit/media-sdk"
36+
"github.com/livekit/media-sdk/dtmf"
3537
"github.com/livekit/media-sdk/rtp"
3638
"github.com/livekit/media-sdk/sdp"
3739
"github.com/livekit/mediatransportutil/pkg/rtcconfig"
@@ -802,3 +804,75 @@ func TestSymmetricRTP(t *testing.T) {
802804
require.Equal(t, newAddr.String(), curDstPtr.String())
803805
})
804806
}
807+
808+
func generateDTMFPackets(t *testing.T, digits string) [][]*rtp.Packet {
809+
t.Helper()
810+
var buf rtp.Buffer
811+
packets := make([][]*rtp.Packet, len(digits))
812+
last := len(buf)
813+
w := rtp.NewSeqWriter(&buf).NewStream(101, dtmf.SampleRate)
814+
timestamp := uint32(1000)
815+
for i := range digits {
816+
err := dtmf.Write(context.Background(), nil, w, timestamp, digits[i:i+1])
817+
require.NoError(t, err)
818+
require.NotEmpty(t, buf)
819+
timestamp += uint32(dtmf.SampleRate / 2)
820+
packets[i] = slices.Clone(buf[last:])
821+
last = len(buf)
822+
}
823+
return packets
824+
}
825+
826+
func dropPackets(t *testing.T, dropType string, packets []*rtp.Packet) []*rtp.Packet {
827+
t.Helper()
828+
switch dropType {
829+
case "none":
830+
return packets
831+
case "first":
832+
require.Greater(t, len(packets), 3)
833+
return packets[3:]
834+
case "last":
835+
require.Greater(t, len(packets), 3)
836+
return packets[:len(packets)-3]
837+
case "middle":
838+
require.Greater(t, len(packets), 6)
839+
ret := slices.Clone(packets[:3])
840+
ret = append(ret, packets[len(packets)-3:]...)
841+
return ret
842+
default:
843+
t.Fatal("unknown drop type: " + dropType)
844+
return nil
845+
}
846+
}
847+
848+
func TestMediaPortDTMF(t *testing.T) {
849+
digitCases := []string{"1", "12", "123"}
850+
lossCases := []string{"none", "first", "last", "middle"}
851+
852+
for _, digits := range digitCases {
853+
packets := generateDTMFPackets(t, digits)
854+
for _, lossPackets := range lossCases {
855+
t.Run(fmt.Sprintf("digits=%s/loss=%s", digits, lossPackets), func(t *testing.T) {
856+
p := &MediaPort{}
857+
p.lastDTMFTimestamp.Store(math.MaxUint32)
858+
got := ""
859+
p.HandleDTMF(func(ev dtmf.Event) {
860+
t.Logf("received DTMF event: %+v", ev)
861+
got = fmt.Sprintf("%s%s", got, strconv.Itoa(int(ev.Code)))
862+
})
863+
for _, digitPackets := range packets {
864+
sendPackets := dropPackets(t, lossPackets, digitPackets)
865+
t.Logf("sending %d/%d packets", len(sendPackets), len(digitPackets))
866+
for _, pkt := range sendPackets {
867+
h := pkt.Header
868+
t.Logf("sending packet: seq=%d, ts=%d, marker=%t", h.SequenceNumber, h.Timestamp, h.Marker)
869+
require.NoError(t, p.dtmfHandler(&h, pkt.Payload))
870+
}
871+
}
872+
t.Logf("sent: %s", digits)
873+
t.Logf("got: %s", got)
874+
require.Equal(t, digits, got)
875+
})
876+
}
877+
}
878+
}

0 commit comments

Comments
 (0)