// SPDX-FileCopyrightText: 2023 The Pion community // SPDX-License-Identifier: MIT package sdp import ( "errors" "strconv" "testing" "github.com/stretchr/testify/require" ) const ( BaseSDP = "v=0\r\n" + "o=jdoe 2890844526 2890842807 IN IP4 10.47.16.5\r\n" + "s=SDP Seminar\r\n" SessionInformationSDP = BaseSDP + "i=A Seminar on the session description protocol\r\n" + "t=3034423619 3042462419\r\n" // https://tools.ietf.org/html/rfc4566#section-5 // Parsers SHOULD be tolerant and also accept records terminated // with a single newline character. SessionInformationSDPLFOnly = "v=0\n" + "o=jdoe 2890844526 2890842807 IN IP4 10.47.16.5\n" + "s=SDP Seminar\n" + "i=A Seminar on the session description protocol\n" + "t=3034423619 3042462419\n" // SessionInformationSDPCROnly = "v=0\r" + // "o=jdoe 2890844526 2890842807 IN IP4 10.47.16.5\r" + // "s=SDP Seminar\r" // "i=A Seminar on the session description protocol\r" + // "t=3034423619 3042462419\r" // Other SDP parsers (e.g. one in VLC media player) allow // empty lines. SessionInformationSDPExtraCRLF = "v=0\r\n" + "o=jdoe 2890844526 2890842807 IN IP4 10.47.16.5\r\n" + "\r\n" + "s=SDP Seminar\r\n" + "\r\n" + "i=A Seminar on the session description protocol\r\n" + "\r\n" + "t=3034423619 3042462419\r\n" + "\r\n" URISDP = BaseSDP + "u=http://www.example.com/seminars/sdp.pdf\r\n" + "t=3034423619 3042462419\r\n" EmailAddressSDP = BaseSDP + "e=j.doe@example.com (Jane Doe)\r\n" + "t=3034423619 3042462419\r\n" PhoneNumberSDP = BaseSDP + "p=+1 617 555-6011\r\n" + "t=3034423619 3042462419\r\n" SessionConnectionInformationSDP = BaseSDP + "c=IN IP4 224.2.17.12/127\r\n" + "t=3034423619 3042462419\r\n" SessionBandwidthSDP = BaseSDP + "b=X-YZ:128\r\n" + "b=AS:12345\r\n" + "t=3034423619 3042462419\r\n" TimingSDP = BaseSDP + "t=2873397496 2873404696\r\n" // Short hand time notation is converted into NTP timestamp format in // seconds. Because of that unittest comparisons will fail as the same time // will be expressed in different units. RepeatTimesSDP = TimingSDP + "r=604800 3600 0 90000\r\n" + "r=3d 2h 0 21h\r\n" RepeatTimesSDPExpected = TimingSDP + "r=604800 3600 0 90000\r\n" + "r=259200 7200 0 75600\r\n" RepeatTimesSDPExtraCRLF = RepeatTimesSDPExpected + "\r\n" // The expected value looks a bit different for the same reason as mentioned // above regarding RepeatTimes. TimeZonesSDP = TimingSDP + "r=2882844526 -1h 2898848070 0\r\n" TimeZonesSDPExpected = TimingSDP + "r=2882844526 -3600 2898848070 0\r\n" TimeZonesSDP2 = TimingSDP + "z=2882844526 -3600 2898848070 0\r\n" TimeZonesSDP2ExtraCRLF = TimeZonesSDP2 + "\r\n" SessionEncryptionKeySDP = TimingSDP + "k=prompt\r\n" SessionEncryptionKeySDPExtraCRLF = SessionEncryptionKeySDP + "\r\n" SessionAttributesSDP = TimingSDP + "a=rtpmap:96 opus/48000\r\n" MediaNameSDP = TimingSDP + "m=video 51372 RTP/AVP 99\r\n" + "m=audio 54400 RTP/SAVPF 0 96\r\n" + "m=message 5028 TCP/MSRP *\r\n" MediaNameSDPExtraCRLF = MediaNameSDP + "\r\n" MediaTitleSDP = MediaNameSDP + "i=Vivamus a posuere nisl\r\n" MediaConnectionInformationSDP = MediaNameSDP + "c=IN IP4 203.0.113.1\r\n" MediaConnectionInformationSDPExtraCRLF = MediaConnectionInformationSDP + "\r\n" MediaDescriptionOutOfOrderSDP = MediaNameSDP + "a=rtpmap:99 h263-1998/90000\r\n" + "a=candidate:0 1 UDP 2113667327 203.0.113.1 54400 typ host\r\n" + "c=IN IP4 203.0.113.1\r\n" + "i=Vivamus a posuere nisl\r\n" MediaDescriptionOutOfOrderSDPActual = MediaNameSDP + "i=Vivamus a posuere nisl\r\n" + "c=IN IP4 203.0.113.1\r\n" + "a=rtpmap:99 h263-1998/90000\r\n" + "a=candidate:0 1 UDP 2113667327 203.0.113.1 54400 typ host\r\n" MediaBandwidthSDP = MediaNameSDP + "b=X-YZ:128\r\n" + "b=AS:12345\r\n" + "b=TIAS:12345\r\n" + "b=RS:12345\r\n" + "b=RR:12345\r\n" MediaEncryptionKeySDP = MediaNameSDP + "k=prompt\r\n" MediaEncryptionKeySDPExtraCRLF = MediaEncryptionKeySDP + "\r\n" MediaAttributesSDP = MediaNameSDP + "a=rtpmap:99 h263-1998/90000\r\n" + "a=candidate:0 1 UDP 2113667327 203.0.113.1 54400 typ host\r\n" + "a=rtcp-fb:97 ccm fir\r\n" + "a=rtcp-fb:97 nack\r\n" + "a=rtcp-fb:97 nack pli\r\n" MediaBfcpSDP = TimingSDP + "m=application 3238 UDP/BFCP *\r\n" + "a=sendrecv\r\n" + "a=setup:actpass\r\n" + "a=connection:new\r\n" + "a=floorctrl:c-s\r\n" MediaCubeSDP = TimingSDP + "m=application 2455 UDP/UDT/IX *\r\n" + "a=ixmap:0 ping\r\n" + "a=ixmap:2 xccp\r\n" MediaTCPMRCPv2 = TimingSDP + "m=application 1544 TCP/MRCPv2 1\r\n" MediaTCPTLSMRCPv2 = TimingSDP + "m=application 1544 TCP/TLS/MRCPv2 1\r\n" CanonicalUnmarshalSDP = "v=0\r\n" + "o=jdoe 2890844526 2890842807 IN IP4 10.47.16.5\r\n" + "s=SDP Seminar\r\n" + "i=A Seminar on the session description protocol\r\n" + "u=http://www.example.com/seminars/sdp.pdf\r\n" + "e=j.doe@example.com (Jane Doe)\r\n" + "p=+1 617 555-6011\r\n" + "c=IN IP4 224.2.17.12/127\r\n" + "b=X-YZ:128\r\n" + "b=AS:12345\r\n" + "t=2873397496 2873404696\r\n" + "t=3034423619 3042462419\r\n" + "r=604800 3600 0 90000\r\n" + "z=2882844526 -3600 2898848070 0\r\n" + "k=prompt\r\n" + "a=candidate:0 1 UDP 2113667327 203.0.113.1 54400 typ host\r\n" + "a=recvonly\r\n" + "m=audio 49170 RTP/AVP 0\r\n" + "i=Vivamus a posuere nisl\r\n" + "c=IN IP4 203.0.113.1\r\n" + "b=X-YZ:128\r\n" + "k=prompt\r\n" + "a=sendrecv\r\n" + "m=video 51372 RTP/AVP 99\r\n" + "a=rtpmap:99 h263-1998/90000\r\n" ) func TestRoundTrip(t *testing.T) { for _, test := range []struct { Name string SDP string Actual string }{ { Name: "SessionInformationSDPLFOnly", SDP: SessionInformationSDPLFOnly, Actual: SessionInformationSDP, }, // { // Name: "SessionInformationSDPCROnly", // SDP: SessionInformationSDPCROnly, // Actual: SessionInformationSDPBaseSDP, // }, { Name: "SessionInformationSDPExtraCRLF", SDP: SessionInformationSDPExtraCRLF, Actual: SessionInformationSDP, }, { Name: "SessionInformation", SDP: SessionInformationSDP, }, { Name: "URI", SDP: URISDP, }, { Name: "EmailAddress", SDP: EmailAddressSDP, }, { Name: "PhoneNumber", SDP: PhoneNumberSDP, }, { Name: "RepeatTimesSDPExtraCRLF", SDP: RepeatTimesSDPExtraCRLF, Actual: RepeatTimesSDPExpected, }, { Name: "SessionConnectionInformation", SDP: SessionConnectionInformationSDP, }, { Name: "SessionBandwidth", SDP: SessionBandwidthSDP, }, { Name: "SessionEncryptionKey", SDP: SessionEncryptionKeySDP, }, { Name: "SessionEncryptionKeyExtraCRLF", SDP: SessionEncryptionKeySDPExtraCRLF, Actual: SessionEncryptionKeySDP, }, { Name: "SessionAttributes", SDP: SessionAttributesSDP, }, { Name: "TimeZonesSDP2ExtraCRLF", SDP: TimeZonesSDP2ExtraCRLF, Actual: TimeZonesSDP2, }, { Name: "MediaName", SDP: MediaNameSDP, }, { Name: "MediaNameExtraCRLF", SDP: MediaNameSDPExtraCRLF, Actual: MediaNameSDP, }, { Name: "MediaTitle", SDP: MediaTitleSDP, }, { Name: "MediaConnectionInformation", SDP: MediaConnectionInformationSDP, }, { Name: "MediaConnectionInformationExtraCRLF", SDP: MediaConnectionInformationSDPExtraCRLF, Actual: MediaConnectionInformationSDP, }, { Name: "MediaDescriptionOutOfOrder", SDP: MediaDescriptionOutOfOrderSDP, Actual: MediaDescriptionOutOfOrderSDPActual, }, { Name: "MediaBandwidth", SDP: MediaBandwidthSDP, }, { Name: "MediaEncryptionKey", SDP: MediaEncryptionKeySDP, }, { Name: "MediaEncryptionKeyExtraCRLF", SDP: MediaEncryptionKeySDPExtraCRLF, Actual: MediaEncryptionKeySDP, }, { Name: "MediaAttributes", SDP: MediaAttributesSDP, }, { Name: "CanonicalUnmarshal", SDP: CanonicalUnmarshalSDP, }, { Name: "MediaBfcpSDP", SDP: MediaBfcpSDP, }, { Name: "MediaCubeSDP", SDP: MediaCubeSDP, }, { Name: "MediaTCPMRCPv2", SDP: MediaTCPMRCPv2, }, { Name: "MediaTCPTLSMRCPv2", SDP: MediaTCPTLSMRCPv2, }, } { test := test t.Run(test.Name, func(t *testing.T) { sd := &SessionDescription{} err := sd.UnmarshalString(test.SDP) if got, want := err, error(nil); !errors.Is(got, want) { t.Fatalf("Unmarshal:\nerr=%v\nwant=%v", got, want) } actual, err := sd.Marshal() if got, want := err, error(nil); !errors.Is(got, want) { t.Fatalf("Marshal:\nerr=%v\nwant=%v", got, want) } want := test.SDP if test.Actual != "" { want = test.Actual } if got := string(actual); got != want { t.Fatalf("Marshal:\ngot=%s\nwant=%s", strconv.Quote(got), strconv.Quote(want), ) } }) } } func TestUnmarshalRepeatTimes(t *testing.T) { sd := &SessionDescription{} if err := sd.UnmarshalString(RepeatTimesSDP); err != nil { t.Errorf("error: %v", err) } actual, err := sd.Marshal() if got, want := err, error(nil); !errors.Is(got, want) { t.Fatalf("Marshal(): err=%v, want %v", got, want) } if string(actual) != RepeatTimesSDPExpected { t.Errorf("error:\n\nEXPECTED:\n%v\nACTUAL:\n%v", RepeatTimesSDPExpected, string(actual)) } err = sd.UnmarshalString(TimingSDP + "r=\r\n") if got, want := err, errSDPInvalidValue; !errors.Is(got, want) { t.Fatalf("Marshal(): err=%v, want %v", got, want) } } func TestUnmarshalTimeZones(t *testing.T) { sd := &SessionDescription{} if err := sd.UnmarshalString(TimeZonesSDP); err != nil { t.Errorf("error: %v", err) } actual, err := sd.Marshal() if got, want := err, error(nil); !errors.Is(got, want) { t.Fatalf("Marshal(): err=%v, want %v", got, want) } if string(actual) != TimeZonesSDPExpected { t.Errorf("error:\n\nEXPECTED:\n%v\nACTUAL:\n%v", TimeZonesSDPExpected, string(actual)) } } func TestUnmarshalNonNilAddress(t *testing.T) { in := "v=0\r\no=0 0 0 IN IP4 0\r\ns=0\r\nc=IN IP4\r\nt=0 0\r\n" var sd SessionDescription err := sd.UnmarshalString(in) if err != nil { t.Fatalf("failed to unmarshal %q", in) } out, err := sd.Marshal() if err != nil { t.Errorf("failed to marshal unmarshalled %q", in) } if string(out) != in { t.Errorf("round trip = %q want %q", out, in) } } func TestUnmarshalZeroValues(t *testing.T) { in := "v=0\r\no=0 0 0 IN IP4 0\r\ns=\r\nt=0 0\r\n" var sd SessionDescription require.NoError(t, sd.UnmarshalString(in)) out, err := sd.Marshal() if err != nil { require.NoError(t, err) } require.Equal(t, in, string(out)) } func BenchmarkUnmarshal(b *testing.B) { b.ReportAllocs() for i := 0; i < b.N; i++ { var sd SessionDescription err := sd.UnmarshalString(CanonicalUnmarshalSDP) if err != nil { b.Fatal(err) } } }