Repository: bluenviron/gortsplib
Pull request #371 (2023-08-22 15:12:10)

allow publishers to set the title of the stream
Fixes https://github.com/bluenviron/mediamtx/issues/979

codecov[bot]: ## [Codecov](https://app.codecov.io/gh/bluenviron/gortsplib/pull/371?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=bluenviron) Report
> Merging [#371](https://app.codecov.io/gh/bluenviron/gortsplib/pull/371?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=bluenviron) (87f57f5) into [v4](https://app.codecov.io/gh/bluenviron/gortsplib/commit/cdbecb1f5dd23d92fdea978a6adb5e24aebc1105?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=bluenviron) (cdbecb1) will **decrease** coverage by `0.02%`.
> The diff coverage is `66.66%`.

```diff
@@            Coverage Diff             @@
##               v4     #371      +/-   ##
==========================================
- Coverage   84.77%   84.75%   -0.02%     
==========================================
  Files         101      101              
  Lines       10193    10201       +8     
==========================================
+ Hits         8641     8646       +5     
- Misses       1193     1195       +2     
- Partials      359      360       +1     
```


| [Files Changed](https://app.codecov.io/gh/bluenviron/gortsplib/pull/371?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=bluenviron) | Coverage Δ | |
|---|---|---|
| [pkg/description/session.go](https://app.codecov.io/gh/bluenviron/gortsplib/pull/371?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=bluenviron#diff-cGtnL2Rlc2NyaXB0aW9uL3Nlc3Npb24uZ28=) | `87.71% <66.66%> (-4.12%)` | :arrow_down: |

... and [2 files with indirect coverage changes](https://app.codecov.io/gh/bluenviron/gortsplib/pull/371/indirect-changes?src=pr&el=tree-more&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=bluenviron)

:mega: We’re building smart automated test selection to slash your CI/CD build times. [Learn more](https://about.codecov.io/iterative-testing/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=bluenviron)


Patch:
diff --git a/client.go b/client.go
index 7158dfd8..429c573d 100644
--- a/client.go
+++ b/client.go
@@ -91,8 +91,8 @@ func findBaseURL(sd *sdp.SessionDescription, res *base.Response, u *url.URL) (*u
 	return u, nil
 }
 
-func resetMediaControls(ms []*description.Media) {
-	for i, media := range ms {
+func prepareForAnnounce(desc *description.Session) {
+	for i, media := range desc.Medias {
 		media.Control = "trackID=" + strconv.FormatInt(int64(i), 10)
 	}
 }
@@ -1086,7 +1086,7 @@ func (c *Client) doAnnounce(u *url.URL, desc *description.Session) (*base.Respon
 		return nil, err
 	}
 
-	resetMediaControls(desc.Medias)
+	prepareForAnnounce(desc)
 
 	byts, err := desc.Marshal(false)
 	if err != nil {
diff --git a/pkg/description/session.go b/pkg/description/session.go
index c7f7a7ce..b3525cc1 100644
--- a/pkg/description/session.go
+++ b/pkg/description/session.go
@@ -9,11 +9,14 @@ import (
 	"github.com/bluenviron/gortsplib/v4/pkg/url"
 )
 
-// Session is the description of a RTSP session.
+// Session is the description of a RTSP stream.
 type Session struct {
 	// base URL of the stream (read only).
 	BaseURL *url.URL
 
+	// title of the stream (optional).
+	Title string
+
 	// available media streams.
 	Medias []*Media
 }
@@ -32,6 +35,11 @@ func (d *Session) FindFormat(forma interface{}) *Media {
 
 // Unmarshal decodes the description from SDP.
 func (d *Session) Unmarshal(ssd *sdp.SessionDescription) error {
+	d.Title = string(ssd.SessionName)
+	if d.Title == " " {
+		d.Title = ""
+	}
+
 	d.Medias = make([]*Media, len(ssd.MediaDescriptions))
 
 	for i, md := range ssd.MediaDescriptions {
@@ -48,6 +56,15 @@ func (d *Session) Unmarshal(ssd *sdp.SessionDescription) error {
 
 // Marshal encodes the description in SDP.
 func (d Session) Marshal(multicast bool) ([]byte, error) {
+	var sessionName psdp.SessionName
+	if d.Title != "" {
+		sessionName = psdp.SessionName(d.Title)
+	} else {
+		// RFC 4566: If a session has no meaningful name, the
+		// value "s= " SHOULD be used (i.e., a single space as the session name).
+		sessionName = psdp.SessionName(" ")
+	}
+
 	var address string
 	if multicast {
 		address = "224.1.0.0"
@@ -56,7 +73,7 @@ func (d Session) Marshal(multicast bool) ([]byte, error) {
 	}
 
 	sout := &sdp.SessionDescription{
-		SessionName: psdp.SessionName("Session"),
+		SessionName: sessionName,
 		Origin: psdp.Origin{
 			Username:       "-",
 			NetworkType:    "IN",
diff --git a/pkg/sdp/sdp.go b/pkg/sdp/sdp.go
index 63ca2b54..d74ad2d4 100644
--- a/pkg/sdp/sdp.go
+++ b/pkg/sdp/sdp.go
@@ -715,5 +715,9 @@ func (s *SessionDescription) Unmarshal(byts []byte) error {
 		}
 	}
 
+	if s.SessionName == "" {
+		return fmt.Errorf("session name not present")
+	}
+
 	return nil
 }
diff --git a/server_conn.go b/server_conn.go
index 3ed1dc87..64425d29 100644
--- a/server_conn.go
+++ b/server_conn.go
@@ -24,8 +24,9 @@ func getSessionID(header base.Header) string {
 	return ""
 }
 
-func streamDescCopyForServer(d *description.Session, contentBase *url.URL) *description.Session {
+func serverSideDescription(d *description.Session, contentBase *url.URL) *description.Session {
 	out := &description.Session{
+		Title:  d.Title,
 		Medias: make([]*description.Media, len(d.Medias)),
 	}
 
@@ -293,7 +294,7 @@ func (sc *ServerConn) handleRequestInner(req *base.Request) (*base.Response, err
 				}
 
 				if stream != nil {
-					byts, _ := streamDescCopyForServer(stream.desc, req.URL).Marshal(multicast)
+					byts, _ := serverSideDescription(stream.desc, req.URL).Marshal(multicast)
 					res.Body = byts
 				}
 			}


Test patch:
diff --git a/client_play_test.go b/client_play_test.go
index 2a4fdb57..759b91c6 100644
--- a/client_play_test.go
+++ b/client_play_test.go
@@ -12,7 +12,6 @@ import (
 
 	"github.com/pion/rtcp"
 	"github.com/pion/rtp"
-	psdp "github.com/pion/sdp/v3"
 	"github.com/stretchr/testify/require"
 	"golang.org/x/net/ipv4"
 
@@ -31,36 +30,17 @@ func ipPtr(v net.IP) *net.IP {
 }
 
 func mediasToSDP(medias []*description.Media) []byte {
-	resetMediaControls(medias)
-
-	sout := &psdp.SessionDescription{
-		SessionName: psdp.SessionName("Stream"),
-		Origin: psdp.Origin{
-			Username:       "-",
-			NetworkType:    "IN",
-			AddressType:    "IP4",
-			UnicastAddress: "127.0.0.1",
-		},
-		// required by Darwin Streaming Server
-		ConnectionInformation: &psdp.ConnectionInformation{
-			NetworkType: "IN",
-			AddressType: "IP4",
-			Address:     &psdp.Address{Address: "0.0.0.0"},
-		},
-		TimeDescriptions: []psdp.TimeDescription{
-			{Timing: psdp.Timing{StartTime: 0, StopTime: 0}},
-		},
-		MediaDescriptions: make([]*psdp.MediaDescription, len(medias)),
+	desc := &description.Session{
+		Medias: medias,
 	}
 
-	for i, media := range medias {
-		sout.MediaDescriptions[i] = media.Marshal()
-	}
+	prepareForAnnounce(desc)
 
-	byts, err := sout.Marshal()
+	byts, err := desc.Marshal(false)
 	if err != nil {
 		panic(err)
 	}
+
 	return byts
 }
 
diff --git a/pkg/description/media_test.go b/pkg/description/media_test.go
index 2fa6ffd1..73be8344 100644
--- a/pkg/description/media_test.go
+++ b/pkg/description/media_test.go
@@ -28,6 +28,7 @@ func TestMediaURL(t *testing.T) {
 		{
 			"missing control",
 			[]byte("v=0\r\n" +
+				"s= \r\n" +
 				"m=video 0 RTP/AVP 96\r\n" +
 				"a=rtpmap:96 H264/90000\r\n"),
 			mustParseURL("rtsp://myuser:mypass@192.168.1.99:554/path/"),
@@ -36,6 +37,7 @@ func TestMediaURL(t *testing.T) {
 		{
 			"absolute control",
 			[]byte("v=0\r\n" +
+				"s= \r\n" +
 				"m=video 0 RTP/AVP 96\r\n" +
 				"a=rtpmap:96 H264/90000\r\n" +
 				"a=control:rtsp://localhost/path/trackID=7"),
@@ -45,6 +47,7 @@ func TestMediaURL(t *testing.T) {
 		{
 			"absolute control rtsps",
 			[]byte("v=0\r\n" +
+				"s= \r\n" +
 				"m=video 0 RTP/AVP 96\r\n" +
 				"a=rtpmap:96 H264/90000\r\n" +
 				"a=control:rtsps://localhost/path/trackID=7"),
@@ -54,6 +57,7 @@ func TestMediaURL(t *testing.T) {
 		{
 			"relative control",
 			[]byte("v=0\r\n" +
+				"s= \r\n" +
 				"m=video 0 RTP/AVP 96\r\n" +
 				"a=rtpmap:96 H264/90000\r\n" +
 				"a=control:trackID=5"),
@@ -63,6 +67,7 @@ func TestMediaURL(t *testing.T) {
 		{
 			"relative control, subpath",
 			[]byte("v=0\r\n" +
+				"s= \r\n" +
 				"m=video 0 RTP/AVP 96\r\n" +
 				"a=rtpmap:96 H264/90000\r\n" +
 				"a=control:trackID=5"),
@@ -72,6 +77,7 @@ func TestMediaURL(t *testing.T) {
 		{
 			"relative control, subpath, without slash",
 			[]byte("v=0\r\n" +
+				"s= \r\n" +
 				"m=video 0 RTP/AVP 96\r\n" +
 				"a=rtpmap:96 H264/90000\r\n" +
 				"a=control:trackID=5"),
@@ -81,6 +87,7 @@ func TestMediaURL(t *testing.T) {
 		{
 			"relative control, url with query",
 			[]byte("v=0\r\n" +
+				"s= \r\n" +
 				"m=video 0 RTP/AVP 96\r\n" +
 				"a=rtpmap:96 H264/90000\r\n" +
 				"a=control:trackID=5"),
@@ -92,6 +99,7 @@ func TestMediaURL(t *testing.T) {
 		{
 			"relative control, url with special chars and query",
 			[]byte("v=0\r\n" +
+				"s= \r\n" +
 				"m=video 0 RTP/AVP 96\r\n" +
 				"a=rtpmap:96 H264/90000\r\n" +
 				"a=control:trackID=5"),
@@ -103,6 +111,7 @@ func TestMediaURL(t *testing.T) {
 		{
 			"relative control, url with query without question mark",
 			[]byte("v=0\r\n" +
+				"s= \r\n" +
 				"m=video 0 RTP/AVP 96\r\n" +
 				"a=rtpmap:96 H264/90000\r\n" +
 				"a=control:trackID=5"),
@@ -112,6 +121,7 @@ func TestMediaURL(t *testing.T) {
 		{
 			"relative control, control is query",
 			[]byte("v=0\r\n" +
+				"s= \r\n" +
 				"m=video 0 RTP/AVP 96\r\n" +
 				"a=rtpmap:96 H264/90000\r\n" +
 				"a=control:?ctype=video"),
@@ -121,6 +131,7 @@ func TestMediaURL(t *testing.T) {
 		{
 			"relative control, control is query and no path",
 			[]byte("v=0\r\n" +
+				"s= \r\n" +
 				"m=video 0 RTP/AVP 96\r\n" +
 				"a=rtpmap:96 H264/90000\r\n" +
 				"a=control:?ctype=video"),
diff --git a/pkg/description/session_test.go b/pkg/description/session_test.go
index 590d875b..c374e34d 100644
--- a/pkg/description/session_test.go
+++ b/pkg/description/session_test.go
@@ -43,7 +43,7 @@ var casesSession = []struct {
 			"b=AS:8\r\n",
 		"v=0\r\n" +
 			"o=- 0 0 IN IP4 127.0.0.1\r\n" +
-			"s=Session\r\n" +
+			"s=Media Presentation\r\n" +
 			"c=IN IP4 0.0.0.0\r\n" +
 			"t=0 0\r\n" +
 			"m=video 0 RTP/AVP 97\r\n" +
@@ -57,9 +57,10 @@ var casesSession = []struct {
 			"m=application 0 RTP/AVP 107\r\n" +
 			"a=control\r\n",
 		Session{
+			Title: `Media Presentation`,
 			Medias: []*Media{
 				{
-					Type:    "video",
+					Type:    MediaTypeVideo,
 					Control: "rtsp://10.0.100.50/profile5/media.smp/trackID=v",
 					Formats: []format.Format{&format.H264{
 						PayloadTyp:        97,
@@ -69,7 +70,7 @@ var casesSession = []struct {
 					}},
 				},
 				{
-					Type:      "audio",
+					Type:      MediaTypeAudio,
 					Direction: MediaDirectionRecvonly,
 					Control:   "rtsp://10.0.100.50/profile5/media.smp/trackID=a",
 					Formats: []format.Format{&format.G711{
@@ -77,7 +78,7 @@ var casesSession = []struct {
 					}},
 				},
 				{
-					Type: "application",
+					Type: MediaTypeApplication,
 					Formats: []format.Format{&format.Generic{
 						PayloadTyp: 107,
 					}},
@@ -112,7 +113,7 @@ var casesSession = []struct {
 			"b=AS:8\r\n",
 		"v=0\r\n" +
 			"o=- 0 0 IN IP4 127.0.0.1\r\n" +
-			"s=Session\r\n" +
+			"s=Media Presentation\r\n" +
 			"c=IN IP4 0.0.0.0\r\n" +
 			"t=0 0\r\n" +
 			"m=video 0 RTP/AVP 97\r\n" +
@@ -126,9 +127,10 @@ var casesSession = []struct {
 			"m=application 0 RTP/AVP 107\r\n" +
 			"a=control\r\n",
 		Session{
+			Title: `Media Presentation`,
 			Medias: []*Media{
 				{
-					Type:    "video",
+					Type:    MediaTypeVideo,
 					Control: "trackID=1",
 					Formats: []format.Format{&format.H264{
 						PayloadTyp:        97,
@@ -138,7 +140,7 @@ var casesSession = []struct {
 					}},
 				},
 				{
-					Type:      "audio",
+					Type:      MediaTypeAudio,
 					Direction: MediaDirectionRecvonly,
 					Control:   "trackID=2",
 					Formats: []format.Format{&format.G711{
@@ -146,7 +148,7 @@ var casesSession = []struct {
 					}},
 				},
 				{
-					Type: "application",
+					Type: MediaTypeApplication,
 					Formats: []format.Format{&format.Generic{
 						PayloadTyp: 107,
 					}},
@@ -158,7 +160,7 @@ var casesSession = []struct {
 		"multiple formats for each media",
 		"v=0\r\n" +
 			"o=- 4158123474391860926 2 IN IP4 127.0.0.1\r\n" +
-			"s=-\r\n" +
+			"s= \r\n" +
 			"t=0 0\r\n" +
 			"a=group:BUNDLE audio video\r\n" +
 			"a=msid-semantic: WMS mediaSessionLocal\r\n" +
@@ -258,7 +260,7 @@ var casesSession = []struct {
 			"a=ssrc:1733091158 label:100\r\n",
 		"v=0\r\n" +
 			"o=- 0 0 IN IP4 127.0.0.1\r\n" +
-			"s=Session\r\n" +
+			"s= \r\n" +
 			"c=IN IP4 0.0.0.0\r\n" +
 			"t=0 0\r\n" +
 			"m=audio 0 RTP/AVP 111 103 104 9 102 0 8 106 105 13 110 112 113 126\r\n" +
@@ -296,9 +298,10 @@ var casesSession = []struct {
 			"a=rtpmap:124 rtx/90000\r\n" +
 			"a=fmtp:124 apt=127\r\na=rtpmap:125 ulpfec/90000\r\n",
 		Session{
+			Title: ``,
 			Medias: []*Media{
 				{
-					Type:      "audio",
+					Type:      MediaTypeAudio,
 					Direction: MediaDirectionSendonly,
 					Formats: []format.Format{
 						&format.Opus{
@@ -365,7 +368,7 @@ var casesSession = []struct {
 					},
 				},
 				{
-					Type:      "video",
+					Type:      MediaTypeVideo,
 					Direction: MediaDirectionSendonly,
 					Formats: []format.Format{
 						&format.VP8{
@@ -439,7 +442,7 @@ var casesSession = []struct {
 			"sprop-parameter-sets=Z00AKp2oHgCJ+WbgICAgQA==,aO48gA==\r\n",
 		"v=0\r\n" +
 			"o=- 0 0 IN IP4 127.0.0.1\r\n" +
-			"s=Session\r\n" +
+			"s=-\r\n" +
 			"c=IN IP4 0.0.0.0\r\n" +
 			"t=0 0\r\n" +
 			"m=video 0 RTP/AVP 96 98\r\n" +
@@ -449,9 +452,10 @@ var casesSession = []struct {
 			"sprop-parameter-sets=Z00AKp2oHgCJ+WbgICAgQA==,aO48gA==\r\n" +
 			"a=rtpmap:98 MetaData\r\n",
 		Session{
+			Title: `-`,
 			Medias: []*Media{
 				{
-					Type: "video",
+					Type: MediaTypeVideo,
 					Formats: []format.Format{
 						&format.H264{
 							PayloadTyp: 96,
@@ -488,7 +492,7 @@ var casesSession = []struct {
 			"a=sendonly\r\n",
 		"v=0\r\n" +
 			"o=- 0 0 IN IP4 127.0.0.1\r\n" +
-			"s=Session\r\n" +
+			"s=RTSP Session with audiobackchannel\r\n" +
 			"c=IN IP4 0.0.0.0\r\n" +
 			"t=0 0\r\n" +
 			"m=video 0 RTP/AVP 26\r\n" +
@@ -504,21 +508,22 @@ var casesSession = []struct {
 			"a=sendonly\r\n" +
 			"a=rtpmap:0 PCMU/8000\r\n",
 		Session{
+			Title: `RTSP Session with audiobackchannel`,
 			Medias: []*Media{
 				{
-					Type:      "video",
+					Type:      MediaTypeVideo,
 					Direction: MediaDirectionRecvonly,
 					Control:   "rtsp://192.168.0.1/video",
 					Formats:   []format.Format{&format.MJPEG{}},
 				},
 				{
-					Type:      "audio",
+					Type:      MediaTypeAudio,
 					Direction: MediaDirectionRecvonly,
 					Control:   "rtsp://192.168.0.1/audio",
 					Formats:   []format.Format{&format.G711{MULaw: true}},
 				},
 				{
-					Type:      "audio",
+					Type:      MediaTypeAudio,
 					Direction: MediaDirectionSendonly,
 					Control:   "rtsp://192.168.0.1/audioback",
 					Formats:   []format.Format{&format.G711{MULaw: true}},
@@ -536,13 +541,14 @@ var casesSession = []struct {
 			"a=rtpmap:95 TP-LINK/90000\r\n",
 		"v=0\r\n" +
 			"o=- 0 0 IN IP4 127.0.0.1\r\n" +
-			"s=Session\r\n" +
+			"s=-\r\n" +
 			"c=IN IP4 0.0.0.0\r\n" +
 			"t=0 0\r\n" +
 			"m=application/TP-LINK 0 RTP/AVP 95\r\n" +
 			"a=control\r\n" +
 			"a=rtpmap:95 TP-LINK/90000\r\n",
 		Session{
+			Title: `-`,
 			Medias: []*Media{
 				{
 					Type: "application/TP-LINK",
@@ -566,13 +572,14 @@ var casesSession = []struct {
 			"a=rtpmap:95 MERCURY/90000\n",
 		"v=0\r\n" +
 			"o=- 0 0 IN IP4 127.0.0.1\r\n" +
-			"s=Session\r\n" +
+			"s=Session streamed by \"MERCURY RTSP Server\"\r\n" +
 			"c=IN IP4 0.0.0.0\r\n" +
 			"t=0 0\r\n" +
 			"m=application/MERCURY 0 RTP/AVP 95\r\n" +
 			"a=control\r\n" +
 			"a=rtpmap:95 MERCURY/90000\r\n",
 		Session{
+			Title: `Session streamed by "MERCURY RTSP Server"`,
 			Medias: []*Media{
 				{
 					Type: "application/MERCURY",
@@ -596,7 +603,7 @@ var casesSession = []struct {
 			"a=fmtp:96 packetization-mode=1\r\n",
 		"v=0\r\n" +
 			"o=- 0 0 IN IP4 127.0.0.1\r\n" +
-			"s=Session\r\n" +
+			"s=-\r\n" +
 			"c=IN IP4 0.0.0.0\r\n" +
 			"t=0 0\r\n" +
 			"m=video 0 RTP/AVP 96\r\n" +
@@ -604,9 +611,10 @@ var casesSession = []struct {
 			"a=rtpmap:96 H264/90000\r\n" +
 			"a=fmtp:96 packetization-mode=1\r\n",
 		Session{
+			Title: "-",
 			Medias: []*Media{
 				{
-					Type: "video",
+					Type: MediaTypeVideo,
 					Formats: []format.Format{
 						&format.H264{
 							PayloadTyp:        96,
@@ -634,41 +642,6 @@ func TestSessionUnmarshal(t *testing.T) {
 	}
 }
 
-func TestSessionUnmarshalErrors(t *testing.T) {
-	for _, ca := range []struct {
-		name string
-		sdp  string
-		err  string
-	}{
-		{
-			"invalid track",
-			"v=0\r\n" +
-				"o=jdoe 2890844526 2890842807 IN IP4 10.47.16.5\r\n" +
-				"s=SDP Seminar\r\n" +
-				"m=video 0 RTP/AVP/TCP 96\r\n" +
-				"a=rtpmap:96 H265/90000\r\n" +
-				"a=fmtp:96 sprop-vps=QAEMAf//AWAAAAMAsAAAAwAAAwB4FwJA; " +
-				"sprop-sps=QgEBAWAAAAMAsAAAAwAAAwB4oAKggC8c1YgXuRZFL/y5/E/qbgQEBAE=; sprop-pps=RAHAcvBTJA==;\r\n" +
-				"a=control:streamid=0\r\n" +
-				"m=audio 0 RTP/AVP/TCP 97\r\n" +
-				"a=rtpmap:97 mpeg4-generic/44100/2\r\n" +
-				"a=fmtp:97 profile-level-id=1;mode=AAC-hbr;sizelength=13;indexlength=3;indexdeltalength=3;config=zzz1210\r\n" +
-				"a=control:streamid=1\r\n",
-			"media 2 is invalid: invalid AAC config: zzz1210",
-		},
-	} {
-		t.Run(ca.name, func(t *testing.T) {
-			var sd sdp.SessionDescription
-			err := sd.Unmarshal([]byte(ca.sdp))
-			require.NoError(t, err)
-
-			var desc Session
-			err = desc.Unmarshal(&sd)
-			require.EqualError(t, err, ca.err)
-		})
-	}
-}
-
 func TestSessionMarshal(t *testing.T) {
 	for _, ca := range casesSession {
 		t.Run(ca.name, func(t *testing.T) {
@@ -722,3 +695,29 @@ func TestSessionFindFormat(t *testing.T) {
 	require.Equal(t, md, me)
 	require.Equal(t, tr, forma)
 }
+
+func FuzzSessionUnmarshalErrors(f *testing.F) {
+	f.Add("v=0\r\n" +
+		"o=jdoe 2890844526 2890842807 IN IP4 10.47.16.5\r\n" +
+		"s=SDP Seminar\r\n" +
+		"m=video 0 RTP/AVP/TCP 96\r\n" +
+		"a=rtpmap:96 H265/90000\r\n" +
+		"a=fmtp:96 sprop-vps=QAEMAf//AWAAAAMAsAAAAwAAAwB4FwJA; " +
+		"sprop-sps=QgEBAWAAAAMAsAAAAwAAAwB4oAKggC8c1YgXuRZFL/y5/E/qbgQEBAE=; sprop-pps=RAHAcvBTJA==;\r\n" +
+		"a=control:streamid=0\r\n" +
+		"m=audio 0 RTP/AVP/TCP 97\r\n" +
+		"a=rtpmap:97 mpeg4-generic/44100/2\r\n" +
+		"a=fmtp:97 profile-level-id=1;mode=AAC-hbr;sizelength=13;indexlength=3;indexdeltalength=3;config=zzz1210\r\n" +
+		"a=control:streamid=1\r\n")
+
+	f.Fuzz(func(t *testing.T, enc string) {
+		var sd sdp.SessionDescription
+		err := sd.Unmarshal([]byte(enc))
+		if err != nil {
+			return
+		}
+
+		var desc Session
+		desc.Unmarshal(&sd) //nolint:errcheck
+	})
+}