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  }