diff --git a/pkg/media/sdp/helpers.go b/pkg/media/sdp/helpers.go new file mode 100644 index 00000000..00fd06ff --- /dev/null +++ b/pkg/media/sdp/helpers.go @@ -0,0 +1,45 @@ +// Copyright 2024 LiveKit, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package sdp + +import ( + "net/netip" + + "github.com/pion/sdp/v3" +) + +func GetAudio(s *sdp.SessionDescription) *sdp.MediaDescription { + for _, m := range s.MediaDescriptions { + if m.MediaName.Media == "audio" { + return m + } + } + return nil +} + +func GetAudioDest(s *sdp.SessionDescription, audio *sdp.MediaDescription) netip.AddrPort { + if audio == nil || s == nil { + return netip.AddrPort{} + } + ci := s.ConnectionInformation + if ci == nil || ci.NetworkType != "IN" { + return netip.AddrPort{} + } + ip, err := netip.ParseAddr(ci.Address.Address) + if err != nil { + return netip.AddrPort{} + } + return netip.AddrPortFrom(ip, uint16(audio.MediaName.Port.Value)) +} diff --git a/pkg/sip/signaling.go b/pkg/media/sdp/offer.go similarity index 54% rename from pkg/sip/signaling.go rename to pkg/media/sdp/offer.go index ef8109d8..6fceb65a 100644 --- a/pkg/sip/signaling.go +++ b/pkg/media/sdp/offer.go @@ -1,4 +1,4 @@ -// Copyright 2023 LiveKit, Inc. +// Copyright 2024 LiveKit, Inc. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -12,13 +12,12 @@ // See the License for the specific language governing permissions and // limitations under the License. -package sip +package sdp import ( "errors" "fmt" - "math/rand" - "net" + "math/rand/v2" "net/netip" "slices" "strconv" @@ -30,10 +29,14 @@ import ( "github.com/livekit/sip/pkg/media" "github.com/livekit/sip/pkg/media/dtmf" "github.com/livekit/sip/pkg/media/rtp" - lksdp "github.com/livekit/sip/pkg/media/sdp" ) -func getCodecs() []sdpCodecInfo { +type CodecInfo struct { + Type byte + Codec media.Codec +} + +func OfferCodecs() []CodecInfo { const dynamicType = 101 codecs := media.EnabledCodecs() slices.SortFunc(codecs, func(a, b media.Codec) int { @@ -47,11 +50,11 @@ func getCodecs() []sdpCodecInfo { } return bi.Priority - ai.Priority }) - infos := make([]sdpCodecInfo, 0, len(codecs)) + infos := make([]CodecInfo, 0, len(codecs)) nextType := byte(dynamicType) for _, c := range codecs { cinfo := c.Info() - info := sdpCodecInfo{ + info := CodecInfo{ Codec: c, } if cinfo.RTPIsStatic { @@ -66,22 +69,22 @@ func getCodecs() []sdpCodecInfo { return infos } -type sdpCodecInfo struct { - Type byte - Codec media.Codec +type MediaDesc struct { + Codecs []CodecInfo + DTMFType byte // set to 0 if there's no DTMF } -func sdpMediaOffer(rtpListenerPort int) []*sdp.MediaDescription { +func OfferMedia(rtpListenerPort int) (MediaDesc, *sdp.MediaDescription) { // Static compiler check for frame duration hardcoded below. var _ = [1]struct{}{}[20*time.Millisecond-rtp.DefFrameDur] - codecs := getCodecs() + codecs := OfferCodecs() attrs := make([]sdp.Attribute, 0, len(codecs)+4) formats := make([]string, 0, len(codecs)) - dtmfType := -1 + dtmfType := byte(0) for _, codec := range codecs { if codec.Codec.Info().SDPName == dtmf.SDPName { - dtmfType = int(codec.Type) + dtmfType = codec.Type } styp := strconv.Itoa(int(codec.Type)) formats = append(formats, styp) @@ -100,8 +103,10 @@ func sdpMediaOffer(rtpListenerPort int) []*sdp.MediaDescription { {Key: "sendrecv"}, }...) - return []*sdp.MediaDescription{ - { + return MediaDesc{ + Codecs: codecs, + DTMFType: dtmfType, + }, &sdp.MediaDescription{ MediaName: sdp.MediaName{ Media: "audio", Port: sdp.RangedPort{Value: rtpListenerPort}, @@ -109,49 +114,56 @@ func sdpMediaOffer(rtpListenerPort int) []*sdp.MediaDescription { Formats: formats, }, Attributes: attrs, - }, - } + } } -func sdpAnswerMediaDesc(rtpListenerPort int, res *MediaConf) []*sdp.MediaDescription { +func AnswerMedia(rtpListenerPort int, audio *AudioConfig) *sdp.MediaDescription { // Static compiler check for frame duration hardcoded below. var _ = [1]struct{}{}[20*time.Millisecond-rtp.DefFrameDur] attrs := make([]sdp.Attribute, 0, 6) attrs = append(attrs, sdp.Attribute{ - Key: "rtpmap", Value: fmt.Sprintf("%d %s", res.AudioType, res.Audio.Info().SDPName), + Key: "rtpmap", Value: fmt.Sprintf("%d %s", audio.Type, audio.Codec.Info().SDPName), }) formats := make([]string, 0, 2) - formats = append(formats, strconv.Itoa(int(res.AudioType))) - if res.DTMFType != 0 { - formats = append(formats, strconv.Itoa(int(res.DTMFType))) + formats = append(formats, strconv.Itoa(int(audio.Type))) + if audio.DTMFType != 0 { + formats = append(formats, strconv.Itoa(int(audio.DTMFType))) attrs = append(attrs, []sdp.Attribute{ - {Key: "rtpmap", Value: fmt.Sprintf("%d %s", res.DTMFType, dtmf.SDPName)}, - {Key: "fmtp", Value: fmt.Sprintf("%d 0-16", res.DTMFType)}, + {Key: "rtpmap", Value: fmt.Sprintf("%d %s", audio.DTMFType, dtmf.SDPName)}, + {Key: "fmtp", Value: fmt.Sprintf("%d 0-16", audio.DTMFType)}, }...) } attrs = append(attrs, []sdp.Attribute{ {Key: "ptime", Value: "20"}, {Key: "sendrecv"}, }...) - return []*sdp.MediaDescription{ - { - MediaName: sdp.MediaName{ - Media: "audio", - Port: sdp.RangedPort{Value: rtpListenerPort}, - Protos: []string{"RTP", "AVP"}, - Formats: formats, - }, - Attributes: attrs, + return &sdp.MediaDescription{ + MediaName: sdp.MediaName{ + Media: "audio", + Port: sdp.RangedPort{Value: rtpListenerPort}, + Protos: []string{"RTP", "AVP"}, + Formats: formats, }, + Attributes: attrs, } } -func sdpGenerateOffer(publicIp netip.Addr, rtpListenerPort int) ([]byte, error) { +type Description struct { + SDP sdp.SessionDescription + Addr netip.AddrPort + MediaDesc +} + +type Offer Description + +type Answer Description + +func NewOffer(publicIp netip.Addr, rtpListenerPort int) *Offer { sessId := rand.Uint64() // TODO: do we need to track these? - mediaDesc := sdpMediaOffer(rtpListenerPort) - answer := sdp.SessionDescription{ + m, mediaDesc := OfferMedia(rtpListenerPort) + offer := sdp.SessionDescription{ Version: 0, Origin: sdp.Origin{ Username: "-", @@ -175,20 +187,28 @@ func sdpGenerateOffer(publicIp netip.Addr, rtpListenerPort int) ([]byte, error) }, }, }, - MediaDescriptions: mediaDesc, + MediaDescriptions: []*sdp.MediaDescription{mediaDesc}, + } + return &Offer{ + SDP: offer, + Addr: netip.AddrPortFrom(publicIp, uint16(rtpListenerPort)), + MediaDesc: m, } - - data, err := answer.Marshal() - return data, err } -func sdpGenerateAnswer(offer *sdp.SessionDescription, publicIp netip.Addr, rtpListenerPort int, res *MediaConf) ([]byte, error) { +func (d *Offer) Answer(publicIp netip.Addr, rtpListenerPort int) (*Answer, *MediaConfig, error) { + audio, err := SelectAudio(d.MediaDesc) + if err != nil { + return nil, nil, err + } + + mediaDesc := AnswerMedia(rtpListenerPort, audio) answer := sdp.SessionDescription{ Version: 0, Origin: sdp.Origin{ Username: "-", - SessionID: offer.Origin.SessionID, - SessionVersion: offer.Origin.SessionID + 2, + SessionID: d.SDP.Origin.SessionID, + SessionVersion: d.SDP.Origin.SessionID + 2, NetworkType: "IN", AddressType: "IP4", UnicastAddress: publicIp.String(), @@ -207,75 +227,73 @@ func sdpGenerateAnswer(offer *sdp.SessionDescription, publicIp netip.Addr, rtpLi }, }, }, - MediaDescriptions: sdpAnswerMediaDesc(rtpListenerPort, res), + MediaDescriptions: []*sdp.MediaDescription{mediaDesc}, } - - return answer.Marshal() + src := netip.AddrPortFrom(publicIp, uint16(rtpListenerPort)) + return &Answer{ + SDP: answer, + Addr: src, + MediaDesc: MediaDesc{ + Codecs: []CodecInfo{ + {Type: audio.Type, Codec: audio.Codec}, + }, + DTMFType: audio.DTMFType, + }, + }, &MediaConfig{ + Local: src, + Remote: d.Addr, + Audio: *audio, + }, nil } -func sdpGetAudio(offer *sdp.SessionDescription) *sdp.MediaDescription { - for _, m := range offer.MediaDescriptions { - if m.MediaName.Media == "audio" { - return m - } +func (d *Answer) Apply(offer *Offer) (*MediaConfig, error) { + audio, err := SelectAudio(d.MediaDesc) + if err != nil { + return nil, err } - return nil + return &MediaConfig{ + Local: offer.Addr, + Remote: d.Addr, + Audio: *audio, + }, nil } -func sdpGetAudioDest(offer *sdp.SessionDescription, audio *sdp.MediaDescription) *net.UDPAddr { - if audio == nil || offer == nil { - return nil +func Parse(data []byte) (*Description, error) { + offer := new(Description) + if err := offer.SDP.Unmarshal(data); err != nil { + return nil, err } - ci := offer.ConnectionInformation - if ci == nil || ci.NetworkType != "IN" { - return nil + audio := GetAudio(&offer.SDP) + if audio == nil { + return nil, errors.New("no audio in sdp") } - ip, err := netip.ParseAddr(ci.Address.Address) + offer.Addr = GetAudioDest(&offer.SDP, audio) + m, err := ParseMedia(audio) if err != nil { - return nil - } - return &net.UDPAddr{ - IP: ip.AsSlice(), - Port: audio.MediaName.Port.Value, + return nil, err } + offer.MediaDesc = *m + return offer, nil } -type MediaConf struct { - Dest *net.UDPAddr - Audio rtp.AudioCodec - AudioType byte - DTMFType byte - Processor media.PCM16Processor +func ParseOffer(data []byte) (*Offer, error) { + d, err := Parse(data) + if err != nil { + return nil, err + } + return (*Offer)(d), nil } -func sdpGetAudioCodec(offer *sdp.SessionDescription) (*MediaConf, error) { - audio := sdpGetAudio(offer) - if audio == nil { - return nil, errors.New("no audio in sdp") - } - dest := sdpGetAudioDest(offer, audio) - c, err := sdpGetCodec(audio) +func ParseAnswer(data []byte) (*Answer, error) { + d, err := Parse(data) if err != nil { return nil, err } - c.Dest = dest - return c, nil + return (*Answer)(d), nil } -func sdpGetCodec(d *sdp.MediaDescription) (*MediaConf, error) { - var ( - priority int - audioCodec rtp.AudioCodec - audioType byte - dtmfType byte - ) - considerCodec := func(typ byte, codec rtp.AudioCodec) { - if audioCodec == nil || codec.Info().Priority > priority { - audioType = typ - audioCodec = codec - priority = codec.Info().Priority - } - } +func ParseMedia(d *sdp.MediaDescription) (*MediaDesc, error) { + var out MediaDesc for _, m := range d.Attributes { switch m.Key { case "rtpmap": @@ -289,14 +307,14 @@ func sdpGetCodec(d *sdp.MediaDescription) (*MediaConf, error) { } name := sub[1] if name == dtmf.SDPName { - dtmfType = byte(typ) - continue - } - codec, ok := lksdp.CodecByName(name).(rtp.AudioCodec) - if !ok { + out.DTMFType = byte(typ) continue } - considerCodec(byte(typ), codec) + codec, _ := CodecByName(name).(rtp.AudioCodec) + out.Codecs = append(out.Codecs, CodecInfo{ + Type: byte(typ), + Codec: codec, + }) } } for _, f := range d.MediaName.Formats { @@ -304,18 +322,50 @@ func sdpGetCodec(d *sdp.MediaDescription) (*MediaConf, error) { if err != nil { continue } - codec, ok := rtp.CodecByPayloadType(byte(typ)).(rtp.AudioCodec) + codec, _ := rtp.CodecByPayloadType(byte(typ)).(rtp.AudioCodec) + out.Codecs = append(out.Codecs, CodecInfo{ + Type: byte(typ), + Codec: codec, + }) + } + return &out, nil +} + +type MediaConfig struct { + Local netip.AddrPort + Remote netip.AddrPort + Audio AudioConfig +} + +type AudioConfig struct { + Codec rtp.AudioCodec + Type byte + DTMFType byte +} + +func SelectAudio(desc MediaDesc) (*AudioConfig, error) { + var ( + priority int + audioCodec rtp.AudioCodec + audioType byte + ) + for _, c := range desc.Codecs { + codec, ok := c.Codec.(rtp.AudioCodec) if !ok { continue } - considerCodec(byte(typ), codec) + if audioCodec == nil || codec.Info().Priority > priority { + audioType = c.Type + audioCodec = codec + priority = codec.Info().Priority + } } if audioCodec == nil { return nil, fmt.Errorf("common audio codec not found") } - return &MediaConf{ - Audio: audioCodec, - AudioType: audioType, - DTMFType: dtmfType, + return &AudioConfig{ + Codec: audioCodec, + Type: audioType, + DTMFType: desc.DTMFType, }, nil } diff --git a/pkg/sip/signaling_test.go b/pkg/media/sdp/offer_test.go similarity index 60% rename from pkg/sip/signaling_test.go rename to pkg/media/sdp/offer_test.go index 295280bd..390537c3 100644 --- a/pkg/sip/signaling_test.go +++ b/pkg/media/sdp/offer_test.go @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -package sip +package sdp_test import ( "testing" @@ -24,58 +24,54 @@ import ( "github.com/livekit/sip/pkg/media/g711" "github.com/livekit/sip/pkg/media/g722" "github.com/livekit/sip/pkg/media/rtp" - lksdp "github.com/livekit/sip/pkg/media/sdp" + . "github.com/livekit/sip/pkg/media/sdp" ) func TestSDPMediaOffer(t *testing.T) { const port = 12345 - offer := sdpMediaOffer(port) - require.Equal(t, []*sdp.MediaDescription{ - { - MediaName: sdp.MediaName{ - Media: "audio", - Port: sdp.RangedPort{Value: port}, - Protos: []string{"RTP", "AVP"}, - Formats: []string{"9", "0", "8", "101"}, - }, - Attributes: []sdp.Attribute{ - {Key: "rtpmap", Value: "9 G722/8000"}, - {Key: "rtpmap", Value: "0 PCMU/8000"}, - {Key: "rtpmap", Value: "8 PCMA/8000"}, - {Key: "rtpmap", Value: "101 telephone-event/8000"}, - {Key: "fmtp", Value: "101 0-16"}, - {Key: "ptime", Value: "20"}, - {Key: "sendrecv"}, - }, + _, offer := OfferMedia(port) + require.Equal(t, &sdp.MediaDescription{ + MediaName: sdp.MediaName{ + Media: "audio", + Port: sdp.RangedPort{Value: port}, + Protos: []string{"RTP", "AVP"}, + Formats: []string{"9", "0", "8", "101"}, + }, + Attributes: []sdp.Attribute{ + {Key: "rtpmap", Value: "9 G722/8000"}, + {Key: "rtpmap", Value: "0 PCMU/8000"}, + {Key: "rtpmap", Value: "8 PCMA/8000"}, + {Key: "rtpmap", Value: "101 telephone-event/8000"}, + {Key: "fmtp", Value: "101 0-16"}, + {Key: "ptime", Value: "20"}, + {Key: "sendrecv"}, }, }, offer) media.CodecSetEnabled(g722.SDPName, false) defer media.CodecSetEnabled(g722.SDPName, true) - offer = sdpMediaOffer(port) - require.Equal(t, []*sdp.MediaDescription{ - { - MediaName: sdp.MediaName{ - Media: "audio", - Port: sdp.RangedPort{Value: port}, - Protos: []string{"RTP", "AVP"}, - Formats: []string{"0", "8", "101"}, - }, - Attributes: []sdp.Attribute{ - {Key: "rtpmap", Value: "0 PCMU/8000"}, - {Key: "rtpmap", Value: "8 PCMA/8000"}, - {Key: "rtpmap", Value: "101 telephone-event/8000"}, - {Key: "fmtp", Value: "101 0-16"}, - {Key: "ptime", Value: "20"}, - {Key: "sendrecv"}, - }, + _, offer = OfferMedia(port) + require.Equal(t, &sdp.MediaDescription{ + MediaName: sdp.MediaName{ + Media: "audio", + Port: sdp.RangedPort{Value: port}, + Protos: []string{"RTP", "AVP"}, + Formats: []string{"0", "8", "101"}, + }, + Attributes: []sdp.Attribute{ + {Key: "rtpmap", Value: "0 PCMU/8000"}, + {Key: "rtpmap", Value: "8 PCMA/8000"}, + {Key: "rtpmap", Value: "101 telephone-event/8000"}, + {Key: "fmtp", Value: "101 0-16"}, + {Key: "ptime", Value: "20"}, + {Key: "sendrecv"}, }, }, offer) } func getCodec(name string) rtp.AudioCodec { - return lksdp.CodecByName(name).(rtp.AudioCodec) + return CodecByName(name).(rtp.AudioCodec) } func TestSDPMediaAnswer(t *testing.T) { @@ -83,7 +79,7 @@ func TestSDPMediaAnswer(t *testing.T) { cases := []struct { name string offer sdp.MediaDescription - exp *MediaConf + exp *AudioConfig }{ { name: "default", @@ -97,10 +93,10 @@ func TestSDPMediaAnswer(t *testing.T) { {Key: "rtpmap", Value: "101 telephone-event/8000"}, }, }, - exp: &MediaConf{ - Audio: getCodec(g722.SDPName), - AudioType: 9, - DTMFType: 101, + exp: &AudioConfig{ + Codec: getCodec(g722.SDPName), + Type: 9, + DTMFType: 101, }, }, { @@ -115,10 +111,10 @@ func TestSDPMediaAnswer(t *testing.T) { {Key: "rtpmap", Value: "101 telephone-event/8000"}, }, }, - exp: &MediaConf{ - Audio: getCodec(g722.SDPName), - AudioType: 9, - DTMFType: 101, + exp: &AudioConfig{ + Codec: getCodec(g722.SDPName), + Type: 9, + DTMFType: 101, }, }, { @@ -132,9 +128,9 @@ func TestSDPMediaAnswer(t *testing.T) { {Key: "rtpmap", Value: "9 G722/8000"}, }, }, - exp: &MediaConf{ - Audio: getCodec(g722.SDPName), - AudioType: 9, + exp: &AudioConfig{ + Codec: getCodec(g722.SDPName), + Type: 9, }, }, { @@ -149,10 +145,10 @@ func TestSDPMediaAnswer(t *testing.T) { {Key: "rtpmap", Value: "103 telephone-event/8000"}, }, }, - exp: &MediaConf{ - Audio: getCodec(g722.SDPName), - AudioType: 9, - DTMFType: 103, + exp: &AudioConfig{ + Codec: getCodec(g722.SDPName), + Type: 9, + DTMFType: 103, }, }, { @@ -166,10 +162,10 @@ func TestSDPMediaAnswer(t *testing.T) { {Key: "rtpmap", Value: "101 telephone-event/8000"}, }, }, - exp: &MediaConf{ - Audio: getCodec(g711.ULawSDPName), - AudioType: 0, - DTMFType: 101, + exp: &AudioConfig{ + Codec: getCodec(g711.ULawSDPName), + Type: 0, + DTMFType: 101, }, }, { @@ -183,10 +179,10 @@ func TestSDPMediaAnswer(t *testing.T) { {Key: "rtpmap", Value: "101 telephone-event/8000"}, }, }, - exp: &MediaConf{ - Audio: getCodec(g722.SDPName), - AudioType: 9, - DTMFType: 101, + exp: &AudioConfig{ + Codec: getCodec(g722.SDPName), + Type: 9, + DTMFType: 101, }, }, { @@ -212,44 +208,44 @@ func TestSDPMediaAnswer(t *testing.T) { {Key: "rtpmap", Value: "101 telephone-event/8000"}, }, }, - exp: &MediaConf{ - Audio: getCodec(g711.ULawSDPName), - AudioType: 0, - DTMFType: 101, + exp: &AudioConfig{ + Codec: getCodec(g711.ULawSDPName), + Type: 0, + DTMFType: 101, }, }, } for _, c := range cases { c := c t.Run(c.name, func(t *testing.T) { - got, err := sdpGetCodec(&c.offer) + m, err := ParseMedia(&c.offer) + require.NoError(t, err) + got, err := SelectAudio(*m) if c.exp == nil { require.Error(t, err) return } - require.NotNil(t, c.exp.Audio) + require.NotNil(t, c.exp.Codec) require.NoError(t, err) require.Equal(t, c.exp, got) }) } - offer := sdpMediaOffer(port) - require.Equal(t, []*sdp.MediaDescription{ - { - MediaName: sdp.MediaName{ - Media: "audio", - Port: sdp.RangedPort{Value: port}, - Protos: []string{"RTP", "AVP"}, - Formats: []string{"9", "0", "8", "101"}, - }, - Attributes: []sdp.Attribute{ - {Key: "rtpmap", Value: "9 G722/8000"}, - {Key: "rtpmap", Value: "0 PCMU/8000"}, - {Key: "rtpmap", Value: "8 PCMA/8000"}, - {Key: "rtpmap", Value: "101 telephone-event/8000"}, - {Key: "fmtp", Value: "101 0-16"}, - {Key: "ptime", Value: "20"}, - {Key: "sendrecv"}, - }, + _, offer := OfferMedia(port) + require.Equal(t, &sdp.MediaDescription{ + MediaName: sdp.MediaName{ + Media: "audio", + Port: sdp.RangedPort{Value: port}, + Protos: []string{"RTP", "AVP"}, + Formats: []string{"9", "0", "8", "101"}, + }, + Attributes: []sdp.Attribute{ + {Key: "rtpmap", Value: "9 G722/8000"}, + {Key: "rtpmap", Value: "0 PCMU/8000"}, + {Key: "rtpmap", Value: "8 PCMA/8000"}, + {Key: "rtpmap", Value: "101 telephone-event/8000"}, + {Key: "fmtp", Value: "101 0-16"}, + {Key: "ptime", Value: "20"}, + {Key: "sendrecv"}, }, }, offer) } diff --git a/pkg/sip/inbound.go b/pkg/sip/inbound.go index d4783b5b..d5ad8583 100644 --- a/pkg/sip/inbound.go +++ b/pkg/sip/inbound.go @@ -535,7 +535,11 @@ func (c *inboundCall) runMediaConn(offerData []byte, conf *config.Config, featur c.media.EnableTimeout(false) // enabled once we accept the call c.media.SetDTMFAudio(conf.AudioDTMF) - answerData, mconf, err := mp.SetOffer(offerData) + answer, mconf, err := mp.SetOffer(offerData) + if err != nil { + return nil, err + } + answerData, err = answer.SDP.Marshal() if err != nil { return nil, err } @@ -546,7 +550,7 @@ func (c *inboundCall) runMediaConn(offerData []byte, conf *config.Config, featur if err = c.media.SetConfig(mconf); err != nil { return nil, err } - if mconf.DTMFType != 0 { + if mconf.Audio.DTMFType != 0 { c.media.HandleDTMF(c.handleDTMF) } @@ -554,7 +558,7 @@ func (c *inboundCall) runMediaConn(offerData []byte, conf *config.Config, featur if w := c.lkRoom.SwapOutput(c.media.GetAudioWriter()); w != nil { _ = w.Close() } - if mconf.DTMFType != 0 { + if mconf.Audio.DTMFType != 0 { c.lkRoom.SetDTMFOutput(c.media) } return answerData, nil diff --git a/pkg/sip/media_port.go b/pkg/sip/media_port.go index 93a66b23..2e7f7c76 100644 --- a/pkg/sip/media_port.go +++ b/pkg/sip/media_port.go @@ -16,23 +16,28 @@ package sip import ( "context" + "net" "net/netip" "sync" "sync/atomic" "time" - "github.com/pion/sdp/v3" - "github.com/livekit/mediatransportutil/pkg/rtcconfig" "github.com/livekit/protocol/logger" "github.com/livekit/sip/pkg/media" "github.com/livekit/sip/pkg/media/dtmf" "github.com/livekit/sip/pkg/media/rtp" + "github.com/livekit/sip/pkg/media/sdp" "github.com/livekit/sip/pkg/mixer" "github.com/livekit/sip/pkg/stats" ) +type MediaConf struct { + sdp.MediaConfig + Processor media.PCM16Processor +} + type MediaConfig struct { IP netip.Addr Ports rtcconfig.PortRange @@ -148,55 +153,49 @@ func (p *MediaPort) GetAudioWriter() media.PCM16Writer { } // NewOffer generates an SDP offer for the media. -func (p *MediaPort) NewOffer() ([]byte, error) { - return sdpGenerateOffer(p.externalIP, p.Port()) +func (p *MediaPort) NewOffer() *sdp.Offer { + return sdp.NewOffer(p.externalIP, p.Port()) } -func (p *MediaPort) decodeSDP(data []byte) (*sdp.SessionDescription, *MediaConf, error) { - desc := &sdp.SessionDescription{} - if err := desc.Unmarshal(data); err != nil { - return nil, nil, err - } - c, err := sdpGetAudioCodec(desc) +// SetAnswer decodes and applies SDP answer for offer from NewOffer. SetConfig must be called with the decoded configuration. +func (p *MediaPort) SetAnswer(offer *sdp.Offer, answerData []byte) (*MediaConf, error) { + answer, err := sdp.ParseAnswer(answerData) if err != nil { - p.log.Infow("SIP SDP failed", "error", err) - return nil, nil, err + return nil, err } - return desc, c, nil -} - -// SetAnswer decodes and applies SDP answer for offer from NewOffer. SetConfig must be called with the decoded configuration. -func (p *MediaPort) SetAnswer(answer []byte) (*MediaConf, error) { - _, c, err := p.decodeSDP(answer) + mc, err := answer.Apply(offer) if err != nil { return nil, err } - return c, nil + return &MediaConf{MediaConfig: *mc}, nil } // SetOffer decodes the offer from another party and returns encoded answer. To accept the offer, call SetConfig. -func (p *MediaPort) SetOffer(offer []byte) ([]byte, *MediaConf, error) { - off, c, err := p.decodeSDP(offer) +func (p *MediaPort) SetOffer(offerData []byte) (*sdp.Answer, *MediaConf, error) { + offer, err := sdp.ParseOffer(offerData) if err != nil { return nil, nil, err } - answer, err := sdpGenerateAnswer(off, p.externalIP, p.Port(), c) + answer, mc, err := offer.Answer(p.externalIP, p.Port()) if err != nil { return nil, nil, err } - return answer, c, nil + return answer, &MediaConf{MediaConfig: *mc}, nil } func (p *MediaPort) SetConfig(c *MediaConf) error { p.log.Infow("using codecs", - "audio-codec", c.Audio.Info().SDPName, "audio-rtp", c.AudioType, - "dtmf-rtp", c.DTMFType, + "audio-codec", c.Audio.Codec.Info().SDPName, "audio-rtp", c.Audio.Type, + "dtmf-rtp", c.Audio.DTMFType, ) p.mu.Lock() defer p.mu.Unlock() - if c.Dest != nil { - p.conn.SetDestAddr(c.Dest) + if ip := c.Remote; ip.IsValid() { + p.conn.SetDestAddr(&net.UDPAddr{ + IP: ip.Addr().AsSlice(), + Port: int(ip.Port()), + }) } p.conf = c @@ -209,16 +208,16 @@ func (p *MediaPort) SetConfig(c *MediaConf) error { func (p *MediaPort) setupOutput() { // TODO: this says "audio", but actually includes DTMF too s := rtp.NewSeqWriter(newRTPStatsWriter(p.mon, "audio", p.conn)) - p.audioOutRTP = s.NewStream(p.conf.AudioType, p.conf.Audio.Info().RTPClockRate) + p.audioOutRTP = s.NewStream(p.conf.Audio.Type, p.conf.Audio.Codec.Info().RTPClockRate) // Encoding pipeline (LK -> SIP) - audioOut := p.conf.Audio.EncodeRTP(p.audioOutRTP) + audioOut := p.conf.Audio.Codec.EncodeRTP(p.audioOutRTP) if processor := p.conf.Processor; processor != nil { audioOut = processor(audioOut) } - if p.conf.DTMFType != 0 { - p.dtmfOutRTP = s.NewStream(p.conf.DTMFType, dtmf.SampleRate) + if p.conf.Audio.DTMFType != 0 { + p.dtmfOutRTP = s.NewStream(p.conf.Audio.DTMFType, dtmf.SampleRate) if p.dtmfAudioEnabled { // Add separate mixer for DTMF audio. // TODO: optimize, if we'll ever need this code path @@ -235,14 +234,14 @@ func (p *MediaPort) setupOutput() { func (p *MediaPort) setupInput() { // Decoding pipeline (SIP -> LK) - audioHandler := p.conf.Audio.DecodeRTP(p.audioIn, p.conf.AudioType) + audioHandler := p.conf.Audio.Codec.DecodeRTP(p.audioIn, p.conf.Audio.Type) p.audioInHandler = audioHandler - audioHandler = rtp.HandleJitter(p.conf.Audio.Info().RTPClockRate, audioHandler) + audioHandler = rtp.HandleJitter(p.conf.Audio.Codec.Info().RTPClockRate, audioHandler) mux := rtp.NewMux(nil) mux.SetDefault(newRTPStatsHandler(p.mon, "", nil)) - mux.Register(p.conf.AudioType, newRTPStatsHandler(p.mon, p.conf.Audio.Info().SDPName, audioHandler)) - if p.conf.DTMFType != 0 { - mux.Register(p.conf.DTMFType, newRTPStatsHandler(p.mon, dtmf.SDPName, rtp.HandlerFunc(func(pck *rtp.Packet) error { + mux.Register(p.conf.Audio.Type, newRTPStatsHandler(p.mon, p.conf.Audio.Codec.Info().SDPName, audioHandler)) + if p.conf.Audio.DTMFType != 0 { + mux.Register(p.conf.Audio.DTMFType, newRTPStatsHandler(p.mon, dtmf.SDPName, rtp.HandlerFunc(func(pck *rtp.Packet) error { ptr := p.dtmfIn.Load() if ptr == nil { return nil diff --git a/pkg/sip/media_port_test.go b/pkg/sip/media_port_test.go index 220fa5d1..94494bbb 100644 --- a/pkg/sip/media_port_test.go +++ b/pkg/sip/media_port_test.go @@ -174,17 +174,20 @@ func TestMediaPort(t *testing.T) { require.NoError(t, err) defer m2.Close() - offer, err := m1.NewOffer() + offer := m1.NewOffer() + offerData, err := offer.SDP.Marshal() require.NoError(t, err) - t.Logf("SDP offer:\n%s", string(offer)) + t.Logf("SDP offer:\n%s", string(offerData)) - answer, conf, err := m2.SetOffer(offer) + answer, conf, err := m2.SetOffer(offerData) + require.NoError(t, err) + answerData, err := answer.SDP.Marshal() require.NoError(t, err) - t.Logf("SDP answer:\n%s", string(answer)) + t.Logf("SDP answer:\n%s", string(answerData)) - mc, err := m1.SetAnswer(answer) + mc, err := m1.SetAnswer(offer, answerData) require.NoError(t, err) err = m1.SetConfig(mc) @@ -193,8 +196,8 @@ func TestMediaPort(t *testing.T) { err = m2.SetConfig(conf) require.NoError(t, err) - require.Equal(t, info.SDPName, m1.Config().Audio.Info().SDPName) - require.Equal(t, info.SDPName, m2.Config().Audio.Info().SDPName) + require.Equal(t, info.SDPName, m1.Config().Audio.Codec.Info().SDPName) + require.Equal(t, info.SDPName, m2.Config().Audio.Codec.Info().SDPName) var buf1 media.PCM16Sample m1.WriteAudioTo(media.NewPCM16BufferWriter(&buf1, rate)) diff --git a/pkg/sip/outbound.go b/pkg/sip/outbound.go index 8c873e57..5506d029 100644 --- a/pkg/sip/outbound.go +++ b/pkg/sip/outbound.go @@ -392,12 +392,13 @@ func (c *outboundCall) sipSignal(ctx context.Context) error { cancel() }() - sdpOffer, err := c.media.NewOffer() + sdpOffer := c.media.NewOffer() + sdpOfferData, err := sdpOffer.SDP.Marshal() if err != nil { return err } - c.mon.SDPSize(len(sdpOffer), true) - c.log.Debugw("SDP offer", "sdp", string(sdpOffer)) + c.mon.SDPSize(len(sdpOfferData), true) + c.log.Debugw("SDP offer", "sdp", string(sdpOfferData)) joinDur := c.mon.JoinDur() c.mon.InviteReq() @@ -405,7 +406,7 @@ func (c *outboundCall) sipSignal(ctx context.Context) error { toUri := CreateURIFromUserAndAddress(c.sipConf.to, c.sipConf.address, TransportFrom(c.sipConf.transport)) ringing := false - sdpResp, err := c.cc.Invite(ctx, toUri, c.sipConf.user, c.sipConf.pass, c.sipConf.headers, sdpOffer, func(code sip.StatusCode) { + sdpResp, err := c.cc.Invite(ctx, toUri, c.sipConf.user, c.sipConf.pass, c.sipConf.headers, sdpOfferData, func(code sip.StatusCode) { if !ringing && code >= sip.StatusRinging { ringing = true c.setStatus(CallRinging) @@ -428,7 +429,7 @@ func (c *outboundCall) sipSignal(ctx context.Context) error { c.log = LoggerWithHeaders(c.log, c.cc) - mc, err := c.media.SetAnswer(sdpResp) + mc, err := c.media.SetAnswer(sdpOffer, sdpResp) if err != nil { return err } diff --git a/pkg/sip/service_test.go b/pkg/sip/service_test.go index 4b5b558f..af8790fb 100644 --- a/pkg/sip/service_test.go +++ b/pkg/sip/service_test.go @@ -18,6 +18,7 @@ import ( "github.com/livekit/sip/pkg/config" "github.com/livekit/sip/pkg/media" + "github.com/livekit/sip/pkg/media/sdp" "github.com/livekit/sip/pkg/stats" ) @@ -107,13 +108,14 @@ func testInvite(t *testing.T, h Handler, hidden bool, from, to string, test func sipClient, err := sipgo.NewClient(sipUserAgent) require.NoError(t, err) - offer, err := sdpGenerateOffer(localIP, 0xB0B) + offer := sdp.NewOffer(localIP, 0xB0B) + offerData, err := offer.SDP.Marshal() require.NoError(t, err) inviteRecipent := sip.Uri{User: to, Host: sipServerAddress} inviteRequest := sip.NewRequest(sip.INVITE, inviteRecipent) inviteRequest.SetDestination(sipServerAddress) - inviteRequest.SetBody(offer) + inviteRequest.SetBody(offerData) inviteRequest.AppendHeader(sip.NewHeader("Content-Type", "application/sdp")) tx, err := sipClient.TransactionRequest(inviteRequest)