github.com/pion/webrtc/v4@v4.0.1/mediaengine_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 "fmt" 11 "regexp" 12 "strings" 13 "testing" 14 15 "github.com/pion/sdp/v3" 16 "github.com/pion/transport/v3/test" 17 "github.com/stretchr/testify/assert" 18 ) 19 20 // pion/webrtc#1078 21 func TestOpusCase(t *testing.T) { 22 pc, err := NewPeerConnection(Configuration{}) 23 assert.NoError(t, err) 24 25 _, err = pc.AddTransceiverFromKind(RTPCodecTypeAudio) 26 assert.NoError(t, err) 27 28 offer, err := pc.CreateOffer(nil) 29 assert.NoError(t, err) 30 31 assert.True(t, regexp.MustCompile(`(?m)^a=rtpmap:\d+ opus/48000/2`).MatchString(offer.SDP)) 32 assert.NoError(t, pc.Close()) 33 } 34 35 // pion/example-webrtc-applications#89 36 func TestVideoCase(t *testing.T) { 37 pc, err := NewPeerConnection(Configuration{}) 38 assert.NoError(t, err) 39 40 _, err = pc.AddTransceiverFromKind(RTPCodecTypeVideo) 41 assert.NoError(t, err) 42 43 offer, err := pc.CreateOffer(nil) 44 assert.NoError(t, err) 45 46 assert.True(t, regexp.MustCompile(`(?m)^a=rtpmap:\d+ H264/90000`).MatchString(offer.SDP)) 47 assert.True(t, regexp.MustCompile(`(?m)^a=rtpmap:\d+ VP8/90000`).MatchString(offer.SDP)) 48 assert.True(t, regexp.MustCompile(`(?m)^a=rtpmap:\d+ VP9/90000`).MatchString(offer.SDP)) 49 assert.NoError(t, pc.Close()) 50 } 51 52 func TestMediaEngineRemoteDescription(t *testing.T) { 53 mustParse := func(raw string) sdp.SessionDescription { 54 s := sdp.SessionDescription{} 55 assert.NoError(t, s.Unmarshal([]byte(raw))) 56 return s 57 } 58 59 t.Run("No Media", func(t *testing.T) { 60 const noMedia = `v=0 61 o=- 4596489990601351948 2 IN IP4 127.0.0.1 62 s=- 63 t=0 0 64 ` 65 m := MediaEngine{} 66 assert.NoError(t, m.RegisterDefaultCodecs()) 67 assert.NoError(t, m.updateFromRemoteDescription(mustParse(noMedia))) 68 69 assert.False(t, m.negotiatedVideo) 70 assert.False(t, m.negotiatedAudio) 71 }) 72 73 t.Run("Enable Opus", func(t *testing.T) { 74 const opusSamePayload = `v=0 75 o=- 4596489990601351948 2 IN IP4 127.0.0.1 76 s=- 77 t=0 0 78 m=audio 9 UDP/TLS/RTP/SAVPF 111 79 a=rtpmap:111 opus/48000/2 80 a=fmtp:111 minptime=10; useinbandfec=1 81 ` 82 83 m := MediaEngine{} 84 assert.NoError(t, m.RegisterDefaultCodecs()) 85 assert.NoError(t, m.updateFromRemoteDescription(mustParse(opusSamePayload))) 86 87 assert.False(t, m.negotiatedVideo) 88 assert.True(t, m.negotiatedAudio) 89 90 opusCodec, _, err := m.getCodecByPayload(111) 91 assert.NoError(t, err) 92 assert.Equal(t, opusCodec.MimeType, MimeTypeOpus) 93 }) 94 95 t.Run("Change Payload Type", func(t *testing.T) { 96 const opusSamePayload = `v=0 97 o=- 4596489990601351948 2 IN IP4 127.0.0.1 98 s=- 99 t=0 0 100 m=audio 9 UDP/TLS/RTP/SAVPF 112 101 a=rtpmap:112 opus/48000/2 102 a=fmtp:112 minptime=10; useinbandfec=1 103 ` 104 105 m := MediaEngine{} 106 assert.NoError(t, m.RegisterDefaultCodecs()) 107 assert.NoError(t, m.updateFromRemoteDescription(mustParse(opusSamePayload))) 108 109 assert.False(t, m.negotiatedVideo) 110 assert.True(t, m.negotiatedAudio) 111 112 _, _, err := m.getCodecByPayload(111) 113 assert.Error(t, err) 114 115 opusCodec, _, err := m.getCodecByPayload(112) 116 assert.NoError(t, err) 117 assert.Equal(t, opusCodec.MimeType, MimeTypeOpus) 118 }) 119 120 t.Run("Ambiguous Payload Type", func(t *testing.T) { 121 const opusSamePayload = `v=0 122 o=- 4596489990601351948 2 IN IP4 127.0.0.1 123 s=- 124 t=0 0 125 m=audio 9 UDP/TLS/RTP/SAVPF 96 126 a=rtpmap:96 opus/48000/2 127 a=fmtp:96 minptime=10; useinbandfec=1 128 ` 129 130 m := MediaEngine{} 131 assert.NoError(t, m.RegisterDefaultCodecs()) 132 assert.NoError(t, m.updateFromRemoteDescription(mustParse(opusSamePayload))) 133 134 assert.False(t, m.negotiatedVideo) 135 assert.True(t, m.negotiatedAudio) 136 137 opusCodec, _, err := m.getCodecByPayload(96) 138 assert.NoError(t, err) 139 assert.Equal(t, opusCodec.MimeType, MimeTypeOpus) 140 }) 141 142 t.Run("Case Insensitive", func(t *testing.T) { 143 const opusUpcase = `v=0 144 o=- 4596489990601351948 2 IN IP4 127.0.0.1 145 s=- 146 t=0 0 147 m=audio 9 UDP/TLS/RTP/SAVPF 111 148 a=rtpmap:111 OPUS/48000/2 149 a=fmtp:111 minptime=10; useinbandfec=1 150 ` 151 152 m := MediaEngine{} 153 assert.NoError(t, m.RegisterDefaultCodecs()) 154 assert.NoError(t, m.updateFromRemoteDescription(mustParse(opusUpcase))) 155 156 assert.False(t, m.negotiatedVideo) 157 assert.True(t, m.negotiatedAudio) 158 159 opusCodec, _, err := m.getCodecByPayload(111) 160 assert.NoError(t, err) 161 assert.Equal(t, opusCodec.MimeType, "audio/OPUS") 162 }) 163 164 t.Run("Handle different fmtp", func(t *testing.T) { 165 const opusNoFmtp = `v=0 166 o=- 4596489990601351948 2 IN IP4 127.0.0.1 167 s=- 168 t=0 0 169 m=audio 9 UDP/TLS/RTP/SAVPF 111 170 a=rtpmap:111 opus/48000/2 171 ` 172 173 m := MediaEngine{} 174 assert.NoError(t, m.RegisterDefaultCodecs()) 175 assert.NoError(t, m.updateFromRemoteDescription(mustParse(opusNoFmtp))) 176 177 assert.False(t, m.negotiatedVideo) 178 assert.True(t, m.negotiatedAudio) 179 180 opusCodec, _, err := m.getCodecByPayload(111) 181 assert.NoError(t, err) 182 assert.Equal(t, opusCodec.MimeType, MimeTypeOpus) 183 }) 184 185 t.Run("Header Extensions", func(t *testing.T) { 186 const headerExtensions = `v=0 187 o=- 4596489990601351948 2 IN IP4 127.0.0.1 188 s=- 189 t=0 0 190 m=audio 9 UDP/TLS/RTP/SAVPF 111 191 a=extmap:7 urn:ietf:params:rtp-hdrext:sdes:mid 192 a=extmap:5 urn:ietf:params:rtp-hdrext:sdes:rtp-stream-id 193 a=rtpmap:111 opus/48000/2 194 ` 195 196 m := MediaEngine{} 197 assert.NoError(t, m.RegisterDefaultCodecs()) 198 assert.NoError(t, m.RegisterHeaderExtension(RTPHeaderExtensionCapability{URI: sdp.SDESMidURI}, RTPCodecTypeAudio)) 199 assert.NoError(t, m.updateFromRemoteDescription(mustParse(headerExtensions))) 200 201 assert.False(t, m.negotiatedVideo) 202 assert.True(t, m.negotiatedAudio) 203 204 absID, absAudioEnabled, absVideoEnabled := m.getHeaderExtensionID(RTPHeaderExtensionCapability{sdp.ABSSendTimeURI}) 205 assert.Equal(t, absID, 0) 206 assert.False(t, absAudioEnabled) 207 assert.False(t, absVideoEnabled) 208 209 midID, midAudioEnabled, midVideoEnabled := m.getHeaderExtensionID(RTPHeaderExtensionCapability{sdp.SDESMidURI}) 210 assert.Equal(t, midID, 7) 211 assert.True(t, midAudioEnabled) 212 assert.False(t, midVideoEnabled) 213 }) 214 215 t.Run("Different Header Extensions on same codec", func(t *testing.T) { 216 const headerExtensions = `v=0 217 o=- 4596489990601351948 2 IN IP4 127.0.0.1 218 s=- 219 t=0 0 220 m=audio 9 UDP/TLS/RTP/SAVPF 111 221 a=rtpmap:111 opus/48000/2 222 m=audio 9 UDP/TLS/RTP/SAVPF 111 223 a=extmap:7 urn:ietf:params:rtp-hdrext:sdes:mid 224 a=extmap:5 urn:ietf:params:rtp-hdrext:sdes:rtp-stream-id 225 a=rtpmap:111 opus/48000/2 226 ` 227 228 m := MediaEngine{} 229 assert.NoError(t, m.RegisterDefaultCodecs()) 230 assert.NoError(t, m.RegisterHeaderExtension(RTPHeaderExtensionCapability{URI: "urn:ietf:params:rtp-hdrext:sdes:mid"}, RTPCodecTypeAudio)) 231 assert.NoError(t, m.RegisterHeaderExtension(RTPHeaderExtensionCapability{URI: "urn:ietf:params:rtp-hdrext:sdes:rtp-stream-id"}, RTPCodecTypeAudio)) 232 assert.NoError(t, m.updateFromRemoteDescription(mustParse(headerExtensions))) 233 234 assert.False(t, m.negotiatedVideo) 235 assert.True(t, m.negotiatedAudio) 236 237 absID, absAudioEnabled, absVideoEnabled := m.getHeaderExtensionID(RTPHeaderExtensionCapability{sdp.ABSSendTimeURI}) 238 assert.Equal(t, absID, 0) 239 assert.False(t, absAudioEnabled) 240 assert.False(t, absVideoEnabled) 241 242 midID, midAudioEnabled, midVideoEnabled := m.getHeaderExtensionID(RTPHeaderExtensionCapability{sdp.SDESMidURI}) 243 assert.Equal(t, midID, 7) 244 assert.True(t, midAudioEnabled) 245 assert.False(t, midVideoEnabled) 246 }) 247 248 t.Run("Prefers exact codec matches", func(t *testing.T) { 249 const profileLevels = `v=0 250 o=- 4596489990601351948 2 IN IP4 127.0.0.1 251 s=- 252 t=0 0 253 m=video 60323 UDP/TLS/RTP/SAVPF 96 98 254 a=rtpmap:96 H264/90000 255 a=fmtp:96 level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=640c1f 256 a=rtpmap:98 H264/90000 257 a=fmtp:98 level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42e01f 258 ` 259 m := MediaEngine{} 260 assert.NoError(t, m.RegisterCodec(RTPCodecParameters{ 261 RTPCodecCapability: RTPCodecCapability{MimeTypeH264, 90000, 0, "level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42e01f", nil}, 262 PayloadType: 127, 263 }, RTPCodecTypeVideo)) 264 assert.NoError(t, m.updateFromRemoteDescription(mustParse(profileLevels))) 265 266 assert.True(t, m.negotiatedVideo) 267 assert.False(t, m.negotiatedAudio) 268 269 supportedH264, _, err := m.getCodecByPayload(98) 270 assert.NoError(t, err) 271 assert.Equal(t, supportedH264.MimeType, MimeTypeH264) 272 273 _, _, err = m.getCodecByPayload(96) 274 assert.Error(t, err) 275 }) 276 277 t.Run("Does not match when fmtpline is set and does not match", func(t *testing.T) { 278 const profileLevels = `v=0 279 o=- 4596489990601351948 2 IN IP4 127.0.0.1 280 s=- 281 t=0 0 282 m=video 60323 UDP/TLS/RTP/SAVPF 96 98 283 a=rtpmap:96 H264/90000 284 a=fmtp:96 level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=640c1f 285 ` 286 m := MediaEngine{} 287 assert.NoError(t, m.RegisterCodec(RTPCodecParameters{ 288 RTPCodecCapability: RTPCodecCapability{MimeTypeH264, 90000, 0, "level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42e01f", nil}, 289 PayloadType: 127, 290 }, RTPCodecTypeVideo)) 291 assert.Error(t, m.updateFromRemoteDescription(mustParse(profileLevels))) 292 293 _, _, err := m.getCodecByPayload(96) 294 assert.Error(t, err) 295 }) 296 297 t.Run("Matches when fmtpline is not set in offer, but exists in mediaengine", func(t *testing.T) { 298 const profileLevels = `v=0 299 o=- 4596489990601351948 2 IN IP4 127.0.0.1 300 s=- 301 t=0 0 302 m=video 60323 UDP/TLS/RTP/SAVPF 96 303 a=rtpmap:96 VP9/90000 304 ` 305 m := MediaEngine{} 306 assert.NoError(t, m.RegisterCodec(RTPCodecParameters{ 307 RTPCodecCapability: RTPCodecCapability{MimeTypeVP9, 90000, 0, "profile-id=0", nil}, 308 PayloadType: 98, 309 }, RTPCodecTypeVideo)) 310 assert.NoError(t, m.updateFromRemoteDescription(mustParse(profileLevels))) 311 312 assert.True(t, m.negotiatedVideo) 313 314 _, _, err := m.getCodecByPayload(96) 315 assert.NoError(t, err) 316 }) 317 318 t.Run("Matches when fmtpline exists in neither", func(t *testing.T) { 319 const profileLevels = `v=0 320 o=- 4596489990601351948 2 IN IP4 127.0.0.1 321 s=- 322 t=0 0 323 m=video 60323 UDP/TLS/RTP/SAVPF 96 324 a=rtpmap:96 VP8/90000 325 ` 326 m := MediaEngine{} 327 assert.NoError(t, m.RegisterCodec(RTPCodecParameters{ 328 RTPCodecCapability: RTPCodecCapability{MimeTypeVP8, 90000, 0, "", nil}, 329 PayloadType: 96, 330 }, RTPCodecTypeVideo)) 331 assert.NoError(t, m.updateFromRemoteDescription(mustParse(profileLevels))) 332 333 assert.True(t, m.negotiatedVideo) 334 335 _, _, err := m.getCodecByPayload(96) 336 assert.NoError(t, err) 337 }) 338 339 t.Run("Matches when rtx apt for exact match codec", func(t *testing.T) { 340 const profileLevels = `v=0 341 o=- 4596489990601351948 2 IN IP4 127.0.0.1 342 s=- 343 t=0 0 344 m=video 60323 UDP/TLS/RTP/SAVPF 94 95 106 107 108 109 96 97 345 a=rtpmap:94 VP8/90000 346 a=rtpmap:95 rtx/90000 347 a=fmtp:95 apt=94 348 a=rtpmap:106 H264/90000 349 a=fmtp:106 level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42001f 350 a=rtpmap:107 rtx/90000 351 a=fmtp:107 apt=106 352 a=rtpmap:108 H264/90000 353 a=fmtp:108 level-asymmetry-allowed=1;packetization-mode=0;profile-level-id=42001f 354 a=rtpmap:109 rtx/90000 355 a=fmtp:109 apt=108 356 a=rtpmap:96 VP9/90000 357 a=fmtp:96 profile-id=2 358 a=rtpmap:97 rtx/90000 359 a=fmtp:97 apt=96 360 ` 361 m := MediaEngine{} 362 assert.NoError(t, m.RegisterCodec(RTPCodecParameters{ 363 RTPCodecCapability: RTPCodecCapability{MimeTypeVP8, 90000, 0, "", nil}, 364 PayloadType: 96, 365 }, RTPCodecTypeVideo)) 366 assert.NoError(t, m.RegisterCodec(RTPCodecParameters{ 367 RTPCodecCapability: RTPCodecCapability{MimeTypeRTX, 90000, 0, "apt=96", nil}, 368 PayloadType: 97, 369 }, RTPCodecTypeVideo)) 370 assert.NoError(t, m.RegisterCodec(RTPCodecParameters{ 371 RTPCodecCapability: RTPCodecCapability{MimeTypeH264, 90000, 0, "level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42001f", nil}, 372 PayloadType: 102, 373 }, RTPCodecTypeVideo)) 374 assert.NoError(t, m.RegisterCodec(RTPCodecParameters{ 375 RTPCodecCapability: RTPCodecCapability{MimeTypeRTX, 90000, 0, "apt=102", nil}, 376 PayloadType: 103, 377 }, RTPCodecTypeVideo)) 378 assert.NoError(t, m.RegisterCodec(RTPCodecParameters{ 379 RTPCodecCapability: RTPCodecCapability{MimeTypeH264, 90000, 0, "level-asymmetry-allowed=1;packetization-mode=0;profile-level-id=42001f", nil}, 380 PayloadType: 104, 381 }, RTPCodecTypeVideo)) 382 assert.NoError(t, m.RegisterCodec(RTPCodecParameters{ 383 RTPCodecCapability: RTPCodecCapability{MimeTypeRTX, 90000, 0, "apt=104", nil}, 384 PayloadType: 105, 385 }, RTPCodecTypeVideo)) 386 assert.NoError(t, m.RegisterCodec(RTPCodecParameters{ 387 RTPCodecCapability: RTPCodecCapability{MimeTypeVP9, 90000, 0, "profile-id=2", nil}, 388 PayloadType: 98, 389 }, RTPCodecTypeVideo)) 390 assert.NoError(t, m.RegisterCodec(RTPCodecParameters{ 391 RTPCodecCapability: RTPCodecCapability{MimeTypeRTX, 90000, 0, "apt=98", nil}, 392 PayloadType: 99, 393 }, RTPCodecTypeVideo)) 394 assert.NoError(t, m.updateFromRemoteDescription(mustParse(profileLevels))) 395 396 assert.True(t, m.negotiatedVideo) 397 398 vp9Codec, _, err := m.getCodecByPayload(96) 399 assert.NoError(t, err) 400 assert.Equal(t, vp9Codec.MimeType, MimeTypeVP9) 401 vp9RTX, _, err := m.getCodecByPayload(97) 402 assert.NoError(t, err) 403 assert.Equal(t, vp9RTX.MimeType, MimeTypeRTX) 404 405 h264P1Codec, _, err := m.getCodecByPayload(106) 406 assert.NoError(t, err) 407 assert.Equal(t, h264P1Codec.MimeType, MimeTypeH264) 408 assert.Equal(t, h264P1Codec.SDPFmtpLine, "level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42001f") 409 h264P1RTX, _, err := m.getCodecByPayload(107) 410 assert.NoError(t, err) 411 assert.Equal(t, h264P1RTX.MimeType, MimeTypeRTX) 412 assert.Equal(t, h264P1RTX.SDPFmtpLine, "apt=106") 413 414 h264P0Codec, _, err := m.getCodecByPayload(108) 415 assert.NoError(t, err) 416 assert.Equal(t, h264P0Codec.MimeType, MimeTypeH264) 417 assert.Equal(t, h264P0Codec.SDPFmtpLine, "level-asymmetry-allowed=1;packetization-mode=0;profile-level-id=42001f") 418 h264P0RTX, _, err := m.getCodecByPayload(109) 419 assert.NoError(t, err) 420 assert.Equal(t, h264P0RTX.MimeType, MimeTypeRTX) 421 assert.Equal(t, h264P0RTX.SDPFmtpLine, "apt=108") 422 }) 423 424 t.Run("Matches when rtx apt for partial match codec", func(t *testing.T) { 425 const profileLevels = `v=0 426 o=- 4596489990601351948 2 IN IP4 127.0.0.1 427 s=- 428 t=0 0 429 m=video 60323 UDP/TLS/RTP/SAVPF 94 96 97 430 a=rtpmap:94 VP8/90000 431 a=rtpmap:96 VP9/90000 432 a=fmtp:96 profile-id=2 433 a=rtpmap:97 rtx/90000 434 a=fmtp:97 apt=96 435 ` 436 m := MediaEngine{} 437 assert.NoError(t, m.RegisterCodec(RTPCodecParameters{ 438 RTPCodecCapability: RTPCodecCapability{MimeTypeVP8, 90000, 0, "", nil}, 439 PayloadType: 94, 440 }, RTPCodecTypeVideo)) 441 assert.NoError(t, m.RegisterCodec(RTPCodecParameters{ 442 RTPCodecCapability: RTPCodecCapability{MimeTypeVP9, 90000, 0, "profile-id=1", nil}, 443 PayloadType: 96, 444 }, RTPCodecTypeVideo)) 445 assert.NoError(t, m.RegisterCodec(RTPCodecParameters{ 446 RTPCodecCapability: RTPCodecCapability{MimeTypeRTX, 90000, 0, "apt=96", nil}, 447 PayloadType: 97, 448 }, RTPCodecTypeVideo)) 449 assert.NoError(t, m.updateFromRemoteDescription(mustParse(profileLevels))) 450 451 assert.True(t, m.negotiatedVideo) 452 453 _, _, err := m.getCodecByPayload(97) 454 assert.ErrorIs(t, err, ErrCodecNotFound) 455 }) 456 } 457 458 func TestMediaEngineHeaderExtensionDirection(t *testing.T) { 459 report := test.CheckRoutines(t) 460 defer report() 461 462 registerCodec := func(m *MediaEngine) { 463 assert.NoError(t, m.RegisterCodec( 464 RTPCodecParameters{ 465 RTPCodecCapability: RTPCodecCapability{MimeTypeOpus, 48000, 0, "", nil}, 466 PayloadType: 111, 467 }, RTPCodecTypeAudio)) 468 } 469 470 t.Run("No Direction", func(t *testing.T) { 471 m := &MediaEngine{} 472 registerCodec(m) 473 assert.NoError(t, m.RegisterHeaderExtension(RTPHeaderExtensionCapability{"pion-header-test"}, RTPCodecTypeAudio)) 474 475 params := m.getRTPParametersByKind(RTPCodecTypeAudio, []RTPTransceiverDirection{RTPTransceiverDirectionRecvonly}) 476 477 assert.Equal(t, 1, len(params.HeaderExtensions)) 478 }) 479 480 t.Run("Same Direction", func(t *testing.T) { 481 m := &MediaEngine{} 482 registerCodec(m) 483 assert.NoError(t, m.RegisterHeaderExtension(RTPHeaderExtensionCapability{"pion-header-test"}, RTPCodecTypeAudio, RTPTransceiverDirectionRecvonly)) 484 485 params := m.getRTPParametersByKind(RTPCodecTypeAudio, []RTPTransceiverDirection{RTPTransceiverDirectionRecvonly}) 486 487 assert.Equal(t, 1, len(params.HeaderExtensions)) 488 }) 489 490 t.Run("Different Direction", func(t *testing.T) { 491 m := &MediaEngine{} 492 registerCodec(m) 493 assert.NoError(t, m.RegisterHeaderExtension(RTPHeaderExtensionCapability{"pion-header-test"}, RTPCodecTypeAudio, RTPTransceiverDirectionSendonly)) 494 495 params := m.getRTPParametersByKind(RTPCodecTypeAudio, []RTPTransceiverDirection{RTPTransceiverDirectionRecvonly}) 496 497 assert.Equal(t, 0, len(params.HeaderExtensions)) 498 }) 499 500 t.Run("Invalid Direction", func(t *testing.T) { 501 m := &MediaEngine{} 502 registerCodec(m) 503 504 assert.ErrorIs(t, m.RegisterHeaderExtension(RTPHeaderExtensionCapability{"pion-header-test"}, RTPCodecTypeAudio, RTPTransceiverDirectionSendrecv), ErrRegisterHeaderExtensionInvalidDirection) 505 assert.ErrorIs(t, m.RegisterHeaderExtension(RTPHeaderExtensionCapability{"pion-header-test"}, RTPCodecTypeAudio, RTPTransceiverDirectionInactive), ErrRegisterHeaderExtensionInvalidDirection) 506 assert.ErrorIs(t, m.RegisterHeaderExtension(RTPHeaderExtensionCapability{"pion-header-test"}, RTPCodecTypeAudio, RTPTransceiverDirection(0)), ErrRegisterHeaderExtensionInvalidDirection) 507 }) 508 509 t.Run("Unique extmapid with different codec", func(t *testing.T) { 510 m := &MediaEngine{} 511 registerCodec(m) 512 assert.NoError(t, m.RegisterHeaderExtension(RTPHeaderExtensionCapability{"pion-header-test"}, RTPCodecTypeAudio)) 513 assert.NoError(t, m.RegisterHeaderExtension(RTPHeaderExtensionCapability{"pion-header-test2"}, RTPCodecTypeVideo)) 514 515 audio := m.getRTPParametersByKind(RTPCodecTypeAudio, []RTPTransceiverDirection{RTPTransceiverDirectionRecvonly}) 516 video := m.getRTPParametersByKind(RTPCodecTypeVideo, []RTPTransceiverDirection{RTPTransceiverDirectionRecvonly}) 517 518 assert.Equal(t, 1, len(audio.HeaderExtensions)) 519 assert.Equal(t, 1, len(video.HeaderExtensions)) 520 assert.NotEqual(t, audio.HeaderExtensions[0].ID, video.HeaderExtensions[0].ID) 521 }) 522 } 523 524 // If a user attempts to register a codec twice we should just discard duplicate calls 525 func TestMediaEngineDoubleRegister(t *testing.T) { 526 m := MediaEngine{} 527 528 assert.NoError(t, m.RegisterCodec( 529 RTPCodecParameters{ 530 RTPCodecCapability: RTPCodecCapability{MimeTypeOpus, 48000, 0, "", nil}, 531 PayloadType: 111, 532 }, RTPCodecTypeAudio)) 533 534 assert.NoError(t, m.RegisterCodec( 535 RTPCodecParameters{ 536 RTPCodecCapability: RTPCodecCapability{MimeTypeOpus, 48000, 0, "", nil}, 537 PayloadType: 111, 538 }, RTPCodecTypeAudio)) 539 540 assert.Equal(t, len(m.audioCodecs), 1) 541 } 542 543 // The cloned MediaEngine instance should be able to update negotiated header extensions. 544 func TestUpdateHeaderExtenstionToClonedMediaEngine(t *testing.T) { 545 src := MediaEngine{} 546 547 assert.NoError(t, src.RegisterCodec( 548 RTPCodecParameters{ 549 RTPCodecCapability: RTPCodecCapability{MimeTypeOpus, 48000, 0, "", nil}, 550 PayloadType: 111, 551 }, RTPCodecTypeAudio)) 552 553 assert.NoError(t, src.RegisterHeaderExtension(RTPHeaderExtensionCapability{"test-extension"}, RTPCodecTypeAudio)) 554 555 validate := func(m *MediaEngine) { 556 assert.NoError(t, m.updateHeaderExtension(2, "test-extension", RTPCodecTypeAudio)) 557 558 id, audioNegotiated, videoNegotiated := m.getHeaderExtensionID(RTPHeaderExtensionCapability{URI: "test-extension"}) 559 assert.Equal(t, 2, id) 560 assert.True(t, audioNegotiated) 561 assert.False(t, videoNegotiated) 562 } 563 564 validate(&src) 565 validate(src.copy()) 566 } 567 568 func TestExtensionIdCollision(t *testing.T) { 569 mustParse := func(raw string) sdp.SessionDescription { 570 s := sdp.SessionDescription{} 571 assert.NoError(t, s.Unmarshal([]byte(raw))) 572 return s 573 } 574 sdpSnippet := `v=0 575 o=- 4596489990601351948 2 IN IP4 127.0.0.1 576 s=- 577 t=0 0 578 m=audio 9 UDP/TLS/RTP/SAVPF 111 579 a=extmap:2 urn:ietf:params:rtp-hdrext:sdes:mid 580 a=extmap:1 urn:ietf:params:rtp-hdrext:ssrc-audio-level 581 a=extmap:5 urn:ietf:params:rtp-hdrext:sdes:rtp-stream-id 582 a=rtpmap:111 opus/48000/2 583 ` 584 585 m := MediaEngine{} 586 assert.NoError(t, m.RegisterDefaultCodecs()) 587 588 assert.NoError(t, m.RegisterHeaderExtension(RTPHeaderExtensionCapability{sdp.SDESMidURI}, RTPCodecTypeVideo)) 589 assert.NoError(t, m.RegisterHeaderExtension(RTPHeaderExtensionCapability{"urn:3gpp:video-orientation"}, RTPCodecTypeVideo)) 590 591 assert.NoError(t, m.RegisterHeaderExtension(RTPHeaderExtensionCapability{sdp.SDESMidURI}, RTPCodecTypeAudio)) 592 assert.NoError(t, m.RegisterHeaderExtension(RTPHeaderExtensionCapability{sdp.AudioLevelURI}, RTPCodecTypeAudio)) 593 594 assert.NoError(t, m.updateFromRemoteDescription(mustParse(sdpSnippet))) 595 596 assert.True(t, m.negotiatedAudio) 597 assert.False(t, m.negotiatedVideo) 598 599 id, audioNegotiated, videoNegotiated := m.getHeaderExtensionID(RTPHeaderExtensionCapability{sdp.ABSSendTimeURI}) 600 assert.Equal(t, id, 0) 601 assert.False(t, audioNegotiated) 602 assert.False(t, videoNegotiated) 603 604 id, audioNegotiated, videoNegotiated = m.getHeaderExtensionID(RTPHeaderExtensionCapability{sdp.SDESMidURI}) 605 assert.Equal(t, id, 2) 606 assert.True(t, audioNegotiated) 607 assert.False(t, videoNegotiated) 608 609 id, audioNegotiated, videoNegotiated = m.getHeaderExtensionID(RTPHeaderExtensionCapability{sdp.AudioLevelURI}) 610 assert.Equal(t, id, 1) 611 assert.True(t, audioNegotiated) 612 assert.False(t, videoNegotiated) 613 614 params := m.getRTPParametersByKind(RTPCodecTypeVideo, []RTPTransceiverDirection{RTPTransceiverDirectionSendonly}) 615 extensions := params.HeaderExtensions 616 617 assert.Equal(t, 2, len(extensions)) 618 619 midIndex := -1 620 if extensions[0].URI == sdp.SDESMidURI { 621 midIndex = 0 622 } else if extensions[1].URI == sdp.SDESMidURI { 623 midIndex = 1 624 } 625 626 voIndex := -1 627 if extensions[0].URI == "urn:3gpp:video-orientation" { 628 voIndex = 0 629 } else if extensions[1].URI == "urn:3gpp:video-orientation" { 630 voIndex = 1 631 } 632 633 assert.NotEqual(t, midIndex, -1) 634 assert.NotEqual(t, voIndex, -1) 635 636 assert.Equal(t, 2, extensions[midIndex].ID) 637 assert.NotEqual(t, 1, extensions[voIndex].ID) 638 assert.NotEqual(t, 2, extensions[voIndex].ID) 639 assert.NotEqual(t, 5, extensions[voIndex].ID) 640 } 641 642 func TestCaseInsensitiveMimeType(t *testing.T) { 643 const offerSdp = ` 644 v=0 645 o=- 8448668841136641781 4 IN IP4 127.0.0.1 646 s=- 647 t=0 0 648 a=group:BUNDLE 0 1 2 649 a=extmap-allow-mixed 650 a=msid-semantic: WMS 4beea6b0-cf95-449c-a1ec-78e16b247426 651 m=video 9 UDP/TLS/RTP/SAVPF 96 127 652 c=IN IP4 0.0.0.0 653 a=rtcp:9 IN IP4 0.0.0.0 654 a=ice-ufrag:1/MvHwjAyVf27aLu 655 a=ice-pwd:3dBU7cFOBl120v33cynDvN1E 656 a=ice-options:google-ice 657 a=fingerprint:sha-256 75:74:5A:A6:A4:E5:52:F4:A7:67:4C:01:C7:EE:91:3F:21:3D:A2:E3:53:7B:6F:30:86:F2:30:AA:65:FB:04:24 658 a=setup:actpass 659 a=mid:1 660 a=sendonly 661 a=rtpmap:96 VP8/90000 662 a=rtcp-fb:96 goog-remb 663 a=rtcp-fb:96 transport-cc 664 a=rtcp-fb:96 ccm fir 665 a=rtcp-fb:96 nack 666 a=rtcp-fb:96 nack pli 667 a=rtpmap:127 H264/90000 668 a=rtcp-fb:127 goog-remb 669 a=rtcp-fb:127 transport-cc 670 a=rtcp-fb:127 ccm fir 671 a=rtcp-fb:127 nack 672 a=rtcp-fb:127 nack pli 673 a=fmtp:127 level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42001f 674 675 ` 676 677 for _, mimeTypeVp8 := range []string{ 678 "video/vp8", 679 "video/VP8", 680 } { 681 t.Run(fmt.Sprintf("MimeType: %s", mimeTypeVp8), func(t *testing.T) { 682 me := &MediaEngine{} 683 feedback := []RTCPFeedback{ 684 {Type: TypeRTCPFBTransportCC}, 685 {Type: TypeRTCPFBCCM, Parameter: "fir"}, 686 {Type: TypeRTCPFBNACK}, 687 {Type: TypeRTCPFBNACK, Parameter: "pli"}, 688 } 689 690 for _, codec := range []RTPCodecParameters{ 691 { 692 RTPCodecCapability: RTPCodecCapability{MimeType: mimeTypeVp8, ClockRate: 90000, RTCPFeedback: feedback}, 693 PayloadType: 96, 694 }, 695 { 696 RTPCodecCapability: RTPCodecCapability{MimeType: "video/h264", ClockRate: 90000, SDPFmtpLine: "level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42001f", RTCPFeedback: feedback}, 697 PayloadType: 127, 698 }, 699 } { 700 assert.NoError(t, me.RegisterCodec(codec, RTPCodecTypeVideo)) 701 } 702 703 api := NewAPI(WithMediaEngine(me)) 704 pc, err := api.NewPeerConnection(Configuration{ 705 SDPSemantics: SDPSemanticsUnifiedPlan, 706 }) 707 assert.NoError(t, err) 708 709 offer := SessionDescription{ 710 Type: SDPTypeOffer, 711 SDP: offerSdp, 712 } 713 714 assert.NoError(t, pc.SetRemoteDescription(offer)) 715 answer, err := pc.CreateAnswer(nil) 716 assert.NoError(t, err) 717 assert.NotNil(t, answer) 718 assert.NoError(t, pc.SetLocalDescription(answer)) 719 assert.True(t, strings.Contains(answer.SDP, "VP8") || strings.Contains(answer.SDP, "vp8")) 720 721 assert.NoError(t, pc.Close()) 722 }) 723 } 724 }