github.com/pion/webrtc/v4@v4.0.1/sdpsemantics_test.go (about) 1 // SPDX-FileCopyrightText: 2023 The Pion community <https://pion.ly> 2 // SPDX-License-Identifier: MIT 3 4 //go:build !js 5 // +build !js 6 7 package webrtc 8 9 import ( 10 "encoding/json" 11 "errors" 12 "strings" 13 "testing" 14 "time" 15 16 "github.com/pion/sdp/v3" 17 "github.com/pion/transport/v3/test" 18 "github.com/stretchr/testify/assert" 19 ) 20 21 func TestSDPSemantics_String(t *testing.T) { 22 testCases := []struct { 23 value SDPSemantics 24 expectedString string 25 }{ 26 {SDPSemanticsUnifiedPlanWithFallback, "unified-plan-with-fallback"}, 27 {SDPSemanticsPlanB, "plan-b"}, 28 {SDPSemanticsUnifiedPlan, "unified-plan"}, 29 } 30 31 assert.Equal(t, 32 ErrUnknownType.Error(), 33 SDPSemantics(42).String(), 34 ) 35 36 for i, testCase := range testCases { 37 assert.Equal(t, 38 testCase.expectedString, 39 testCase.value.String(), 40 "testCase: %d %v", i, testCase, 41 ) 42 assert.Equal(t, 43 testCase.value, 44 newSDPSemantics(testCase.expectedString), 45 "testCase: %d %v", i, testCase, 46 ) 47 } 48 } 49 50 func TestSDPSemantics_JSON(t *testing.T) { 51 testCases := []struct { 52 value SDPSemantics 53 JSON []byte 54 }{ 55 {SDPSemanticsUnifiedPlanWithFallback, []byte("\"unified-plan-with-fallback\"")}, 56 {SDPSemanticsPlanB, []byte("\"plan-b\"")}, 57 {SDPSemanticsUnifiedPlan, []byte("\"unified-plan\"")}, 58 } 59 60 for i, testCase := range testCases { 61 res, err := json.Marshal(testCase.value) 62 assert.NoError(t, err) 63 assert.Equal(t, 64 testCase.JSON, 65 res, 66 "testCase: %d %v", i, testCase, 67 ) 68 69 var v SDPSemantics 70 err = json.Unmarshal(testCase.JSON, &v) 71 assert.NoError(t, err) 72 assert.Equal(t, v, testCase.value) 73 } 74 } 75 76 // The following tests are for non-standard SDP semantics 77 // (i.e. not unified-unified) 78 79 func getMdNames(sdp *sdp.SessionDescription) []string { 80 mdNames := make([]string, 0, len(sdp.MediaDescriptions)) 81 for _, media := range sdp.MediaDescriptions { 82 mdNames = append(mdNames, media.MediaName.Media) 83 } 84 return mdNames 85 } 86 87 func extractSsrcList(md *sdp.MediaDescription) []string { 88 ssrcMap := map[string]struct{}{} 89 for _, attr := range md.Attributes { 90 if attr.Key == sdp.AttrKeySSRC { 91 ssrc := strings.Fields(attr.Value)[0] 92 ssrcMap[ssrc] = struct{}{} 93 } 94 } 95 ssrcList := make([]string, 0, len(ssrcMap)) 96 for ssrc := range ssrcMap { 97 ssrcList = append(ssrcList, ssrc) 98 } 99 return ssrcList 100 } 101 102 func TestSDPSemantics_PlanBOfferTransceivers(t *testing.T) { 103 report := test.CheckRoutines(t) 104 defer report() 105 106 lim := test.TimeOut(time.Second * 30) 107 defer lim.Stop() 108 109 opc, err := NewPeerConnection(Configuration{ 110 SDPSemantics: SDPSemanticsPlanB, 111 }) 112 assert.NoError(t, err) 113 114 _, err = opc.AddTransceiverFromKind(RTPCodecTypeVideo, RTPTransceiverInit{ 115 Direction: RTPTransceiverDirectionSendrecv, 116 }) 117 assert.NoError(t, err) 118 119 _, err = opc.AddTransceiverFromKind(RTPCodecTypeVideo, RTPTransceiverInit{ 120 Direction: RTPTransceiverDirectionSendrecv, 121 }) 122 assert.NoError(t, err) 123 124 _, err = opc.AddTransceiverFromKind(RTPCodecTypeAudio, RTPTransceiverInit{ 125 Direction: RTPTransceiverDirectionSendrecv, 126 }) 127 assert.NoError(t, err) 128 129 _, err = opc.AddTransceiverFromKind(RTPCodecTypeAudio, RTPTransceiverInit{ 130 Direction: RTPTransceiverDirectionSendrecv, 131 }) 132 assert.NoError(t, err) 133 134 offer, err := opc.CreateOffer(nil) 135 assert.NoError(t, err) 136 137 mdNames := getMdNames(offer.parsed) 138 assert.ObjectsAreEqual(mdNames, []string{"video", "audio", "data"}) 139 140 // Verify that each section has 2 SSRCs (one for each transceiver) 141 for _, section := range []string{"video", "audio"} { 142 for _, media := range offer.parsed.MediaDescriptions { 143 if media.MediaName.Media == section { 144 assert.Len(t, extractSsrcList(media), 2) 145 } 146 } 147 } 148 149 apc, err := NewPeerConnection(Configuration{ 150 SDPSemantics: SDPSemanticsPlanB, 151 }) 152 assert.NoError(t, err) 153 154 assert.NoError(t, apc.SetRemoteDescription(offer)) 155 156 answer, err := apc.CreateAnswer(nil) 157 assert.NoError(t, err) 158 159 mdNames = getMdNames(answer.parsed) 160 assert.ObjectsAreEqual(mdNames, []string{"video", "audio", "data"}) 161 162 closePairNow(t, apc, opc) 163 } 164 165 func TestSDPSemantics_PlanBAnswerSenders(t *testing.T) { 166 report := test.CheckRoutines(t) 167 defer report() 168 169 lim := test.TimeOut(time.Second * 30) 170 defer lim.Stop() 171 172 opc, err := NewPeerConnection(Configuration{ 173 SDPSemantics: SDPSemanticsPlanB, 174 }) 175 assert.NoError(t, err) 176 177 _, err = opc.AddTransceiverFromKind(RTPCodecTypeVideo, RTPTransceiverInit{ 178 Direction: RTPTransceiverDirectionRecvonly, 179 }) 180 assert.NoError(t, err) 181 182 _, err = opc.AddTransceiverFromKind(RTPCodecTypeAudio, RTPTransceiverInit{ 183 Direction: RTPTransceiverDirectionRecvonly, 184 }) 185 assert.NoError(t, err) 186 187 offer, err := opc.CreateOffer(nil) 188 assert.NoError(t, err) 189 190 assert.ObjectsAreEqual(getMdNames(offer.parsed), []string{"video", "audio", "data"}) 191 192 apc, err := NewPeerConnection(Configuration{ 193 SDPSemantics: SDPSemanticsPlanB, 194 }) 195 assert.NoError(t, err) 196 197 video1, err := NewTrackLocalStaticSample(RTPCodecCapability{MimeType: MimeTypeH264, SDPFmtpLine: "level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42001f"}, "1", "1") 198 assert.NoError(t, err) 199 200 _, err = apc.AddTrack(video1) 201 assert.NoError(t, err) 202 203 video2, err := NewTrackLocalStaticSample(RTPCodecCapability{MimeType: MimeTypeH264, SDPFmtpLine: "level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42001f"}, "2", "2") 204 assert.NoError(t, err) 205 206 _, err = apc.AddTrack(video2) 207 assert.NoError(t, err) 208 209 audio1, err := NewTrackLocalStaticSample(RTPCodecCapability{MimeType: MimeTypeOpus}, "3", "3") 210 assert.NoError(t, err) 211 212 _, err = apc.AddTrack(audio1) 213 assert.NoError(t, err) 214 215 audio2, err := NewTrackLocalStaticSample(RTPCodecCapability{MimeType: MimeTypeOpus}, "4", "4") 216 assert.NoError(t, err) 217 218 _, err = apc.AddTrack(audio2) 219 assert.NoError(t, err) 220 221 assert.NoError(t, apc.SetRemoteDescription(offer)) 222 223 answer, err := apc.CreateAnswer(nil) 224 assert.NoError(t, err) 225 226 assert.ObjectsAreEqual(getMdNames(answer.parsed), []string{"video", "audio", "data"}) 227 228 // Verify that each section has 2 SSRCs (one for each sender) 229 for _, section := range []string{"video", "audio"} { 230 for _, media := range answer.parsed.MediaDescriptions { 231 if media.MediaName.Media == section { 232 assert.Lenf(t, extractSsrcList(media), 2, "%q should have 2 SSRCs in Plan-B mode", section) 233 } 234 } 235 } 236 237 closePairNow(t, apc, opc) 238 } 239 240 func TestSDPSemantics_UnifiedPlanWithFallback(t *testing.T) { 241 report := test.CheckRoutines(t) 242 defer report() 243 244 lim := test.TimeOut(time.Second * 30) 245 defer lim.Stop() 246 247 opc, err := NewPeerConnection(Configuration{ 248 SDPSemantics: SDPSemanticsPlanB, 249 }) 250 assert.NoError(t, err) 251 252 _, err = opc.AddTransceiverFromKind(RTPCodecTypeVideo, RTPTransceiverInit{ 253 Direction: RTPTransceiverDirectionRecvonly, 254 }) 255 assert.NoError(t, err) 256 257 _, err = opc.AddTransceiverFromKind(RTPCodecTypeAudio, RTPTransceiverInit{ 258 Direction: RTPTransceiverDirectionRecvonly, 259 }) 260 assert.NoError(t, err) 261 262 offer, err := opc.CreateOffer(nil) 263 assert.NoError(t, err) 264 265 assert.ObjectsAreEqual(getMdNames(offer.parsed), []string{"video", "audio", "data"}) 266 267 apc, err := NewPeerConnection(Configuration{ 268 SDPSemantics: SDPSemanticsUnifiedPlanWithFallback, 269 }) 270 assert.NoError(t, err) 271 272 video1, err := NewTrackLocalStaticSample(RTPCodecCapability{MimeType: MimeTypeH264, SDPFmtpLine: "level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42001f"}, "1", "1") 273 assert.NoError(t, err) 274 275 _, err = apc.AddTrack(video1) 276 assert.NoError(t, err) 277 278 video2, err := NewTrackLocalStaticSample(RTPCodecCapability{MimeType: MimeTypeH264, SDPFmtpLine: "level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42001f"}, "2", "2") 279 assert.NoError(t, err) 280 281 _, err = apc.AddTrack(video2) 282 assert.NoError(t, err) 283 284 audio1, err := NewTrackLocalStaticSample(RTPCodecCapability{MimeType: MimeTypeOpus}, "3", "3") 285 assert.NoError(t, err) 286 287 _, err = apc.AddTrack(audio1) 288 assert.NoError(t, err) 289 290 audio2, err := NewTrackLocalStaticSample(RTPCodecCapability{MimeType: MimeTypeOpus}, "4", "4") 291 assert.NoError(t, err) 292 293 _, err = apc.AddTrack(audio2) 294 assert.NoError(t, err) 295 296 assert.NoError(t, apc.SetRemoteDescription(offer)) 297 298 answer, err := apc.CreateAnswer(nil) 299 assert.NoError(t, err) 300 301 assert.ObjectsAreEqual(getMdNames(answer.parsed), []string{"video", "audio", "data"}) 302 303 extractSsrcList := func(md *sdp.MediaDescription) []string { 304 ssrcMap := map[string]struct{}{} 305 for _, attr := range md.Attributes { 306 if attr.Key == sdp.AttrKeySSRC { 307 ssrc := strings.Fields(attr.Value)[0] 308 ssrcMap[ssrc] = struct{}{} 309 } 310 } 311 ssrcList := make([]string, 0, len(ssrcMap)) 312 for ssrc := range ssrcMap { 313 ssrcList = append(ssrcList, ssrc) 314 } 315 return ssrcList 316 } 317 // Verify that each section has 2 SSRCs (one for each sender) 318 for _, section := range []string{"video", "audio"} { 319 for _, media := range answer.parsed.MediaDescriptions { 320 if media.MediaName.Media == section { 321 assert.Lenf(t, extractSsrcList(media), 2, "%q should have 2 SSRCs in Plan-B fallback mode", section) 322 } 323 } 324 } 325 326 closePairNow(t, apc, opc) 327 } 328 329 // Assert that we can catch Remote SessionDescription that don't match our Semantics 330 func TestSDPSemantics_SetRemoteDescription_Mismatch(t *testing.T) { 331 planBOffer := "v=0\r\no=- 4648475892259889561 3 IN IP4 127.0.0.1\r\ns=-\r\nt=0 0\r\na=group:BUNDLE video audio\r\na=ice-ufrag:1hhfzwf0ijpzm\r\na=ice-pwd:jm5puo2ab1op3vs59ca53bdk7s\r\na=fingerprint:sha-256 40:42:FB:47:87:52:BF:CB:EC:3A:DF:EB:06:DA:2D:B7:2F:59:42:10:23:7B:9D:4C:C9:58:DD:FF:A2:8F:17:67\r\nm=video 9 UDP/TLS/RTP/SAVPF 96\r\nc=IN IP4 0.0.0.0\r\na=rtcp:9 IN IP4 0.0.0.0\r\na=setup:passive\r\na=mid:video\r\na=sendonly\r\na=rtcp-mux\r\na=rtpmap:96 H264/90000\r\na=rtcp-fb:96 nack\r\na=rtcp-fb:96 goog-remb\r\na=fmtp:96 packetization-mode=1;profile-level-id=42e01f\r\na=ssrc:1505338584 cname:10000000b5810aac\r\na=ssrc:1 cname:trackB\r\nm=audio 9 UDP/TLS/RTP/SAVPF 111\r\nc=IN IP4 0.0.0.0\r\na=rtcp:9 IN IP4 0.0.0.0\r\na=setup:passive\r\na=mid:audio\r\na=sendonly\r\na=rtcp-mux\r\na=rtpmap:111 opus/48000/2\r\na=ssrc:697641945 cname:10000000b5810aac\r\n" 332 unifiedPlanOffer := "v=0\r\no=- 4648475892259889561 3 IN IP4 127.0.0.1\r\ns=-\r\nt=0 0\r\na=group:BUNDLE 0 1\r\na=ice-ufrag:1hhfzwf0ijpzm\r\na=ice-pwd:jm5puo2ab1op3vs59ca53bdk7s\r\na=fingerprint:sha-256 40:42:FB:47:87:52:BF:CB:EC:3A:DF:EB:06:DA:2D:B7:2F:59:42:10:23:7B:9D:4C:C9:58:DD:FF:A2:8F:17:67\r\nm=video 9 UDP/TLS/RTP/SAVPF 96\r\nc=IN IP4 0.0.0.0\r\na=rtcp:9 IN IP4 0.0.0.0\r\na=setup:passive\r\na=mid:0\r\na=sendonly\r\na=rtcp-mux\r\na=rtpmap:96 H264/90000\r\na=rtcp-fb:96 nack\r\na=rtcp-fb:96 goog-remb\r\na=fmtp:96 packetization-mode=1;profile-level-id=42e01f\r\na=ssrc:1505338584 cname:10000000b5810aac\r\nm=audio 9 UDP/TLS/RTP/SAVPF 111\r\nc=IN IP4 0.0.0.0\r\na=rtcp:9 IN IP4 0.0.0.0\r\na=setup:passive\r\na=mid:1\r\na=sendonly\r\na=rtcp-mux\r\na=rtpmap:111 opus/48000/2\r\na=ssrc:697641945 cname:10000000b5810aac\r\n" 333 334 report := test.CheckRoutines(t) 335 defer report() 336 337 lim := test.TimeOut(time.Second * 30) 338 defer lim.Stop() 339 340 t.Run("PlanB", func(t *testing.T) { 341 pc, err := NewPeerConnection(Configuration{ 342 SDPSemantics: SDPSemanticsUnifiedPlan, 343 }) 344 assert.NoError(t, err) 345 346 err = pc.SetRemoteDescription(SessionDescription{SDP: planBOffer, Type: SDPTypeOffer}) 347 assert.NoError(t, err) 348 349 _, err = pc.CreateAnswer(nil) 350 assert.True(t, errors.Is(err, ErrIncorrectSDPSemantics)) 351 352 assert.NoError(t, pc.Close()) 353 }) 354 355 t.Run("UnifiedPlan", func(t *testing.T) { 356 pc, err := NewPeerConnection(Configuration{ 357 SDPSemantics: SDPSemanticsPlanB, 358 }) 359 assert.NoError(t, err) 360 361 err = pc.SetRemoteDescription(SessionDescription{SDP: unifiedPlanOffer, Type: SDPTypeOffer}) 362 assert.NoError(t, err) 363 364 _, err = pc.CreateAnswer(nil) 365 assert.True(t, errors.Is(err, ErrIncorrectSDPSemantics)) 366 367 assert.NoError(t, pc.Close()) 368 }) 369 }