// Copyright 2020 Justine Alexandra Roberts Tunney // // 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_test import ( "fmt" "gb-cms/sdp" "testing" ) type sdpTest struct { name string // arbitrary name for test s string // raw sdp input to parse s2 string // non-blank if sdp looks different when we format it sdp *sdp.SDP // memory structure of 's' after parsing err error } var sdpTests = []sdpTest{ { name: "Asterisk PCMU+DTMF", s: ("v=0\r\n" + "o=root 31589 31589 IN IP4 10.0.0.38\r\n" + "s=session\r\n" + "c=IN IP4 10.0.0.38\r\n" + "t=0 0\r\n" + "m=audio 30126 RTP/AVP 0 101\r\n" + "a=rtpmap:0 PCMU/8000\r\n" + "a=rtpmap:101 telephone-event/8000\r\n" + "a=fmtp:101 0-16\r\n" + "a=silenceSupp:off - - - -\r\n" + "a=ptime:20\r\n" + "a=sendrecv\r\n"), sdp: &sdp.SDP{ Origin: sdp.Origin{ User: "root", ID: "31589", Version: "31589", Addr: "10.0.0.38", }, Session: "session", Time: "0 0", Addr: "10.0.0.38", Audio: &sdp.Media{ Proto: "RTP/AVP", Port: 30126, Codecs: []sdp.Codec{ {PT: 0, Name: "PCMU", Rate: 8000}, {PT: 101, Name: "telephone-event", Rate: 8000, Fmtp: "0-16"}, }, }, Attrs: [][2]string{ {"silenceSupp", "off - - - -"}, }, Ptime: 20, }, }, { name: "Audio+Video+Implicit+Fmtp", s: "v=0\r\n" + "o=- 3366701332 3366701332 IN IP4 1.2.3.4\r\n" + "c=IN IP4 1.2.3.4\r\n" + "m=audio 32898 RTP/AVP 18\r\n" + "m=video 32900 RTP/AVP 34\r\n" + "a=fmtp:18 annexb=yes", s2: "v=0\r\n" + "o=- 3366701332 3366701332 IN IP4 1.2.3.4\r\n" + "s=pokémon\r\n" + "c=IN IP4 1.2.3.4\r\n" + "t=0 0\r\n" + "m=audio 32898 RTP/AVP 18\r\n" + "a=rtpmap:18 G729/8000\r\n" + "a=fmtp:18 annexb=yes\r\n" + "m=video 32900 RTP/AVP 34\r\n" + "a=rtpmap:34 H263/90000\r\n" + "a=sendrecv\r\n", sdp: &sdp.SDP{ Origin: sdp.Origin{ User: "-", ID: "3366701332", Version: "3366701332", Addr: "1.2.3.4", }, Addr: "1.2.3.4", Session: "pokémon", Time: "0 0", Audio: &sdp.Media{ Proto: "RTP/AVP", Port: 32898, Codecs: []sdp.Codec{ {PT: 18, Name: "G729", Rate: 8000, Fmtp: "annexb=yes"}, }, }, Video: &sdp.Media{ Proto: "RTP/AVP", Port: 32900, Codecs: []sdp.Codec{ {PT: 34, Name: "H263", Rate: 90000}, }, }, Attrs: [][2]string{}, }, }, { name: "Implicit Codecs", s: "v=0\r\n" + "o=- 3366701332 3366701332 IN IP4 1.2.3.4\r\n" + "s=-\r\n" + "c=IN IP4 1.2.3.4\r\n" + "t=0 0\r\n" + "m=audio 32898 RTP/AVP 9 18 0 101\r\n" + "a=rtpmap:101 telephone-event/8000\r\n" + "a=ptime:20\r\n", s2: "v=0\r\n" + "o=- 3366701332 3366701332 IN IP4 1.2.3.4\r\n" + "s=-\r\n" + "c=IN IP4 1.2.3.4\r\n" + "t=0 0\r\n" + "m=audio 32898 RTP/AVP 9 18 0 101\r\n" + "a=rtpmap:9 G722/8000\r\n" + "a=rtpmap:18 G729/8000\r\n" + "a=rtpmap:0 PCMU/8000\r\n" + "a=rtpmap:101 telephone-event/8000\r\n" + "a=ptime:20\r\n" + "a=sendrecv\r\n", sdp: &sdp.SDP{ Origin: sdp.Origin{ User: "-", ID: "3366701332", Version: "3366701332", Addr: "1.2.3.4", }, Session: "-", Time: "0 0", Addr: "1.2.3.4", Audio: &sdp.Media{ Proto: "RTP/AVP", Port: 32898, Codecs: []sdp.Codec{ {PT: 9, Name: "G722", Rate: 8000}, {PT: 18, Name: "G729", Rate: 8000}, {PT: 0, Name: "PCMU", Rate: 8000}, {PT: 101, Name: "telephone-event", Rate: 8000}, }, }, Ptime: 20, Attrs: [][2]string{}, }, }, { name: "IPv6", s: "v=0\r\n" + "o=- 3366701332 3366701332 IN IP6 dead:beef::666\r\n" + "s=-\r\n" + "c=IN IP6 dead:beef::666\r\n" + "t=0 0\r\n" + "m=audio 32898 RTP/AVP 9 18 0 101\r\n" + "a=rtpmap:101 telephone-event/8000\r\n" + "a=ptime:20\r\n", s2: "v=0\r\n" + "o=- 3366701332 3366701332 IN IP6 dead:beef::666\r\n" + "s=-\r\n" + "c=IN IP6 dead:beef::666\r\n" + "t=0 0\r\n" + "m=audio 32898 RTP/AVP 9 18 0 101\r\n" + "a=rtpmap:9 G722/8000\r\n" + "a=rtpmap:18 G729/8000\r\n" + "a=rtpmap:0 PCMU/8000\r\n" + "a=rtpmap:101 telephone-event/8000\r\n" + "a=ptime:20\r\n" + "a=sendrecv\r\n", sdp: &sdp.SDP{ Origin: sdp.Origin{ User: "-", ID: "3366701332", Version: "3366701332", Addr: "dead:beef::666", }, Session: "-", Time: "0 0", Addr: "dead:beef::666", Audio: &sdp.Media{ Proto: "RTP/AVP", Port: 32898, Codecs: []sdp.Codec{ {PT: 9, Name: "G722", Rate: 8000}, {PT: 18, Name: "G729", Rate: 8000}, {PT: 0, Name: "PCMU", Rate: 8000}, {PT: 101, Name: "telephone-event", Rate: 8000}, }, }, Ptime: 20, Attrs: [][2]string{}, }, }, { name: "pjmedia long sdp is long", s: ("v=0\r\n" + "o=- 3457169218 3457169218 IN IP4 10.11.34.37\r\n" + "s=pjmedia\r\n" + "c=IN IP4 10.11.34.37\r\n" + "t=0 0\r\n" + "m=audio 4000 RTP/AVP 103 102 104 113 3 0 8 9 101\r\n" + "a=rtpmap:103 speex/16000\r\n" + "a=rtpmap:102 speex/8000\r\n" + "a=rtpmap:104 speex/32000\r\n" + "a=rtpmap:113 iLBC/8000\r\n" + "a=fmtp:113 mode=30\r\n" + "a=rtpmap:3 GSM/8000\r\n" + "a=rtpmap:0 PCMU/8000\r\n" + "a=rtpmap:8 PCMA/8000\r\n" + "a=rtpmap:9 G722/8000\r\n" + "a=rtpmap:101 telephone-event/8000\r\n" + "a=fmtp:101 0-15\r\n" + "a=rtcp:4001 IN IP4 10.11.34.37\r\n" + "a=X-nat:0\r\n" + "a=ptime:20\r\n" + "a=sendrecv\r\n"), sdp: &sdp.SDP{ Origin: sdp.Origin{ User: "-", ID: "3457169218", Version: "3457169218", Addr: "10.11.34.37", }, Session: "pjmedia", Time: "0 0", Addr: "10.11.34.37", Audio: &sdp.Media{ Proto: "RTP/AVP", Port: 4000, Codecs: []sdp.Codec{ {PT: 103, Name: "speex", Rate: 16000}, {PT: 102, Name: "speex", Rate: 8000}, {PT: 104, Name: "speex", Rate: 32000}, {PT: 113, Name: "iLBC", Rate: 8000, Fmtp: "mode=30"}, {PT: 3, Name: "GSM", Rate: 8000}, {PT: 0, Name: "PCMU", Rate: 8000}, {PT: 8, Name: "PCMA", Rate: 8000}, {PT: 9, Name: "G722", Rate: 8000}, {PT: 101, Name: "telephone-event", Rate: 8000, Fmtp: "0-15"}, }, }, Ptime: 20, Attrs: [][2]string{ {"rtcp", "4001 IN IP4 10.11.34.37"}, {"X-nat", "0"}, }, }, }, { name: "mp3 tcp", s: ("v=0\r\n" + "o=- 3366701332 3366701334 IN IP4 10.11.34.37\r\n" + "s=squigglies\r\n" + "c=IN IP6 dead:beef::666\r\n" + "t=0 0\r\n" + "m=audio 80 TCP/IP 111\r\n" + "a=rtpmap:111 MP3/44100/2\r\n" + "a=sendonly\r\n"), sdp: &sdp.SDP{ Origin: sdp.Origin{ User: "-", ID: "3366701332", Version: "3366701334", Addr: "10.11.34.37", }, Session: "squigglies", Time: "0 0", Addr: "dead:beef::666", SendOnly: true, Audio: &sdp.Media{ Proto: "TCP/IP", Port: 80, Codecs: []sdp.Codec{ {PT: 111, Name: "MP3", Rate: 44100, Param: "2"}, }, }, Attrs: [][2]string{}, }, }, } func sdpCompareCodec(t *testing.T, name string, correct, codec *sdp.Codec) { if correct != nil && codec == nil { t.Error(name, "not found") } if correct == nil && codec != nil { t.Error(name, "DO NOT WANT", codec) } if codec == nil { return } if correct.PT != codec.PT { t.Error(name, "PT", correct.PT, "!=", codec.PT) } if correct.Name != codec.Name { t.Error(name, "Name", correct.Name, "!=", codec.Name) } if correct.Rate != codec.Rate { t.Error(name, "Rate", correct.Rate, "!=", codec.Rate) } if correct.Param != codec.Param { t.Error(name, "Param", correct.Param, "!=", codec.Param) } if correct.Fmtp != codec.Fmtp { t.Error(name, "Fmtp", correct.Fmtp, "!=", codec.Fmtp) } } func sdpCompareCodecs(t *testing.T, name string, corrects, codecs []sdp.Codec) { if corrects != nil && codecs == nil { t.Error(name, "codecs not found") } if corrects == nil && codecs != nil { t.Error(name, "codecs WUT", codecs) } if corrects == nil || codecs == nil { return } if len(corrects) != len(codecs) { t.Error(name, "len(Codecs)", len(corrects), "!=", len(codecs)) } else { for i := range corrects { c1, c2 := &corrects[i], &codecs[i] if c1 == nil || c2 == nil { t.Error(name, "where my codecs at?") } else { sdpCompareCodec(t, name, c1, c2) } } } } func sdpCompareMedia(t *testing.T, name string, correct, media *sdp.Media) { if correct != nil && media == nil { t.Error(name, "not found") } if correct == nil && media != nil { t.Error(name, "DO NOT WANT", media) } if correct == nil || media == nil { return } if correct.Proto != media.Proto { t.Error(name, "Proto", correct.Proto, "!=", media.Proto) } if correct.Port != media.Port { t.Error(name, "Port", correct.Port, "!=", media.Port) } if media.Codecs == nil || len(media.Codecs) < 1 { t.Error(name, "Must have at least one codec") } sdpCompareCodecs(t, name, correct.Codecs, media.Codecs) } func TestParse(t *testing.T) { sdpStr := "v=0\r\n" + "o=34020099991320000015 2950 2950 IN IP4 192.168.1.64\r\n" + "s=Play\r\n" + "c=IN IP4 192.168.1.64\r\n" + "t=2 3\r\n" + "m=audio 15066 RTP/AVP 8 96\r\n" + "a=recvonly\r\n" + "a=rtpmap:8 PCMA/8000\r\n" + "a=rtpmap:96 PS/90000\r\n" + "y=0200000017\r\n" + "f=v/////a/1/8/1\r\n" for _, test := range sdpTests { //sdp, err := sdp.Parse(test.s) sdp, err := sdp.Parse(sdpStr) if err != nil { if test.err == nil { t.Errorf("%v", err) continue } else { // test was supposed to fail panic("todo") } } if test.sdp.Addr != sdp.Addr { t.Error(test.name, "Addr", test.sdp.Addr, "!=", sdp.Addr) } if test.sdp.Origin.User != sdp.Origin.User { t.Error(test.name, "Origin.User", test.sdp.Origin.User, "!=", sdp.Origin.User) } if test.sdp.Origin.ID != sdp.Origin.ID { t.Error(test.name, "Origin.Username doesn't match") } if test.sdp.Origin.Version != sdp.Origin.Version { t.Error(test.name, "Origin.Version doesn't match") } if test.sdp.Origin.Addr != sdp.Origin.Addr { t.Error(test.name, "Origin.Addr doesn't match") } if test.sdp.Session != sdp.Session { t.Error(test.name, "Session", test.sdp.Session, "!=", sdp.Session) } if test.sdp.Time != sdp.Time { t.Error(test.name, "Time", test.sdp.Time, "!=", sdp.Time) } if test.sdp.Ptime != sdp.Ptime { t.Error(test.name, "Ptime", test.sdp.Ptime, "!=", sdp.Ptime) } if test.sdp.RecvOnly != sdp.RecvOnly { t.Error(test.name, "RecvOnly doesn't match") } if test.sdp.SendOnly != sdp.SendOnly { t.Error(test.name, "SendOnly doesn't match") } if test.sdp.Attrs != nil { if sdp.Attrs == nil { t.Error(test.name, "Attrs weren't extracted") } else if len(sdp.Attrs) != len(test.sdp.Attrs) { t.Error(test.name, "Attrs length not same") } else { for i := range sdp.Attrs { p1, p2 := test.sdp.Attrs[i], sdp.Attrs[i] if p1[0] != p2[0] || p1[1] != p2[1] { t.Error(test.name, "attr", p1, "!=", p2) break } } } } else { if sdp.Attrs != nil { t.Error(test.name, "unexpected attrs", sdp.Attrs) } } sdpCompareMedia(t, "Audio", test.sdp.Audio, sdp.Audio) sdpCompareMedia(t, "Video", test.sdp.Video, sdp.Video) } } func TestFormatSDP(t *testing.T) { for _, test := range sdpTests { sdp := test.sdp.String() s := test.s if test.s2 != "" { s = test.s2 } if s != sdp { t.Error("\n" + test.name + "\n\n" + s + "\nIS NOT\n\n" + sdp) fmt.Printf("%s", sdp) fmt.Printf("pokémon") } } }