sdpsemantics_test.go 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369
  1. // SPDX-FileCopyrightText: 2023 The Pion community <https://pion.ly>
  2. // SPDX-License-Identifier: MIT
  3. //go:build !js
  4. // +build !js
  5. package webrtc
  6. import (
  7. "encoding/json"
  8. "errors"
  9. "strings"
  10. "testing"
  11. "time"
  12. "github.com/pion/sdp/v3"
  13. "github.com/pion/transport/v2/test"
  14. "github.com/stretchr/testify/assert"
  15. )
  16. func TestSDPSemantics_String(t *testing.T) {
  17. testCases := []struct {
  18. value SDPSemantics
  19. expectedString string
  20. }{
  21. {SDPSemanticsUnifiedPlanWithFallback, "unified-plan-with-fallback"},
  22. {SDPSemanticsPlanB, "plan-b"},
  23. {SDPSemanticsUnifiedPlan, "unified-plan"},
  24. }
  25. assert.Equal(t,
  26. unknownStr,
  27. SDPSemantics(42).String(),
  28. )
  29. for i, testCase := range testCases {
  30. assert.Equal(t,
  31. testCase.expectedString,
  32. testCase.value.String(),
  33. "testCase: %d %v", i, testCase,
  34. )
  35. assert.Equal(t,
  36. testCase.value,
  37. newSDPSemantics(testCase.expectedString),
  38. "testCase: %d %v", i, testCase,
  39. )
  40. }
  41. }
  42. func TestSDPSemantics_JSON(t *testing.T) {
  43. testCases := []struct {
  44. value SDPSemantics
  45. JSON []byte
  46. }{
  47. {SDPSemanticsUnifiedPlanWithFallback, []byte("\"unified-plan-with-fallback\"")},
  48. {SDPSemanticsPlanB, []byte("\"plan-b\"")},
  49. {SDPSemanticsUnifiedPlan, []byte("\"unified-plan\"")},
  50. }
  51. for i, testCase := range testCases {
  52. res, err := json.Marshal(testCase.value)
  53. assert.NoError(t, err)
  54. assert.Equal(t,
  55. testCase.JSON,
  56. res,
  57. "testCase: %d %v", i, testCase,
  58. )
  59. var v SDPSemantics
  60. err = json.Unmarshal(testCase.JSON, &v)
  61. assert.NoError(t, err)
  62. assert.Equal(t, v, testCase.value)
  63. }
  64. }
  65. // The following tests are for non-standard SDP semantics
  66. // (i.e. not unified-unified)
  67. func getMdNames(sdp *sdp.SessionDescription) []string {
  68. mdNames := make([]string, 0, len(sdp.MediaDescriptions))
  69. for _, media := range sdp.MediaDescriptions {
  70. mdNames = append(mdNames, media.MediaName.Media)
  71. }
  72. return mdNames
  73. }
  74. func extractSsrcList(md *sdp.MediaDescription) []string {
  75. ssrcMap := map[string]struct{}{}
  76. for _, attr := range md.Attributes {
  77. if attr.Key == sdp.AttrKeySSRC {
  78. ssrc := strings.Fields(attr.Value)[0]
  79. ssrcMap[ssrc] = struct{}{}
  80. }
  81. }
  82. ssrcList := make([]string, 0, len(ssrcMap))
  83. for ssrc := range ssrcMap {
  84. ssrcList = append(ssrcList, ssrc)
  85. }
  86. return ssrcList
  87. }
  88. func TestSDPSemantics_PlanBOfferTransceivers(t *testing.T) {
  89. report := test.CheckRoutines(t)
  90. defer report()
  91. lim := test.TimeOut(time.Second * 30)
  92. defer lim.Stop()
  93. opc, err := NewPeerConnection(Configuration{
  94. SDPSemantics: SDPSemanticsPlanB,
  95. })
  96. assert.NoError(t, err)
  97. _, err = opc.AddTransceiverFromKind(RTPCodecTypeVideo, RTPTransceiverInit{
  98. Direction: RTPTransceiverDirectionSendrecv,
  99. })
  100. assert.NoError(t, err)
  101. _, err = opc.AddTransceiverFromKind(RTPCodecTypeVideo, RTPTransceiverInit{
  102. Direction: RTPTransceiverDirectionSendrecv,
  103. })
  104. assert.NoError(t, err)
  105. _, err = opc.AddTransceiverFromKind(RTPCodecTypeAudio, RTPTransceiverInit{
  106. Direction: RTPTransceiverDirectionSendrecv,
  107. })
  108. assert.NoError(t, err)
  109. _, err = opc.AddTransceiverFromKind(RTPCodecTypeAudio, RTPTransceiverInit{
  110. Direction: RTPTransceiverDirectionSendrecv,
  111. })
  112. assert.NoError(t, err)
  113. offer, err := opc.CreateOffer(nil)
  114. assert.NoError(t, err)
  115. mdNames := getMdNames(offer.parsed)
  116. assert.ObjectsAreEqual(mdNames, []string{"video", "audio", "data"})
  117. // Verify that each section has 2 SSRCs (one for each transceiver)
  118. for _, section := range []string{"video", "audio"} {
  119. for _, media := range offer.parsed.MediaDescriptions {
  120. if media.MediaName.Media == section {
  121. assert.Len(t, extractSsrcList(media), 2)
  122. }
  123. }
  124. }
  125. apc, err := NewPeerConnection(Configuration{
  126. SDPSemantics: SDPSemanticsPlanB,
  127. })
  128. assert.NoError(t, err)
  129. assert.NoError(t, apc.SetRemoteDescription(offer))
  130. answer, err := apc.CreateAnswer(nil)
  131. assert.NoError(t, err)
  132. mdNames = getMdNames(answer.parsed)
  133. assert.ObjectsAreEqual(mdNames, []string{"video", "audio", "data"})
  134. closePairNow(t, apc, opc)
  135. }
  136. func TestSDPSemantics_PlanBAnswerSenders(t *testing.T) {
  137. report := test.CheckRoutines(t)
  138. defer report()
  139. lim := test.TimeOut(time.Second * 30)
  140. defer lim.Stop()
  141. opc, err := NewPeerConnection(Configuration{
  142. SDPSemantics: SDPSemanticsPlanB,
  143. })
  144. assert.NoError(t, err)
  145. _, err = opc.AddTransceiverFromKind(RTPCodecTypeVideo, RTPTransceiverInit{
  146. Direction: RTPTransceiverDirectionRecvonly,
  147. })
  148. assert.NoError(t, err)
  149. _, err = opc.AddTransceiverFromKind(RTPCodecTypeAudio, RTPTransceiverInit{
  150. Direction: RTPTransceiverDirectionRecvonly,
  151. })
  152. assert.NoError(t, err)
  153. offer, err := opc.CreateOffer(nil)
  154. assert.NoError(t, err)
  155. assert.ObjectsAreEqual(getMdNames(offer.parsed), []string{"video", "audio", "data"})
  156. apc, err := NewPeerConnection(Configuration{
  157. SDPSemantics: SDPSemanticsPlanB,
  158. })
  159. assert.NoError(t, err)
  160. video1, err := NewTrackLocalStaticSample(RTPCodecCapability{MimeType: MimeTypeH264, SDPFmtpLine: "level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42001f"}, "1", "1")
  161. assert.NoError(t, err)
  162. _, err = apc.AddTrack(video1)
  163. assert.NoError(t, err)
  164. video2, err := NewTrackLocalStaticSample(RTPCodecCapability{MimeType: MimeTypeH264, SDPFmtpLine: "level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42001f"}, "2", "2")
  165. assert.NoError(t, err)
  166. _, err = apc.AddTrack(video2)
  167. assert.NoError(t, err)
  168. audio1, err := NewTrackLocalStaticSample(RTPCodecCapability{MimeType: MimeTypeOpus}, "3", "3")
  169. assert.NoError(t, err)
  170. _, err = apc.AddTrack(audio1)
  171. assert.NoError(t, err)
  172. audio2, err := NewTrackLocalStaticSample(RTPCodecCapability{MimeType: MimeTypeOpus}, "4", "4")
  173. assert.NoError(t, err)
  174. _, err = apc.AddTrack(audio2)
  175. assert.NoError(t, err)
  176. assert.NoError(t, apc.SetRemoteDescription(offer))
  177. answer, err := apc.CreateAnswer(nil)
  178. assert.NoError(t, err)
  179. assert.ObjectsAreEqual(getMdNames(answer.parsed), []string{"video", "audio", "data"})
  180. // Verify that each section has 2 SSRCs (one for each sender)
  181. for _, section := range []string{"video", "audio"} {
  182. for _, media := range answer.parsed.MediaDescriptions {
  183. if media.MediaName.Media == section {
  184. assert.Lenf(t, extractSsrcList(media), 2, "%q should have 2 SSRCs in Plan-B mode", section)
  185. }
  186. }
  187. }
  188. closePairNow(t, apc, opc)
  189. }
  190. func TestSDPSemantics_UnifiedPlanWithFallback(t *testing.T) {
  191. report := test.CheckRoutines(t)
  192. defer report()
  193. lim := test.TimeOut(time.Second * 30)
  194. defer lim.Stop()
  195. opc, err := NewPeerConnection(Configuration{
  196. SDPSemantics: SDPSemanticsPlanB,
  197. })
  198. assert.NoError(t, err)
  199. _, err = opc.AddTransceiverFromKind(RTPCodecTypeVideo, RTPTransceiverInit{
  200. Direction: RTPTransceiverDirectionRecvonly,
  201. })
  202. assert.NoError(t, err)
  203. _, err = opc.AddTransceiverFromKind(RTPCodecTypeAudio, RTPTransceiverInit{
  204. Direction: RTPTransceiverDirectionRecvonly,
  205. })
  206. assert.NoError(t, err)
  207. offer, err := opc.CreateOffer(nil)
  208. assert.NoError(t, err)
  209. assert.ObjectsAreEqual(getMdNames(offer.parsed), []string{"video", "audio", "data"})
  210. apc, err := NewPeerConnection(Configuration{
  211. SDPSemantics: SDPSemanticsUnifiedPlanWithFallback,
  212. })
  213. assert.NoError(t, err)
  214. video1, err := NewTrackLocalStaticSample(RTPCodecCapability{MimeType: MimeTypeH264, SDPFmtpLine: "level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42001f"}, "1", "1")
  215. assert.NoError(t, err)
  216. _, err = apc.AddTrack(video1)
  217. assert.NoError(t, err)
  218. video2, err := NewTrackLocalStaticSample(RTPCodecCapability{MimeType: MimeTypeH264, SDPFmtpLine: "level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42001f"}, "2", "2")
  219. assert.NoError(t, err)
  220. _, err = apc.AddTrack(video2)
  221. assert.NoError(t, err)
  222. audio1, err := NewTrackLocalStaticSample(RTPCodecCapability{MimeType: MimeTypeOpus}, "3", "3")
  223. assert.NoError(t, err)
  224. _, err = apc.AddTrack(audio1)
  225. assert.NoError(t, err)
  226. audio2, err := NewTrackLocalStaticSample(RTPCodecCapability{MimeType: MimeTypeOpus}, "4", "4")
  227. assert.NoError(t, err)
  228. _, err = apc.AddTrack(audio2)
  229. assert.NoError(t, err)
  230. assert.NoError(t, apc.SetRemoteDescription(offer))
  231. answer, err := apc.CreateAnswer(nil)
  232. assert.NoError(t, err)
  233. assert.ObjectsAreEqual(getMdNames(answer.parsed), []string{"video", "audio", "data"})
  234. extractSsrcList := func(md *sdp.MediaDescription) []string {
  235. ssrcMap := map[string]struct{}{}
  236. for _, attr := range md.Attributes {
  237. if attr.Key == sdp.AttrKeySSRC {
  238. ssrc := strings.Fields(attr.Value)[0]
  239. ssrcMap[ssrc] = struct{}{}
  240. }
  241. }
  242. ssrcList := make([]string, 0, len(ssrcMap))
  243. for ssrc := range ssrcMap {
  244. ssrcList = append(ssrcList, ssrc)
  245. }
  246. return ssrcList
  247. }
  248. // Verify that each section has 2 SSRCs (one for each sender)
  249. for _, section := range []string{"video", "audio"} {
  250. for _, media := range answer.parsed.MediaDescriptions {
  251. if media.MediaName.Media == section {
  252. assert.Lenf(t, extractSsrcList(media), 2, "%q should have 2 SSRCs in Plan-B fallback mode", section)
  253. }
  254. }
  255. }
  256. closePairNow(t, apc, opc)
  257. }
  258. // Assert that we can catch Remote SessionDescription that don't match our Semantics
  259. func TestSDPSemantics_SetRemoteDescription_Mismatch(t *testing.T) {
  260. 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"
  261. 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"
  262. report := test.CheckRoutines(t)
  263. defer report()
  264. lim := test.TimeOut(time.Second * 30)
  265. defer lim.Stop()
  266. t.Run("PlanB", func(t *testing.T) {
  267. pc, err := NewPeerConnection(Configuration{
  268. SDPSemantics: SDPSemanticsUnifiedPlan,
  269. })
  270. assert.NoError(t, err)
  271. err = pc.SetRemoteDescription(SessionDescription{SDP: planBOffer, Type: SDPTypeOffer})
  272. assert.NoError(t, err)
  273. _, err = pc.CreateAnswer(nil)
  274. assert.True(t, errors.Is(err, ErrIncorrectSDPSemantics))
  275. assert.NoError(t, pc.Close())
  276. })
  277. t.Run("UnifiedPlan", func(t *testing.T) {
  278. pc, err := NewPeerConnection(Configuration{
  279. SDPSemantics: SDPSemanticsPlanB,
  280. })
  281. assert.NoError(t, err)
  282. err = pc.SetRemoteDescription(SessionDescription{SDP: unifiedPlanOffer, Type: SDPTypeOffer})
  283. assert.NoError(t, err)
  284. _, err = pc.CreateAnswer(nil)
  285. assert.True(t, errors.Is(err, ErrIncorrectSDPSemantics))
  286. assert.NoError(t, pc.Close())
  287. })
  288. }