diff --git a/README.md b/README.md index d931c6b..5b34855 100644 --- a/README.md +++ b/README.md @@ -19,10 +19,10 @@ Features: - [x] Hangup control on caller - [x] Timeouts handling - [x] Digest auth +- [x] DTMF encoder, decoder via RFC4733 - [ ] Transfers on answer, dial - [ ] SDP codec fields manipulating - [ ] SDP negotiation fail -- [ ] DTMF passing Checkout `echome` example to see more. diff --git a/media.go b/media.go index 22da76a..1bdfa2b 100644 --- a/media.go +++ b/media.go @@ -1,6 +1,7 @@ package sipgox import ( + "encoding/binary" "fmt" "io" "net" @@ -11,6 +12,8 @@ import ( "github.com/emiago/sipgox/sdp" "github.com/pion/rtcp" "github.com/pion/rtp" + "github.com/rs/zerolog" + "github.com/rs/zerolog/log" ) var ( @@ -18,6 +21,9 @@ var ( RTPPortStart = 0 RTPPortEnd = 0 rtpPortOffset = atomic.Int32{} + + RTPDebug = false + RTCPDebug = false ) type MediaSession struct { @@ -34,6 +40,8 @@ type MediaSession struct { // Depending of negotiation this can change // Not thread safe Formats sdp.Formats + + log zerolog.Logger } func NewMediaSession(laddr *net.UDPAddr) (s *MediaSession, e error) { @@ -42,6 +50,7 @@ func NewMediaSession(laddr *net.UDPAddr) (s *MediaSession, e error) { sdp.FORMAT_TYPE_ULAW, sdp.FORMAT_TYPE_ALAW, }, Laddr: laddr, + log: log.With().Str("caller", "media").Logger(), } // Try to listen on this ports @@ -248,7 +257,14 @@ func (m *MediaSession) ReadRTP() (rtp.Packet, error) { return p, err } - return p, p.Unmarshal(buf[:n]) + if err := p.Unmarshal(buf[:n]); err != nil { + return p, err + } + + if RTPDebug { + m.log.Debug().Msgf("Recv RTP\n%s", p.String()) + } + return p, err } func (m *MediaSession) ReadRTPWithDeadline(t time.Time) (rtp.Packet, error) { @@ -291,6 +307,10 @@ func (m *MediaSession) ReadRTCPRaw(buf []byte) (int, error) { } func (m *MediaSession) WriteRTP(p *rtp.Packet) error { + if RTPDebug { + m.log.Debug().Msgf("RTP write:\n%s", p.String()) + } + data, err := p.Marshal() if err != nil { return err @@ -314,6 +334,12 @@ func (m *MediaSession) WriteRTP(p *rtp.Packet) error { } func (m *MediaSession) WriteRTCP(p rtcp.Packet) error { + if RTCPDebug { + if sr, ok := p.(fmt.Stringer); ok { + m.log.Debug().Msgf("RTCP write: \n%s", sr.String()) + } + } + data, err := p.Marshal() if err != nil { return err @@ -369,3 +395,95 @@ func selectFormats(sendCodecs []string, recvCodecs []string) []int { } return formats } + +// DTMF event mapping (RFC 4733) +var dtmfEventMapping = map[rune]byte{ + '0': 0, + '1': 1, + '2': 2, + '3': 3, + '4': 4, + '5': 5, + '6': 6, + '7': 7, + '8': 8, + '9': 9, + '*': 10, + '#': 11, + 'A': 12, + 'B': 13, + 'C': 14, + 'D': 15, +} + +func RTPDTMFEncode(char rune) []DTMFEvent { + event := dtmfEventMapping[char] + + events := make([]DTMFEvent, 7) + + for i := 0; i < 4; i++ { + d := DTMFEvent{ + Event: event, + EndOfEvent: false, + Volume: 10, + Duration: 160 * (uint16(i) + 1), + } + events[i] = d + } + + // End events. Took this from linphone example, but not clear why sending this many + for i := 4; i < 7; i++ { + d := DTMFEvent{ + Event: event, + EndOfEvent: true, + Volume: 10, + Duration: 160 * 5, // Must not be increased for end event + } + events[i] = d + } + + return events +} + +// DTMFEvent represents a DTMF event +type DTMFEvent struct { + Event uint8 + EndOfEvent bool + Volume uint8 + Duration uint16 +} + +func (ev *DTMFEvent) String() string { + out := "RTP DTMF Event:\n" + out += fmt.Sprintf("\tEvent: %d\n", ev.Event) + out += fmt.Sprintf("\tEndOfEvent: %v\n", ev.EndOfEvent) + out += fmt.Sprintf("\tVolume: %d\n", ev.Volume) + out += fmt.Sprintf("\tDuration: %d\n", ev.Duration) + return out +} + +// DecodeRTPPayload decodes an RTP payload into a DTMF event +func DTMFDecode(payload []byte, d *DTMFEvent) error { + if len(payload) < 4 { + return fmt.Errorf("payload too short") + } + + d.Event = payload[0] + d.EndOfEvent = payload[1]&0x80 != 0 + d.Volume = payload[1] & 0x7F + d.Duration = binary.BigEndian.Uint16(payload[2:4]) + // d.Duration = uint16(payload[2])<<8 | uint16(payload[3]) + return nil +} + +func DTMFEncode(d DTMFEvent) []byte { + header := make([]byte, 4) + header[0] = d.Event + + if d.EndOfEvent { + header[1] = 0x80 + } + header[1] |= d.Volume & 0x3F + binary.BigEndian.PutUint16(header[2:4], d.Duration) + return header +} diff --git a/media_test.go b/media_test.go index 67aefc3..d059ba9 100644 --- a/media_test.go +++ b/media_test.go @@ -25,3 +25,37 @@ func TestMediaPortRange(t *testing.T) { } } + +func TestDTMFEncodeDecode(t *testing.T) { + // Example payload for DTMF digit '1' with volume 10 and duration 1000 + // Event: 0x01 (DTMF digit '1') + // E bit: 0x80 (End of Event) + // Volume: 0x0A (Volume 10) + // Duration: 0x03E8 (Duration 1000) + payload := []byte{0x01, 0x8A, 0x03, 0xE8} + + event := DTMFEvent{} + err := DTMFDecode(payload, &event) + if err != nil { + t.Fatalf("Error decoding payload: %v", err) + } + + if event.Event != 0x01 { + t.Errorf("Unexpected Event. got: %v, want: %v", event.Event, 0x01) + } + + if event.EndOfEvent != true { + t.Errorf("Unexpected EndOfEvent. got: %v, want: %v", event.EndOfEvent, true) + } + + if event.Volume != 0x0A { + t.Errorf("Unexpected Volume. got: %v, want: %v", event.Volume, 0x0A) + } + + if event.Duration != 0x03E8 { + t.Errorf("Unexpected Duration. got: %v, want: %v", event.Duration, 0x03E8) + } + + encoded := DTMFEncode(event) + require.Equal(t, payload, encoded) +} diff --git a/phone.go b/phone.go index 030172d..dbb802e 100644 --- a/phone.go +++ b/phone.go @@ -200,6 +200,9 @@ func (p *Phone) createServerListener(s *sipgo.Server, a ListenAddr) (*Listener, return nil, fmt.Errorf("listen udp error. err=%w", err) } + // Port can be dynamic + a.Addr = udpConn.LocalAddr().String() + return &Listener{ a, udpConn, @@ -217,6 +220,7 @@ func (p *Phone) createServerListener(s *sipgo.Server, a ListenAddr) (*Listener, return nil, fmt.Errorf("listen tcp error. err=%w", err) } + a.Addr = conn.Addr().String() // and uses listener to buffer if network == "ws" { return &Listener{ diff --git a/register_transaction.go b/register_transaction.go index 3f88aec..f5d3082 100644 --- a/register_transaction.go +++ b/register_transaction.go @@ -29,8 +29,10 @@ func NewRegisterTransaction(log zerolog.Logger, client *sipgo.Client, recipient // log := p.getLoggerCtx(ctx, "Register") req := sip.NewRequest(sip.REGISTER, recipient) req.AppendHeader(&contact) - expires := sip.ExpiresHeader(expiry) - req.AppendHeader(&expires) + if expiry > 0 { + expires := sip.ExpiresHeader(expiry) + req.AppendHeader(&expires) + } if allowHDRS != nil { req.AppendHeader(sip.NewHeader("Allow", strings.Join(allowHDRS, ", "))) } diff --git a/sdp_utils.go b/sdp_utils.go index bebceed..8e9bbaa 100644 --- a/sdp_utils.go +++ b/sdp_utils.go @@ -47,13 +47,14 @@ func SDPGenerateForAudio(originIP net.IP, connectionIP net.IP, rtpPort int, mode // "b=AS:84", fmt.Sprintf("c=IN IP4 %s", connectionIP), "t=0 0", - fmt.Sprintf("m=audio %d RTP/AVP %s", rtpPort, strings.Join(fmts, " ")), + fmt.Sprintf("m=audio %d RTP/AVP %s", rtpPort, strings.Join(fmts, " ")+" 101"), "a=" + string(mode), // "a=ssrc:111222 cname:user@example.com", // "a=rtpmap:0 PCMU/8000", // "a=rtpmap:8 PCMA/8000", - // "a=rtpmap:101 telephone-event/8000", - // "a=fmtp:101 0-16", + // THIS is FOR DTM + "a=rtpmap:101 telephone-event/8000", + "a=fmtp:101 0-16", // "", // "a=rtpmap:120 telephone-event/16000", // "a=fmtp:120 0-16",