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  }