Repository: bluenviron/gortsplib
Pull request #368 (2023-08-17 11:34:35)

allow setting additional properties of streams through description.Session

codecov[bot]: ## [Codecov](https://app.codecov.io/gh/bluenviron/gortsplib/pull/368?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=bluenviron) Report
> Merging [#368](https://app.codecov.io/gh/bluenviron/gortsplib/pull/368?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=bluenviron) (a37ff20) into [v4](https://app.codecov.io/gh/bluenviron/gortsplib/commit/4e000eb2dd39b24b6b6e16f596f1f45570c5e8c2?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=bluenviron) (4e000eb) will **increase** coverage by `0.00%`.
> The diff coverage is `86.06%`.

```diff
@@           Coverage Diff           @@
##               v4     #368   +/-   ##
=======================================
  Coverage   84.76%   84.77%           
=======================================
  Files         101      101           
  Lines       10189    10193    +4     
=======================================
+ Hits         8637     8641    +4     
  Misses       1193     1193           
  Partials      359      359           
```


| [Files Changed](https://app.codecov.io/gh/bluenviron/gortsplib/pull/368?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=bluenviron) | Coverage Δ | |
|---|---|---|
| [client.go](https://app.codecov.io/gh/bluenviron/gortsplib/pull/368?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=bluenviron#diff-Y2xpZW50Lmdv) | `74.12% <72.09%> (+0.04%)` | :arrow_up: |
| [pkg/description/media.go](https://app.codecov.io/gh/bluenviron/gortsplib/pull/368?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=bluenviron#diff-cGtnL2Rlc2NyaXB0aW9uL21lZGlhLmdv) | `91.44% <83.33%> (ø)` | |
| [server\_session.go](https://app.codecov.io/gh/bluenviron/gortsplib/pull/368?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=bluenviron#diff-c2VydmVyX3Nlc3Npb24uZ28=) | `79.20% <90.90%> (ø)` | |
| [pkg/description/stream.go](https://app.codecov.io/gh/bluenviron/gortsplib/pull/368?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=bluenviron#diff-cGtnL2Rlc2NyaXB0aW9uL3N0cmVhbS5nbw==) | `91.83% <94.11%> (ø)` | |
| [client\_media.go](https://app.codecov.io/gh/bluenviron/gortsplib/pull/368?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=bluenviron#diff-Y2xpZW50X21lZGlhLmdv) | `92.85% <100.00%> (ø)` | |
| [server\_conn.go](https://app.codecov.io/gh/bluenviron/gortsplib/pull/368?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=bluenviron#diff-c2VydmVyX2Nvbm4uZ28=) | `90.85% <100.00%> (+0.05%)` | :arrow_up: |
| [server\_session\_media.go](https://app.codecov.io/gh/bluenviron/gortsplib/pull/368?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=bluenviron#diff-c2VydmVyX3Nlc3Npb25fbWVkaWEuZ28=) | `94.71% <100.00%> (ø)` | |
| [server\_stream.go](https://app.codecov.io/gh/bluenviron/gortsplib/pull/368?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=bluenviron#diff-c2VydmVyX3N0cmVhbS5nbw==) | `85.56% <100.00%> (ø)` | |
| [server\_stream\_media.go](https://app.codecov.io/gh/bluenviron/gortsplib/pull/368?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=bluenviron#diff-c2VydmVyX3N0cmVhbV9tZWRpYS5nbw==) | `90.62% <100.00%> (ø)` | |

: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 3f4c6578..7158dfd8 100644
--- a/client.go
+++ b/client.go
@@ -23,10 +23,10 @@ import (
 	"github.com/bluenviron/gortsplib/v4/pkg/base"
 	"github.com/bluenviron/gortsplib/v4/pkg/bytecounter"
 	"github.com/bluenviron/gortsplib/v4/pkg/conn"
+	"github.com/bluenviron/gortsplib/v4/pkg/description"
 	"github.com/bluenviron/gortsplib/v4/pkg/format"
 	"github.com/bluenviron/gortsplib/v4/pkg/headers"
 	"github.com/bluenviron/gortsplib/v4/pkg/liberrors"
-	"github.com/bluenviron/gortsplib/v4/pkg/media"
 	"github.com/bluenviron/gortsplib/v4/pkg/rtptime"
 	"github.com/bluenviron/gortsplib/v4/pkg/sdp"
 	"github.com/bluenviron/gortsplib/v4/pkg/url"
@@ -91,7 +91,7 @@ func findBaseURL(sd *sdp.SessionDescription, res *base.Response, u *url.URL) (*u
 	return u, nil
 }
 
-func resetMediaControls(ms media.Medias) {
+func resetMediaControls(ms []*description.Media) {
 	for i, media := range ms {
 		media.Control = "trackID=" + strconv.FormatInt(int64(i), 10)
 	}
@@ -148,14 +148,14 @@ type describeReq struct {
 }
 
 type announceReq struct {
-	url    *url.URL
-	medias media.Medias
-	res    chan clientRes
+	url  *url.URL
+	desc *description.Session
+	res  chan clientRes
 }
 
 type setupReq struct {
-	media    *media.Media
 	baseURL  *url.URL
+	media    *description.Media
 	rtpPort  int
 	rtcpPort int
 	res      chan clientRes
@@ -175,10 +175,9 @@ type pauseReq struct {
 }
 
 type clientRes struct {
-	medias  media.Medias
-	baseURL *url.URL
-	res     *base.Response
-	err     error
+	sd  *description.Session // describe only
+	res *base.Response
+	err error
 }
 
 // ClientOnRequestFunc is the prototype of Client.OnRequest.
@@ -200,13 +199,13 @@ type ClientOnDecodeErrorFunc func(err error)
 type OnPacketRTPFunc func(*rtp.Packet)
 
 // OnPacketRTPAnyFunc is the prototype of the callback passed to OnPacketRTP(Any).
-type OnPacketRTPAnyFunc func(*media.Media, format.Format, *rtp.Packet)
+type OnPacketRTPAnyFunc func(*description.Media, format.Format, *rtp.Packet)
 
 // OnPacketRTCPFunc is the prototype of the callback passed to OnPacketRTCP().
 type OnPacketRTCPFunc func(rtcp.Packet)
 
 // OnPacketRTCPAnyFunc is the prototype of the callback passed to OnPacketRTCPAny().
-type OnPacketRTCPAnyFunc func(*media.Media, rtcp.Packet)
+type OnPacketRTCPAnyFunc func(*description.Media, rtcp.Packet)
 
 // Client is a RTSP client.
 type Client struct {
@@ -306,7 +305,7 @@ type Client struct {
 	lastDescribeURL      *url.URL
 	baseURL              *url.URL
 	effectiveTransport   *Transport
-	medias               map[*media.Media]*clientMedia
+	medias               map[*description.Media]*clientMedia
 	tcpCallbackByChannel map[int]readFunc
 	lastRange            *headers.Range
 	checkTimeoutTimer    *time.Timer
@@ -442,7 +441,7 @@ func (c *Client) Start(scheme string, host string) error {
 }
 
 // StartRecording connects to the address and starts publishing given media.
-func (c *Client) StartRecording(address string, medias media.Medias) error {
+func (c *Client) StartRecording(address string, desc *description.Session) error {
 	u, err := url.Parse(address)
 	if err != nil {
 		return err
@@ -453,13 +452,13 @@ func (c *Client) StartRecording(address string, medias media.Medias) error {
 		return err
 	}
 
-	_, err = c.Announce(u, medias)
+	_, err = c.Announce(u, desc)
 	if err != nil {
 		c.Close()
 		return err
 	}
 
-	err = c.SetupAll(u, medias)
+	err = c.SetupAll(u, desc.Medias)
 	if err != nil {
 		c.Close()
 		return err
@@ -505,11 +504,11 @@ func (c *Client) runInner() error {
 			req.res <- clientRes{res: res, err: err}
 
 		case req := <-c.describe:
-			medias, baseURL, res, err := c.doDescribe(req.url)
-			req.res <- clientRes{medias: medias, baseURL: baseURL, res: res, err: err}
+			sd, res, err := c.doDescribe(req.url)
+			req.res <- clientRes{sd: sd, res: res, err: err}
 
 		case req := <-c.announce:
-			res, err := c.doAnnounce(req.url, req.medias)
+			res, err := c.doAnnounce(req.url, req.desc)
 			req.res <- clientRes{res: res, err: err}
 
 		case req := <-c.setup:
@@ -624,7 +623,7 @@ func (c *Client) trySwitchingProtocol() error {
 	c.connURL = prevConnURL
 
 	// some Hikvision cameras require a describe before a setup
-	_, _, _, err := c.doDescribe(c.lastDescribeURL)
+	_, _, err := c.doDescribe(c.lastDescribeURL)
 	if err != nil {
 		return err
 	}
@@ -649,7 +648,7 @@ func (c *Client) trySwitchingProtocol() error {
 	return nil
 }
 
-func (c *Client) trySwitchingProtocol2(medi *media.Media, baseURL *url.URL) (*base.Response, error) {
+func (c *Client) trySwitchingProtocol2(medi *description.Media, baseURL *url.URL) (*base.Response, error) {
 	c.OnTransportSwitch(fmt.Errorf("switching to TCP because server requested it"))
 
 	prevConnURL := c.connURL
@@ -661,7 +660,7 @@ func (c *Client) trySwitchingProtocol2(medi *media.Media, baseURL *url.URL) (*ba
 	c.connURL = prevConnURL
 
 	// some Hikvision cameras require a describe before a setup
-	_, _, _, err := c.doDescribe(c.lastDescribeURL)
+	_, _, err := c.doDescribe(c.lastDescribeURL)
 	if err != nil {
 		return nil, err
 	}
@@ -970,7 +969,7 @@ func (c *Client) doOptions(u *url.URL) (*base.Response, error) {
 	return res, nil
 }
 
-// Options writes an OPTIONS request and reads a response.
+// Options sends an OPTIONS request.
 func (c *Client) Options(u *url.URL) (*base.Response, error) {
 	cres := make(chan clientRes)
 	select {
@@ -983,14 +982,14 @@ func (c *Client) Options(u *url.URL) (*base.Response, error) {
 	}
 }
 
-func (c *Client) doDescribe(u *url.URL) (media.Medias, *url.URL, *base.Response, error) {
+func (c *Client) doDescribe(u *url.URL) (*description.Session, *base.Response, error) {
 	err := c.checkState(map[clientState]struct{}{
 		clientStateInitial:   {},
 		clientStatePrePlay:   {},
 		clientStatePreRecord: {},
 	})
 	if err != nil {
-		return nil, nil, nil, err
+		return nil, nil, err
 	}
 
 	res, err := c.do(&base.Request{
@@ -1001,7 +1000,7 @@ func (c *Client) doDescribe(u *url.URL) (media.Medias, *url.URL, *base.Response,
 		},
 	}, false, false)
 	if err != nil {
-		return nil, nil, nil, err
+		return nil, nil, err
 	}
 
 	if res.StatusCode != base.StatusOK {
@@ -1013,7 +1012,7 @@ func (c *Client) doDescribe(u *url.URL) (media.Medias, *url.URL, *base.Response,
 
 			ru, err := url.Parse(res.Header["Location"][0])
 			if err != nil {
-				return nil, nil, nil, err
+				return nil, nil, err
 			}
 
 			if u.User != nil {
@@ -1028,57 +1027,58 @@ func (c *Client) doDescribe(u *url.URL) (media.Medias, *url.URL, *base.Response,
 			return c.doDescribe(ru)
 		}
 
-		return nil, nil, res, liberrors.ErrClientBadStatusCode{Code: res.StatusCode, Message: res.StatusMessage}
+		return nil, res, liberrors.ErrClientBadStatusCode{Code: res.StatusCode, Message: res.StatusMessage}
 	}
 
 	ct, ok := res.Header["Content-Type"]
 	if !ok || len(ct) != 1 {
-		return nil, nil, nil, liberrors.ErrClientContentTypeMissing{}
+		return nil, nil, liberrors.ErrClientContentTypeMissing{}
 	}
 
 	// strip encoding information from Content-Type header
 	ct = base.HeaderValue{strings.Split(ct[0], ";")[0]}
 
 	if ct[0] != "application/sdp" {
-		return nil, nil, nil, liberrors.ErrClientContentTypeUnsupported{CT: ct}
+		return nil, nil, liberrors.ErrClientContentTypeUnsupported{CT: ct}
 	}
 
-	var sd sdp.SessionDescription
-	err = sd.Unmarshal(res.Body)
+	var ssd sdp.SessionDescription
+	err = ssd.Unmarshal(res.Body)
 	if err != nil {
-		return nil, nil, nil, err
+		return nil, nil, err
 	}
 
-	var medias media.Medias
-	err = medias.Unmarshal(sd.MediaDescriptions)
+	var desc description.Session
+	err = desc.Unmarshal(&ssd)
 	if err != nil {
-		return nil, nil, nil, err
+		return nil, nil, err
 	}
 
-	baseURL, err := findBaseURL(&sd, res, u)
+	baseURL, err := findBaseURL(&ssd, res, u)
 	if err != nil {
-		return nil, nil, nil, err
+		return nil, nil, err
 	}
+	desc.BaseURL = baseURL
 
 	c.lastDescribeURL = u
 
-	return medias, baseURL, res, nil
+	return &desc, res, nil
 }
 
-// Describe writes a DESCRIBE request and reads a Response.
-func (c *Client) Describe(u *url.URL) (media.Medias, *url.URL, *base.Response, error) {
+// Describe sends a DESCRIBE request.
+func (c *Client) Describe(u *url.URL) (*description.Session, *base.Response, error) {
 	cres := make(chan clientRes)
 	select {
 	case c.describe <- describeReq{url: u, res: cres}:
 		res := <-cres
-		return res.medias, res.baseURL, res.res, res.err
+		return res.sd, res.res, res.err
 
 	case <-c.ctx.Done():
-		return nil, nil, nil, liberrors.ErrClientTerminated{}
+		return nil, nil, liberrors.ErrClientTerminated{}
 	}
 }
 
-func (c *Client) doAnnounce(u *url.URL, medias media.Medias) (*base.Response, error) {
+func (c *Client) doAnnounce(u *url.URL, desc *description.Session) (*base.Response, error) {
 	err := c.checkState(map[clientState]struct{}{
 		clientStateInitial: {},
 	})
@@ -1086,9 +1086,9 @@ func (c *Client) doAnnounce(u *url.URL, medias media.Medias) (*base.Response, er
 		return nil, err
 	}
 
-	resetMediaControls(medias)
+	resetMediaControls(desc.Medias)
 
-	byts, err := medias.Marshal(false).Marshal()
+	byts, err := desc.Marshal(false)
 	if err != nil {
 		return nil, err
 	}
@@ -1117,11 +1117,11 @@ func (c *Client) doAnnounce(u *url.URL, medias media.Medias) (*base.Response, er
 	return res, nil
 }
 
-// Announce writes an ANNOUNCE request and reads a Response.
-func (c *Client) Announce(u *url.URL, medias media.Medias) (*base.Response, error) {
+// Announce sends an ANNOUNCE request.
+func (c *Client) Announce(u *url.URL, desc *description.Session) (*base.Response, error) {
 	cres := make(chan clientRes)
 	select {
-	case c.announce <- announceReq{url: u, medias: medias, res: cres}:
+	case c.announce <- announceReq{url: u, desc: desc, res: cres}:
 		res := <-cres
 		return res.res, res.err
 
@@ -1132,7 +1132,7 @@ func (c *Client) Announce(u *url.URL, medias media.Medias) (*base.Response, erro
 
 func (c *Client) doSetup(
 	baseURL *url.URL,
-	medi *media.Media,
+	medi *description.Media,
 	rtpPort int,
 	rtcpPort int,
 ) (*base.Response, error) {
@@ -1384,7 +1384,7 @@ func (c *Client) doSetup(
 	}
 
 	if c.medias == nil {
-		c.medias = make(map[*media.Media]*clientMedia)
+		c.medias = make(map[*description.Media]*clientMedia)
 	}
 
 	c.medias[medi] = cm
@@ -1417,20 +1417,20 @@ func (c *Client) findFreeChannelPair() int {
 	}
 }
 
-// Setup writes a SETUP request and reads a Response.
+// Setup sends a SETUP request.
 // rtpPort and rtcpPort are used only if transport is UDP.
 // if rtpPort and rtcpPort are zero, they are chosen automatically.
 func (c *Client) Setup(
 	baseURL *url.URL,
-	media *media.Media,
+	media *description.Media,
 	rtpPort int,
 	rtcpPort int,
 ) (*base.Response, error) {
 	cres := make(chan clientRes)
 	select {
 	case c.setup <- setupReq{
-		media:    media,
 		baseURL:  baseURL,
+		media:    media,
 		rtpPort:  rtpPort,
 		rtcpPort: rtcpPort,
 		res:      cres,
@@ -1444,7 +1444,7 @@ func (c *Client) Setup(
 }
 
 // SetupAll setups all the given medias.
-func (c *Client) SetupAll(baseURL *url.URL, medias media.Medias) error {
+func (c *Client) SetupAll(baseURL *url.URL, medias []*description.Media) error {
 	for _, m := range medias {
 		_, err := c.Setup(baseURL, m, 0, 0)
 		if err != nil {
@@ -1509,7 +1509,7 @@ func (c *Client) doPlay(ra *headers.Range) (*base.Response, error) {
 	return res, nil
 }
 
-// Play writes a PLAY request and reads a Response.
+// Play sends a PLAY request.
 // This can be called only after Setup().
 func (c *Client) Play(ra *headers.Range) (*base.Response, error) {
 	cres := make(chan clientRes)
@@ -1551,7 +1551,7 @@ func (c *Client) doRecord() (*base.Response, error) {
 	return nil, nil
 }
 
-// Record writes a RECORD request and reads a Response.
+// Record sends a RECORD request.
 // This can be called only after Announce() and Setup().
 func (c *Client) Record() (*base.Response, error) {
 	cres := make(chan clientRes)
@@ -1601,7 +1601,7 @@ func (c *Client) doPause() (*base.Response, error) {
 	return res, nil
 }
 
-// Pause writes a PAUSE request and reads a Response.
+// Pause sends a PAUSE request.
 // This can be called only after Play() or Record().
 func (c *Client) Pause() (*base.Response, error) {
 	cres := make(chan clientRes)
@@ -1648,26 +1648,26 @@ func (c *Client) OnPacketRTCPAny(cb OnPacketRTCPAnyFunc) {
 }
 
 // OnPacketRTP sets the callback that is called when a RTP packet is read.
-func (c *Client) OnPacketRTP(medi *media.Media, forma format.Format, cb OnPacketRTPFunc) {
+func (c *Client) OnPacketRTP(medi *description.Media, forma format.Format, cb OnPacketRTPFunc) {
 	cm := c.medias[medi]
 	ct := cm.formats[forma.PayloadType()]
 	ct.onPacketRTP = cb
 }
 
 // OnPacketRTCP sets the callback that is called when a RTCP packet is read.
-func (c *Client) OnPacketRTCP(medi *media.Media, cb OnPacketRTCPFunc) {
+func (c *Client) OnPacketRTCP(medi *description.Media, cb OnPacketRTCPFunc) {
 	cm := c.medias[medi]
 	cm.onPacketRTCP = cb
 }
 
 // WritePacketRTP writes a RTP packet to the server.
-func (c *Client) WritePacketRTP(medi *media.Media, pkt *rtp.Packet) error {
+func (c *Client) WritePacketRTP(medi *description.Media, pkt *rtp.Packet) error {
 	return c.WritePacketRTPWithNTP(medi, pkt, c.timeNow())
 }
 
 // WritePacketRTPWithNTP writes a RTP packet to the server.
 // ntp is the absolute time of the packet, and is sent with periodic RTCP sender reports.
-func (c *Client) WritePacketRTPWithNTP(medi *media.Media, pkt *rtp.Packet, ntp time.Time) error {
+func (c *Client) WritePacketRTPWithNTP(medi *description.Media, pkt *rtp.Packet, ntp time.Time) error {
 	byts := make([]byte, c.MaxPacketSize)
 	n, err := pkt.MarshalTo(byts)
 	if err != nil {
@@ -1688,7 +1688,7 @@ func (c *Client) WritePacketRTPWithNTP(medi *media.Media, pkt *rtp.Packet, ntp t
 }
 
 // WritePacketRTCP writes a RTCP packet to the server.
-func (c *Client) WritePacketRTCP(medi *media.Media, pkt rtcp.Packet) error {
+func (c *Client) WritePacketRTCP(medi *description.Media, pkt rtcp.Packet) error {
 	byts, err := pkt.Marshal()
 	if err != nil {
 		return err
@@ -1713,7 +1713,7 @@ func (c *Client) PacketPTS(forma format.Format, pkt *rtp.Packet) (time.Duration,
 
 // PacketNTP returns the NTP timestamp of an incoming RTP packet.
 // The NTP timestamp is computed from sender reports.
-func (c *Client) PacketNTP(medi *media.Media, pkt *rtp.Packet) (time.Time, bool) {
+func (c *Client) PacketNTP(medi *description.Media, pkt *rtp.Packet) (time.Time, bool) {
 	cm := c.medias[medi]
 	ct := cm.formats[pkt.PayloadType]
 	return ct.rtcpReceiver.PacketNTP(pkt.Timestamp)
diff --git a/client_media.go b/client_media.go
index 044311da..9e49b403 100644
--- a/client_media.go
+++ b/client_media.go
@@ -9,12 +9,12 @@ import (
 	"github.com/pion/rtp"
 
 	"github.com/bluenviron/gortsplib/v4/pkg/base"
-	"github.com/bluenviron/gortsplib/v4/pkg/media"
+	"github.com/bluenviron/gortsplib/v4/pkg/description"
 )
 
 type clientMedia struct {
 	c                      *Client
-	media                  *media.Media
+	media                  *description.Media
 	formats                map[uint8]*clientFormat
 	tcpChannel             int
 	udpRTPListener         *clientUDPListener
@@ -71,7 +71,7 @@ func (cm *clientMedia) allocateUDPListeners(multicast bool, rtpAddress string, r
 	return err
 }
 
-func (cm *clientMedia) setMedia(medi *media.Media) {
+func (cm *clientMedia) setMedia(medi *description.Media) {
 	cm.media = medi
 
 	cm.formats = make(map[uint8]*clientFormat)
diff --git a/examples/client-publish-format-g711/main.go b/examples/client-publish-format-g711/main.go
index d5c66796..90ea7f46 100644
--- a/examples/client-publish-format-g711/main.go
+++ b/examples/client-publish-format-g711/main.go
@@ -5,8 +5,8 @@ import (
 	"net"
 
 	"github.com/bluenviron/gortsplib/v4"
+	"github.com/bluenviron/gortsplib/v4/pkg/description"
 	"github.com/bluenviron/gortsplib/v4/pkg/format"
-	"github.com/bluenviron/gortsplib/v4/pkg/media"
 	"github.com/pion/rtp"
 )
 
@@ -35,16 +35,18 @@ func main() {
 	}
 	log.Println("stream connected")
 
-	// create a media that contains a G711 format
-	medi := &media.Media{
-		Type:    media.TypeAudio,
-		Formats: []format.Format{&format.G711{}},
+	// create a description that contains a G711 format
+	desc := &description.Session{
+		Medias: []*description.Media{{
+			Type:    description.MediaTypeVideo,
+			Formats: []format.Format{&format.G711{}},
+		}},
 	}
 
 	c := gortsplib.Client{}
 
-	// connect to the server and start recording the media
-	err = c.StartRecording("rtsp://localhost:8554/mystream", media.Medias{medi})
+	// connect to the server and start recording
+	err = c.StartRecording("rtsp://localhost:8554/mystream", desc)
 	if err != nil {
 		panic(err)
 	}
@@ -59,7 +61,7 @@ func main() {
 		}
 
 		// route RTP packet to the server
-		err = c.WritePacketRTP(medi, &pkt)
+		err = c.WritePacketRTP(desc.Medias[0], &pkt)
 		if err != nil {
 			panic(err)
 		}
diff --git a/examples/client-publish-format-g722/main.go b/examples/client-publish-format-g722/main.go
index d21d7ec9..da8a05c8 100644
--- a/examples/client-publish-format-g722/main.go
+++ b/examples/client-publish-format-g722/main.go
@@ -5,8 +5,8 @@ import (
 	"net"
 
 	"github.com/bluenviron/gortsplib/v4"
+	"github.com/bluenviron/gortsplib/v4/pkg/description"
 	"github.com/bluenviron/gortsplib/v4/pkg/format"
-	"github.com/bluenviron/gortsplib/v4/pkg/media"
 	"github.com/pion/rtp"
 )
 
@@ -35,16 +35,18 @@ func main() {
 	}
 	log.Println("stream connected")
 
-	// create a media that contains a G722 format
-	medi := &media.Media{
-		Type:    media.TypeAudio,
-		Formats: []format.Format{&format.G722{}},
+	// create a description that contains a G722 format
+	desc := &description.Session{
+		Medias: []*description.Media{{
+			Type:    description.MediaTypeVideo,
+			Formats: []format.Format{&format.G722{}},
+		}},
 	}
 
 	c := gortsplib.Client{}
 
-	// connect to the server and start recording the media
-	err = c.StartRecording("rtsp://localhost:8554/mystream", media.Medias{medi})
+	// connect to the server and start recording
+	err = c.StartRecording("rtsp://localhost:8554/mystream", desc)
 	if err != nil {
 		panic(err)
 	}
@@ -59,7 +61,7 @@ func main() {
 		}
 
 		// route RTP packet to the server
-		err = c.WritePacketRTP(medi, &pkt)
+		err = c.WritePacketRTP(desc.Medias[0], &pkt)
 		if err != nil {
 			panic(err)
 		}
diff --git a/examples/client-publish-format-h264/main.go b/examples/client-publish-format-h264/main.go
index 6f8de274..0ed4bcdd 100644
--- a/examples/client-publish-format-h264/main.go
+++ b/examples/client-publish-format-h264/main.go
@@ -5,8 +5,8 @@ import (
 	"net"
 
 	"github.com/bluenviron/gortsplib/v4"
+	"github.com/bluenviron/gortsplib/v4/pkg/description"
 	"github.com/bluenviron/gortsplib/v4/pkg/format"
-	"github.com/bluenviron/gortsplib/v4/pkg/media"
 	"github.com/pion/rtp"
 )
 
@@ -37,18 +37,20 @@ func main() {
 	}
 	log.Println("stream connected")
 
-	// create a media that contains a H264 format
-	medi := &media.Media{
-		Type: media.TypeVideo,
-		Formats: []format.Format{&format.H264{
-			PayloadTyp:        96,
-			PacketizationMode: 1,
+	// create a stream description that contains a H264 format
+	desc := &description.Session{
+		Medias: []*description.Media{{
+			Type: description.MediaTypeVideo,
+			Formats: []format.Format{&format.H264{
+				PayloadTyp:        96,
+				PacketizationMode: 1,
+			}},
 		}},
 	}
 
-	// connect to the server and start recording the media
+	// connect to the server and start recording
 	c := gortsplib.Client{}
-	err = c.StartRecording("rtsp://localhost:8554/mystream", media.Medias{medi})
+	err = c.StartRecording("rtsp://localhost:8554/mystream", desc)
 	if err != nil {
 		panic(err)
 	}
@@ -63,7 +65,7 @@ func main() {
 		}
 
 		// route RTP packet to the server
-		err = c.WritePacketRTP(medi, &pkt)
+		err = c.WritePacketRTP(desc.Medias[0], &pkt)
 		if err != nil {
 			panic(err)
 		}
diff --git a/examples/client-publish-format-h265/main.go b/examples/client-publish-format-h265/main.go
index 20437569..73b9f51d 100644
--- a/examples/client-publish-format-h265/main.go
+++ b/examples/client-publish-format-h265/main.go
@@ -5,8 +5,8 @@ import (
 	"net"
 
 	"github.com/bluenviron/gortsplib/v4"
+	"github.com/bluenviron/gortsplib/v4/pkg/description"
 	"github.com/bluenviron/gortsplib/v4/pkg/format"
-	"github.com/bluenviron/gortsplib/v4/pkg/media"
 	"github.com/pion/rtp"
 )
 
@@ -36,17 +36,19 @@ func main() {
 	}
 	log.Println("stream connected")
 
-	// create a media that contains a H265 format
-	medi := &media.Media{
-		Type: media.TypeVideo,
-		Formats: []format.Format{&format.H265{
-			PayloadTyp: 96,
+	// create a description that contains a H265 format
+	desc := &description.Session{
+		Medias: []*description.Media{{
+			Type: description.MediaTypeVideo,
+			Formats: []format.Format{&format.H265{
+				PayloadTyp: 96,
+			}},
 		}},
 	}
 
-	// connect to the server and start recording the media
+	// connect to the server and start recording
 	c := gortsplib.Client{}
-	err = c.StartRecording("rtsp://localhost:8554/mystream", media.Medias{medi})
+	err = c.StartRecording("rtsp://localhost:8554/mystream", desc)
 	if err != nil {
 		panic(err)
 	}
@@ -61,7 +63,7 @@ func main() {
 		}
 
 		// route RTP packet to the server
-		err = c.WritePacketRTP(medi, &pkt)
+		err = c.WritePacketRTP(desc.Medias[0], &pkt)
 		if err != nil {
 			panic(err)
 		}
diff --git a/examples/client-publish-format-lpcm/main.go b/examples/client-publish-format-lpcm/main.go
index 6ef5159b..4ac0c8d9 100644
--- a/examples/client-publish-format-lpcm/main.go
+++ b/examples/client-publish-format-lpcm/main.go
@@ -5,8 +5,8 @@ import (
 	"net"
 
 	"github.com/bluenviron/gortsplib/v4"
+	"github.com/bluenviron/gortsplib/v4/pkg/description"
 	"github.com/bluenviron/gortsplib/v4/pkg/format"
-	"github.com/bluenviron/gortsplib/v4/pkg/media"
 	"github.com/pion/rtp"
 )
 
@@ -35,21 +35,23 @@ func main() {
 	}
 	log.Println("stream connected")
 
-	// create a media that contains a LPCM format
-	medi := &media.Media{
-		Type: media.TypeAudio,
-		Formats: []format.Format{&format.LPCM{
-			PayloadTyp:   96,
-			BitDepth:     16,
-			SampleRate:   44100,
-			ChannelCount: 1,
+	// create a description that contains a LPCM format
+	desc := &description.Session{
+		Medias: []*description.Media{{
+			Type: description.MediaTypeVideo,
+			Formats: []format.Format{&format.LPCM{
+				PayloadTyp:   96,
+				BitDepth:     16,
+				SampleRate:   44100,
+				ChannelCount: 1,
+			}},
 		}},
 	}
 
 	c := gortsplib.Client{}
 
-	// connect to the server and start recording the media
-	err = c.StartRecording("rtsp://localhost:8554/mystream", media.Medias{medi})
+	// connect to the server and start recording
+	err = c.StartRecording("rtsp://localhost:8554/mystream", desc)
 	if err != nil {
 		panic(err)
 	}
@@ -64,7 +66,7 @@ func main() {
 		}
 
 		// route RTP packet to the server
-		err = c.WritePacketRTP(medi, &pkt)
+		err = c.WritePacketRTP(desc.Medias[0], &pkt)
 		if err != nil {
 			panic(err)
 		}
diff --git a/examples/client-publish-format-mjpeg/main.go b/examples/client-publish-format-mjpeg/main.go
index c9e30edb..843cf9e3 100644
--- a/examples/client-publish-format-mjpeg/main.go
+++ b/examples/client-publish-format-mjpeg/main.go
@@ -5,8 +5,8 @@ import (
 	"net"
 
 	"github.com/bluenviron/gortsplib/v4"
+	"github.com/bluenviron/gortsplib/v4/pkg/description"
 	"github.com/bluenviron/gortsplib/v4/pkg/format"
-	"github.com/bluenviron/gortsplib/v4/pkg/media"
 	"github.com/pion/rtp"
 )
 
@@ -35,15 +35,17 @@ func main() {
 	}
 	log.Println("stream connected")
 
-	// create a media that contains a M-JPEG format
-	medi := &media.Media{
-		Type:    media.TypeVideo,
-		Formats: []format.Format{&format.MJPEG{}},
+	// create a description that contains a M-JPEG format
+	desc := &description.Session{
+		Medias: []*description.Media{{
+			Type:    description.MediaTypeVideo,
+			Formats: []format.Format{&format.MJPEG{}},
+		}},
 	}
 
-	// connect to the server and start recording the media
+	// connect to the server and start recording
 	c := gortsplib.Client{}
-	err = c.StartRecording("rtsp://localhost:8554/mystream", media.Medias{medi})
+	err = c.StartRecording("rtsp://localhost:8554/mystream", desc)
 	if err != nil {
 		panic(err)
 	}
@@ -58,7 +60,7 @@ func main() {
 		}
 
 		// route RTP packet to the server
-		err = c.WritePacketRTP(medi, &pkt)
+		err = c.WritePacketRTP(desc.Medias[0], &pkt)
 		if err != nil {
 			panic(err)
 		}
diff --git a/examples/client-publish-format-mpeg4audio/main.go b/examples/client-publish-format-mpeg4audio/main.go
index db2732d9..0e340d95 100644
--- a/examples/client-publish-format-mpeg4audio/main.go
+++ b/examples/client-publish-format-mpeg4audio/main.go
@@ -5,8 +5,8 @@ import (
 	"net"
 
 	"github.com/bluenviron/gortsplib/v4"
+	"github.com/bluenviron/gortsplib/v4/pkg/description"
 	"github.com/bluenviron/gortsplib/v4/pkg/format"
-	"github.com/bluenviron/gortsplib/v4/pkg/media"
 	"github.com/bluenviron/mediacommon/pkg/codecs/mpeg4audio"
 	"github.com/pion/rtp"
 )
@@ -36,25 +36,27 @@ func main() {
 	}
 	log.Println("stream connected")
 
-	// create a media that contains a MPEG-4 audio format
-	medi := &media.Media{
-		Type: media.TypeAudio,
-		Formats: []format.Format{&format.MPEG4Audio{
-			PayloadTyp: 96,
-			Config: &mpeg4audio.Config{
-				Type:         mpeg4audio.ObjectTypeAACLC,
-				SampleRate:   48000,
-				ChannelCount: 2,
-			},
-			SizeLength:       13,
-			IndexLength:      3,
-			IndexDeltaLength: 3,
+	// create a description that contains a MPEG-4 audio format
+	desc := &description.Session{
+		Medias: []*description.Media{{
+			Type: description.MediaTypeVideo,
+			Formats: []format.Format{&format.MPEG4Audio{
+				PayloadTyp: 96,
+				Config: &mpeg4audio.Config{
+					Type:         mpeg4audio.ObjectTypeAACLC,
+					SampleRate:   48000,
+					ChannelCount: 2,
+				},
+				SizeLength:       13,
+				IndexLength:      3,
+				IndexDeltaLength: 3,
+			}},
 		}},
 	}
 
-	// connect to the server and start recording the media
+	// connect to the server and start recording
 	c := gortsplib.Client{}
-	err = c.StartRecording("rtsp://localhost:8554/mystream", media.Medias{medi})
+	err = c.StartRecording("rtsp://localhost:8554/mystream", desc)
 	if err != nil {
 		panic(err)
 	}
@@ -69,7 +71,7 @@ func main() {
 		}
 
 		// route RTP packet to the server
-		err = c.WritePacketRTP(medi, &pkt)
+		err = c.WritePacketRTP(desc.Medias[0], &pkt)
 		if err != nil {
 			panic(err)
 		}
diff --git a/examples/client-publish-format-opus/main.go b/examples/client-publish-format-opus/main.go
index 5cda7324..8f88f346 100644
--- a/examples/client-publish-format-opus/main.go
+++ b/examples/client-publish-format-opus/main.go
@@ -5,8 +5,8 @@ import (
 	"net"
 
 	"github.com/bluenviron/gortsplib/v4"
+	"github.com/bluenviron/gortsplib/v4/pkg/description"
 	"github.com/bluenviron/gortsplib/v4/pkg/format"
-	"github.com/bluenviron/gortsplib/v4/pkg/media"
 	"github.com/pion/rtp"
 )
 
@@ -35,19 +35,20 @@ func main() {
 	}
 	log.Println("stream connected")
 
-	// create a media that contains a Opus format
-	medi := &media.Media{
-		Type: media.TypeAudio,
-		Formats: []format.Format{&format.Opus{
-			PayloadTyp: 96,
-			IsStereo:   false,
+	// create a description that contains a Opus format
+	desc := &description.Session{
+		Medias: []*description.Media{{
+			Type: description.MediaTypeVideo,
+			Formats: []format.Format{&format.Opus{
+				PayloadTyp: 96,
+				IsStereo:   false,
+			}},
 		}},
 	}
 
+	// connect to the server and start recording
 	c := gortsplib.Client{}
-
-	// connect to the server and start recording the media
-	err = c.StartRecording("rtsp://localhost:8554/mystream", media.Medias{medi})
+	err = c.StartRecording("rtsp://localhost:8554/mystream", desc)
 	if err != nil {
 		panic(err)
 	}
@@ -62,7 +63,7 @@ func main() {
 		}
 
 		// route RTP packet to the server
-		err = c.WritePacketRTP(medi, &pkt)
+		err = c.WritePacketRTP(desc.Medias[0], &pkt)
 		if err != nil {
 			panic(err)
 		}
diff --git a/examples/client-publish-format-vp8/main.go b/examples/client-publish-format-vp8/main.go
index ed2ad00e..d2e647e6 100644
--- a/examples/client-publish-format-vp8/main.go
+++ b/examples/client-publish-format-vp8/main.go
@@ -5,8 +5,8 @@ import (
 	"net"
 
 	"github.com/bluenviron/gortsplib/v4"
+	"github.com/bluenviron/gortsplib/v4/pkg/description"
 	"github.com/bluenviron/gortsplib/v4/pkg/format"
-	"github.com/bluenviron/gortsplib/v4/pkg/media"
 	"github.com/pion/rtp"
 )
 
@@ -36,17 +36,19 @@ func main() {
 	}
 	log.Println("stream connected")
 
-	// create a media that contains a VP8 format
-	medi := &media.Media{
-		Type: media.TypeVideo,
-		Formats: []format.Format{&format.VP8{
-			PayloadTyp: 96,
+	// create a description that contains a VP8 format
+	desc := &description.Session{
+		Medias: []*description.Media{{
+			Type: description.MediaTypeVideo,
+			Formats: []format.Format{&format.VP8{
+				PayloadTyp: 96,
+			}},
 		}},
 	}
 
-	// connect to the server and start recording the media
+	// connect to the server and start recording
 	c := gortsplib.Client{}
-	err = c.StartRecording("rtsp://localhost:8554/mystream", media.Medias{medi})
+	err = c.StartRecording("rtsp://localhost:8554/mystream", desc)
 	if err != nil {
 		panic(err)
 	}
@@ -61,7 +63,7 @@ func main() {
 		}
 
 		// route RTP packet to the server
-		err = c.WritePacketRTP(medi, &pkt)
+		err = c.WritePacketRTP(desc.Medias[0], &pkt)
 		if err != nil {
 			panic(err)
 		}
diff --git a/examples/client-publish-format-vp9/main.go b/examples/client-publish-format-vp9/main.go
index 3ba463b7..496238a5 100644
--- a/examples/client-publish-format-vp9/main.go
+++ b/examples/client-publish-format-vp9/main.go
@@ -5,8 +5,8 @@ import (
 	"net"
 
 	"github.com/bluenviron/gortsplib/v4"
+	"github.com/bluenviron/gortsplib/v4/pkg/description"
 	"github.com/bluenviron/gortsplib/v4/pkg/format"
-	"github.com/bluenviron/gortsplib/v4/pkg/media"
 	"github.com/pion/rtp"
 )
 
@@ -36,17 +36,19 @@ func main() {
 	}
 	log.Println("stream connected")
 
-	// create a media that contains a VP9 format
-	medi := &media.Media{
-		Type: media.TypeVideo,
-		Formats: []format.Format{&format.VP9{
-			PayloadTyp: 96,
+	// create a description that contains a VP9 format
+	desc := &description.Session{
+		Medias: []*description.Media{{
+			Type: description.MediaTypeVideo,
+			Formats: []format.Format{&format.VP9{
+				PayloadTyp: 96,
+			}},
 		}},
 	}
 
-	// connect to the server and start recording the media
+	// connect to the server and start recording
 	c := gortsplib.Client{}
-	err = c.StartRecording("rtsp://localhost:8554/mystream", media.Medias{medi})
+	err = c.StartRecording("rtsp://localhost:8554/mystream", desc)
 	if err != nil {
 		panic(err)
 	}
@@ -61,7 +63,7 @@ func main() {
 		}
 
 		// route RTP packet to the server
-		err = c.WritePacketRTP(medi, &pkt)
+		err = c.WritePacketRTP(desc.Medias[0], &pkt)
 		if err != nil {
 			panic(err)
 		}
diff --git a/examples/client-publish-options/main.go b/examples/client-publish-options/main.go
index d25f3699..78a16697 100644
--- a/examples/client-publish-options/main.go
+++ b/examples/client-publish-options/main.go
@@ -6,8 +6,8 @@ import (
 	"time"
 
 	"github.com/bluenviron/gortsplib/v4"
+	"github.com/bluenviron/gortsplib/v4/pkg/description"
 	"github.com/bluenviron/gortsplib/v4/pkg/format"
-	"github.com/bluenviron/gortsplib/v4/pkg/media"
 	"github.com/pion/rtp"
 )
 
@@ -37,12 +37,14 @@ func main() {
 	}
 	log.Println("stream connected")
 
-	// create a media that contains a H264 media
-	medi := &media.Media{
-		Type: media.TypeVideo,
-		Formats: []format.Format{&format.H264{
-			PayloadTyp:        96,
-			PacketizationMode: 1,
+	// create a stream description that contains a H264 format
+	desc := &description.Session{
+		Medias: []*description.Media{{
+			Type: description.MediaTypeVideo,
+			Formats: []format.Format{&format.H264{
+				PayloadTyp:        96,
+				PacketizationMode: 1,
+			}},
 		}},
 	}
 
@@ -56,8 +58,8 @@ func main() {
 		WriteTimeout: 10 * time.Second,
 	}
 
-	// connect to the server and start recording the media
-	err = c.StartRecording("rtsp://localhost:8554/mystream", media.Medias{medi})
+	// connect to the server and start recording
+	err = c.StartRecording("rtsp://localhost:8554/mystream", desc)
 	if err != nil {
 		panic(err)
 	}
@@ -72,7 +74,7 @@ func main() {
 		}
 
 		// route RTP packet to the server
-		err = c.WritePacketRTP(medi, &pkt)
+		err = c.WritePacketRTP(desc.Medias[0], &pkt)
 		if err != nil {
 			panic(err)
 		}
diff --git a/examples/client-publish-pause/main.go b/examples/client-publish-pause/main.go
index 54d2024a..ec631db5 100644
--- a/examples/client-publish-pause/main.go
+++ b/examples/client-publish-pause/main.go
@@ -6,8 +6,8 @@ import (
 	"time"
 
 	"github.com/bluenviron/gortsplib/v4"
+	"github.com/bluenviron/gortsplib/v4/pkg/description"
 	"github.com/bluenviron/gortsplib/v4/pkg/format"
-	"github.com/bluenviron/gortsplib/v4/pkg/media"
 	"github.com/pion/rtp"
 )
 
@@ -38,18 +38,20 @@ func main() {
 	}
 	log.Println("stream connected")
 
-	// create a media that contains a H264 format
-	medi := &media.Media{
-		Type: media.TypeVideo,
-		Formats: []format.Format{&format.H264{
-			PayloadTyp:        96,
-			PacketizationMode: 1,
+	// create a stream description that contains a H264 format
+	desc := &description.Session{
+		Medias: []*description.Media{{
+			Type: description.MediaTypeVideo,
+			Formats: []format.Format{&format.H264{
+				PayloadTyp:        96,
+				PacketizationMode: 1,
+			}},
 		}},
 	}
 
-	// connect to the server and start recording the media
+	// connect to the server and start recording
 	c := gortsplib.Client{}
-	err = c.StartRecording("rtsp://localhost:8554/mystream", media.Medias{medi})
+	err = c.StartRecording("rtsp://localhost:8554/mystream", desc)
 	if err != nil {
 		panic(err)
 	}
@@ -66,7 +68,7 @@ func main() {
 				}
 
 				// route RTP packet to the server
-				c.WritePacketRTP(medi, &pkt)
+				c.WritePacketRTP(desc.Medias[0], &pkt)
 
 				// read another RTP packet from source
 				n, _, err = pc.ReadFrom(buf)
diff --git a/examples/client-query/main.go b/examples/client-query/main.go
index a66b0668..d199dc60 100644
--- a/examples/client-query/main.go
+++ b/examples/client-query/main.go
@@ -25,10 +25,10 @@ func main() {
 	}
 	defer c.Close()
 
-	medias, _, _, err := c.Describe(u)
+	desc, _, err := c.Describe(u)
 	if err != nil {
 		panic(err)
 	}
 
-	log.Printf("available medias: %v\n", medias)
+	log.Printf("available medias: %v\n", desc.Medias)
 }
diff --git a/examples/client-read-format-g711/main.go b/examples/client-read-format-g711/main.go
index fd875f3f..d6deadf7 100644
--- a/examples/client-read-format-g711/main.go
+++ b/examples/client-read-format-g711/main.go
@@ -31,14 +31,14 @@ func main() {
 	defer c.Close()
 
 	// find published medias
-	medias, baseURL, _, err := c.Describe(u)
+	desc, _, err := c.Describe(u)
 	if err != nil {
 		panic(err)
 	}
 
 	// find the G711 media and format
 	var forma *format.G711
-	medi := medias.FindFormat(&forma)
+	medi := desc.FindFormat(&forma)
 	if medi == nil {
 		panic("media not found")
 	}
@@ -50,7 +50,7 @@ func main() {
 	}
 
 	// setup a single media
-	_, err = c.Setup(baseURL, medi, 0, 0)
+	_, err = c.Setup(desc.BaseURL, medi, 0, 0)
 	if err != nil {
 		panic(err)
 	}
diff --git a/examples/client-read-format-g722/main.go b/examples/client-read-format-g722/main.go
index de852b58..1d11ccf2 100644
--- a/examples/client-read-format-g722/main.go
+++ b/examples/client-read-format-g722/main.go
@@ -31,14 +31,14 @@ func main() {
 	defer c.Close()
 
 	// find published medias
-	medias, baseURL, _, err := c.Describe(u)
+	desc, _, err := c.Describe(u)
 	if err != nil {
 		panic(err)
 	}
 
 	// find the G722 media and format
 	var forma *format.G722
-	medi := medias.FindFormat(&forma)
+	medi := desc.FindFormat(&forma)
 	if medi == nil {
 		panic("media not found")
 	}
@@ -50,7 +50,7 @@ func main() {
 	}
 
 	// setup a single media
-	_, err = c.Setup(baseURL, medi, 0, 0)
+	_, err = c.Setup(desc.BaseURL, medi, 0, 0)
 	if err != nil {
 		panic(err)
 	}
diff --git a/examples/client-read-format-h264-convert-to-jpeg/main.go b/examples/client-read-format-h264-convert-to-jpeg/main.go
index 5e1f4d0d..e4d41625 100644
--- a/examples/client-read-format-h264-convert-to-jpeg/main.go
+++ b/examples/client-read-format-h264-convert-to-jpeg/main.go
@@ -58,14 +58,14 @@ func main() {
 	defer c.Close()
 
 	// find published medias
-	medias, baseURL, _, err := c.Describe(u)
+	desc, _, err := c.Describe(u)
 	if err != nil {
 		panic(err)
 	}
 
 	// find the H264 media and format
 	var forma *format.H264
-	medi := medias.FindFormat(&forma)
+	medi := desc.FindFormat(&forma)
 	if medi == nil {
 		panic("media not found")
 	}
@@ -92,7 +92,7 @@ func main() {
 	}
 
 	// setup a single media
-	_, err = c.Setup(baseURL, medi, 0, 0)
+	_, err = c.Setup(desc.BaseURL, medi, 0, 0)
 	if err != nil {
 		panic(err)
 	}
diff --git a/examples/client-read-format-h264-save-to-disk/main.go b/examples/client-read-format-h264-save-to-disk/main.go
index 1d8c3ca9..fbda0cf3 100644
--- a/examples/client-read-format-h264-save-to-disk/main.go
+++ b/examples/client-read-format-h264-save-to-disk/main.go
@@ -32,14 +32,14 @@ func main() {
 	defer c.Close()
 
 	// find published medias
-	medias, baseURL, _, err := c.Describe(u)
+	desc, _, err := c.Describe(u)
 	if err != nil {
 		panic(err)
 	}
 
 	// find the H264 media and format
 	var forma *format.H264
-	medi := medias.FindFormat(&forma)
+	medi := desc.FindFormat(&forma)
 	if medi == nil {
 		panic("media not found")
 	}
@@ -57,7 +57,7 @@ func main() {
 	}
 
 	// setup a single media
-	_, err = c.Setup(baseURL, medi, 0, 0)
+	_, err = c.Setup(desc.BaseURL, medi, 0, 0)
 	if err != nil {
 		panic(err)
 	}
diff --git a/examples/client-read-format-h264/main.go b/examples/client-read-format-h264/main.go
index 776f91a2..12c535f4 100644
--- a/examples/client-read-format-h264/main.go
+++ b/examples/client-read-format-h264/main.go
@@ -35,14 +35,14 @@ func main() {
 	defer c.Close()
 
 	// find published medias
-	medias, baseURL, _, err := c.Describe(u)
+	desc, _, err := c.Describe(u)
 	if err != nil {
 		panic(err)
 	}
 
 	// find the H264 media and format
 	var forma *format.H264
-	medi := medias.FindFormat(&forma)
+	medi := desc.FindFormat(&forma)
 	if medi == nil {
 		panic("media not found")
 	}
@@ -69,7 +69,7 @@ func main() {
 	}
 
 	// setup a single media
-	_, err = c.Setup(baseURL, medi, 0, 0)
+	_, err = c.Setup(desc.BaseURL, medi, 0, 0)
 	if err != nil {
 		panic(err)
 	}
diff --git a/examples/client-read-format-h265/main.go b/examples/client-read-format-h265/main.go
index 074f4d18..06b8901d 100644
--- a/examples/client-read-format-h265/main.go
+++ b/examples/client-read-format-h265/main.go
@@ -32,14 +32,14 @@ func main() {
 	defer c.Close()
 
 	// find published medias
-	medias, baseURL, _, err := c.Describe(u)
+	desc, _, err := c.Describe(u)
 	if err != nil {
 		panic(err)
 	}
 
 	// find the H265 media and format
 	var forma *format.H265
-	medi := medias.FindFormat(&forma)
+	medi := desc.FindFormat(&forma)
 	if medi == nil {
 		panic("media not found")
 	}
@@ -51,7 +51,7 @@ func main() {
 	}
 
 	// setup a single media
-	_, err = c.Setup(baseURL, medi, 0, 0)
+	_, err = c.Setup(desc.BaseURL, medi, 0, 0)
 	if err != nil {
 		panic(err)
 	}
diff --git a/examples/client-read-format-lpcm/main.go b/examples/client-read-format-lpcm/main.go
index 125cdc22..60c70f75 100644
--- a/examples/client-read-format-lpcm/main.go
+++ b/examples/client-read-format-lpcm/main.go
@@ -31,14 +31,14 @@ func main() {
 	defer c.Close()
 
 	// find published medias
-	medias, baseURL, _, err := c.Describe(u)
+	desc, _, err := c.Describe(u)
 	if err != nil {
 		panic(err)
 	}
 
 	// find the LPCM media and format
 	var forma *format.LPCM
-	medi := medias.FindFormat(&forma)
+	medi := desc.FindFormat(&forma)
 	if medi == nil {
 		panic("media not found")
 	}
@@ -50,7 +50,7 @@ func main() {
 	}
 
 	// setup a single media
-	_, err = c.Setup(baseURL, medi, 0, 0)
+	_, err = c.Setup(desc.BaseURL, medi, 0, 0)
 	if err != nil {
 		panic(err)
 	}
diff --git a/examples/client-read-format-mjpeg/main.go b/examples/client-read-format-mjpeg/main.go
index 1df37f7e..e473840a 100644
--- a/examples/client-read-format-mjpeg/main.go
+++ b/examples/client-read-format-mjpeg/main.go
@@ -35,14 +35,14 @@ func main() {
 	defer c.Close()
 
 	// find published medias
-	medias, baseURL, _, err := c.Describe(u)
+	desc, _, err := c.Describe(u)
 	if err != nil {
 		panic(err)
 	}
 
 	// find the M-JPEG media and format
 	var forma *format.MJPEG
-	medi := medias.FindFormat(&forma)
+	medi := desc.FindFormat(&forma)
 	if medi == nil {
 		panic("media not found")
 	}
@@ -54,7 +54,7 @@ func main() {
 	}
 
 	// setup a single media
-	_, err = c.Setup(baseURL, medi, 0, 0)
+	_, err = c.Setup(desc.BaseURL, medi, 0, 0)
 	if err != nil {
 		panic(err)
 	}
diff --git a/examples/client-read-format-mpeg4audio-save-to-disk/main.go b/examples/client-read-format-mpeg4audio-save-to-disk/main.go
index 0028546d..b66938e3 100644
--- a/examples/client-read-format-mpeg4audio-save-to-disk/main.go
+++ b/examples/client-read-format-mpeg4audio-save-to-disk/main.go
@@ -11,7 +11,7 @@ import (
 
 // This example shows how to
 // 1. connect to a RTSP server
-// 2. check if there's an MPEG4-audio media
+// 2. check if there's an MPEG-4 audio media
 // 3. save the content of the media into a file in MPEG-TS format
 
 func main() {
@@ -31,32 +31,32 @@ func main() {
 	defer c.Close()
 
 	// find published medias
-	medias, baseURL, _, err := c.Describe(u)
+	desc, _, err := c.Describe(u)
 	if err != nil {
 		panic(err)
 	}
 
-	// find the MPEG4-audio media and format
+	// find the MPEG-4 audio media and format
 	var forma *format.MPEG4Audio
-	medi := medias.FindFormat(&forma)
+	medi := desc.FindFormat(&forma)
 	if medi == nil {
 		panic("media not found")
 	}
 
-	// setup RTP/MPEG4-audio -> MPEG4-audio decoder
+	// setup RTP/MPEG-4 audio -> MPEG-4 audio decoder
 	rtpDec, err := forma.CreateDecoder()
 	if err != nil {
 		panic(err)
 	}
 
-	// setup MPEG4-audio -> MPEG-TS muxer
+	// setup MPEG-4 audio -> MPEG-TS muxer
 	mpegtsMuxer, err := newMPEGTSMuxer(forma.Config)
 	if err != nil {
 		panic(err)
 	}
 
 	// setup a single media
-	_, err = c.Setup(baseURL, medi, 0, 0)
+	_, err = c.Setup(desc.BaseURL, medi, 0, 0)
 	if err != nil {
 		panic(err)
 	}
diff --git a/examples/client-read-format-mpeg4audio/main.go b/examples/client-read-format-mpeg4audio/main.go
index 995d3d80..a85a72e0 100644
--- a/examples/client-read-format-mpeg4audio/main.go
+++ b/examples/client-read-format-mpeg4audio/main.go
@@ -31,14 +31,14 @@ func main() {
 	defer c.Close()
 
 	// find published medias
-	medias, baseURL, _, err := c.Describe(u)
+	desc, _, err := c.Describe(u)
 	if err != nil {
 		panic(err)
 	}
 
 	// find the MPEG4-audio media and format
 	var forma *format.MPEG4Audio
-	medi := medias.FindFormat(&forma)
+	medi := desc.FindFormat(&forma)
 	if medi == nil {
 		panic("media not found")
 	}
@@ -50,7 +50,7 @@ func main() {
 	}
 
 	// setup a single media
-	_, err = c.Setup(baseURL, medi, 0, 0)
+	_, err = c.Setup(desc.BaseURL, medi, 0, 0)
 	if err != nil {
 		panic(err)
 	}
diff --git a/examples/client-read-format-opus/main.go b/examples/client-read-format-opus/main.go
index e0444de9..e9729649 100644
--- a/examples/client-read-format-opus/main.go
+++ b/examples/client-read-format-opus/main.go
@@ -31,14 +31,14 @@ func main() {
 	defer c.Close()
 
 	// find published medias
-	medias, baseURL, _, err := c.Describe(u)
+	desc, _, err := c.Describe(u)
 	if err != nil {
 		panic(err)
 	}
 
 	// find the Opus media and format
 	var forma *format.Opus
-	medi := medias.FindFormat(&forma)
+	medi := desc.FindFormat(&forma)
 	if medi == nil {
 		panic("media not found")
 	}
@@ -50,7 +50,7 @@ func main() {
 	}
 
 	// setup a single media
-	_, err = c.Setup(baseURL, medi, 0, 0)
+	_, err = c.Setup(desc.BaseURL, medi, 0, 0)
 	if err != nil {
 		panic(err)
 	}
diff --git a/examples/client-read-format-vp8/main.go b/examples/client-read-format-vp8/main.go
index 2d3b03b1..194ebd44 100644
--- a/examples/client-read-format-vp8/main.go
+++ b/examples/client-read-format-vp8/main.go
@@ -32,14 +32,14 @@ func main() {
 	defer c.Close()
 
 	// find published medias
-	medias, baseURL, _, err := c.Describe(u)
+	desc, _, err := c.Describe(u)
 	if err != nil {
 		panic(err)
 	}
 
 	// find the VP8 media and format
 	var forma *format.VP8
-	medi := medias.FindFormat(&forma)
+	medi := desc.FindFormat(&forma)
 	if medi == nil {
 		panic("media not found")
 	}
@@ -51,7 +51,7 @@ func main() {
 	}
 
 	// setup a single media
-	_, err = c.Setup(baseURL, medi, 0, 0)
+	_, err = c.Setup(desc.BaseURL, medi, 0, 0)
 	if err != nil {
 		panic(err)
 	}
diff --git a/examples/client-read-format-vp9/main.go b/examples/client-read-format-vp9/main.go
index 2a81bc91..655cc9af 100644
--- a/examples/client-read-format-vp9/main.go
+++ b/examples/client-read-format-vp9/main.go
@@ -32,14 +32,14 @@ func main() {
 	defer c.Close()
 
 	// find published medias
-	medias, baseURL, _, err := c.Describe(u)
+	desc, _, err := c.Describe(u)
 	if err != nil {
 		panic(err)
 	}
 
 	// find the VP9 media and format
 	var forma *format.VP9
-	medi := medias.FindFormat(&forma)
+	medi := desc.FindFormat(&forma)
 	if medi == nil {
 		panic("media not found")
 	}
@@ -51,7 +51,7 @@ func main() {
 	}
 
 	// setup a single media
-	_, err = c.Setup(baseURL, medi, 0, 0)
+	_, err = c.Setup(desc.BaseURL, medi, 0, 0)
 	if err != nil {
 		panic(err)
 	}
diff --git a/examples/client-read-options/main.go b/examples/client-read-options/main.go
index 172f8939..c1c1a251 100644
--- a/examples/client-read-options/main.go
+++ b/examples/client-read-options/main.go
@@ -5,8 +5,8 @@ import (
 	"time"
 
 	"github.com/bluenviron/gortsplib/v4"
+	"github.com/bluenviron/gortsplib/v4/pkg/description"
 	"github.com/bluenviron/gortsplib/v4/pkg/format"
-	"github.com/bluenviron/gortsplib/v4/pkg/media"
 	"github.com/bluenviron/gortsplib/v4/pkg/url"
 	"github.com/pion/rtcp"
 	"github.com/pion/rtp"
@@ -41,24 +41,24 @@ func main() {
 	defer c.Close()
 
 	// find published medias
-	medias, baseURL, _, err := c.Describe(u)
+	desc, _, err := c.Describe(u)
 	if err != nil {
 		panic(err)
 	}
 
 	// setup all medias
-	err = c.SetupAll(baseURL, medias)
+	err = c.SetupAll(desc.BaseURL, desc.Medias)
 	if err != nil {
 		panic(err)
 	}
 
 	// called when a RTP packet arrives
-	c.OnPacketRTPAny(func(medi *media.Media, forma format.Format, pkt *rtp.Packet) {
+	c.OnPacketRTPAny(func(medi *description.Media, forma format.Format, pkt *rtp.Packet) {
 		log.Printf("RTP packet from media %v\n", medi)
 	})
 
 	// called when a RTCP packet arrives
-	c.OnPacketRTCPAny(func(medi *media.Media, pkt rtcp.Packet) {
+	c.OnPacketRTCPAny(func(medi *description.Media, pkt rtcp.Packet) {
 		log.Printf("RTCP packet from media %v, type %T\n", medi, pkt)
 	})
 
diff --git a/examples/client-read-pause/main.go b/examples/client-read-pause/main.go
index 78e15cc6..faa9aa17 100644
--- a/examples/client-read-pause/main.go
+++ b/examples/client-read-pause/main.go
@@ -5,8 +5,8 @@ import (
 	"time"
 
 	"github.com/bluenviron/gortsplib/v4"
+	"github.com/bluenviron/gortsplib/v4/pkg/description"
 	"github.com/bluenviron/gortsplib/v4/pkg/format"
-	"github.com/bluenviron/gortsplib/v4/pkg/media"
 	"github.com/bluenviron/gortsplib/v4/pkg/url"
 	"github.com/pion/rtcp"
 	"github.com/pion/rtp"
@@ -35,24 +35,24 @@ func main() {
 	defer c.Close()
 
 	// find published medias
-	medias, baseURL, _, err := c.Describe(u)
+	desc, _, err := c.Describe(u)
 	if err != nil {
 		panic(err)
 	}
 
 	// setup all medias
-	err = c.SetupAll(baseURL, medias)
+	err = c.SetupAll(desc.BaseURL, desc.Medias)
 	if err != nil {
 		panic(err)
 	}
 
 	// called when a RTP packet arrives
-	c.OnPacketRTPAny(func(medi *media.Media, forma format.Format, pkt *rtp.Packet) {
+	c.OnPacketRTPAny(func(medi *description.Media, forma format.Format, pkt *rtp.Packet) {
 		log.Printf("RTP packet from media %v\n", medi)
 	})
 
 	// called when a RTCP packet arrives
-	c.OnPacketRTCPAny(func(medi *media.Media, pkt rtcp.Packet) {
+	c.OnPacketRTCPAny(func(medi *description.Media, pkt rtcp.Packet) {
 		log.Printf("RTCP packet from media %v, type %T\n", medi, pkt)
 	})
 
diff --git a/examples/client-read-republish/main.go b/examples/client-read-republish/main.go
index d8565339..92b719a4 100644
--- a/examples/client-read-republish/main.go
+++ b/examples/client-read-republish/main.go
@@ -4,8 +4,8 @@ import (
 	"log"
 
 	"github.com/bluenviron/gortsplib/v4"
+	"github.com/bluenviron/gortsplib/v4/pkg/description"
 	"github.com/bluenviron/gortsplib/v4/pkg/format"
-	"github.com/bluenviron/gortsplib/v4/pkg/media"
 	"github.com/bluenviron/gortsplib/v4/pkg/url"
 	"github.com/pion/rtp"
 )
@@ -31,31 +31,31 @@ func main() {
 	defer reader.Close()
 
 	// find published medias
-	medias, baseURL, _, err := reader.Describe(sourceURL)
+	desc, _, err := reader.Describe(sourceURL)
 	if err != nil {
 		panic(err)
 	}
 
-	log.Printf("republishing %d medias", len(medias))
+	log.Printf("republishing %d medias", len(desc.Medias))
 
 	// setup all medias
 	// this must be called before StartRecording(), since it overrides the control attribute.
-	err = reader.SetupAll(baseURL, medias)
+	err = reader.SetupAll(desc.BaseURL, desc.Medias)
 	if err != nil {
 		panic(err)
 	}
 
 	// connect to the server and start recording the same medias
 	publisher := gortsplib.Client{}
-	err = publisher.StartRecording("rtsp://localhost:8554/mystream2", medias)
+	err = publisher.StartRecording("rtsp://localhost:8554/mystream2", desc)
 	if err != nil {
 		panic(err)
 	}
 	defer publisher.Close()
 
 	// read RTP packets from the reader and route them to the publisher
-	reader.OnPacketRTPAny(func(medi *media.Media, forma format.Format, pkt *rtp.Packet) {
-		publisher.WritePacketRTP(medi, pkt)
+	reader.OnPacketRTPAny(func(medi *description.Media, forma format.Format, pkt *rtp.Packet) {
+		publisher.WritePacketRTP(desc.Medias[0], pkt)
 	})
 
 	// start playing
diff --git a/examples/client-read/main.go b/examples/client-read/main.go
index 0522787d..057baeab 100644
--- a/examples/client-read/main.go
+++ b/examples/client-read/main.go
@@ -4,8 +4,8 @@ import (
 	"log"
 
 	"github.com/bluenviron/gortsplib/v4"
+	"github.com/bluenviron/gortsplib/v4/pkg/description"
 	"github.com/bluenviron/gortsplib/v4/pkg/format"
-	"github.com/bluenviron/gortsplib/v4/pkg/media"
 	"github.com/bluenviron/gortsplib/v4/pkg/url"
 	"github.com/pion/rtcp"
 	"github.com/pion/rtp"
@@ -32,24 +32,24 @@ func main() {
 	defer c.Close()
 
 	// find published medias
-	medias, baseURL, _, err := c.Describe(u)
+	desc, _, err := c.Describe(u)
 	if err != nil {
 		panic(err)
 	}
 
 	// setup all medias
-	err = c.SetupAll(baseURL, medias)
+	err = c.SetupAll(desc.BaseURL, desc.Medias)
 	if err != nil {
 		panic(err)
 	}
 
 	// called when a RTP packet arrives
-	c.OnPacketRTPAny(func(medi *media.Media, forma format.Format, pkt *rtp.Packet) {
+	c.OnPacketRTPAny(func(medi *description.Media, forma format.Format, pkt *rtp.Packet) {
 		log.Printf("RTP packet from media %v\n", medi)
 	})
 
 	// called when a RTCP packet arrives
-	c.OnPacketRTCPAny(func(medi *media.Media, pkt rtcp.Packet) {
+	c.OnPacketRTCPAny(func(medi *description.Media, pkt rtcp.Packet) {
 		log.Printf("RTCP packet from media %v, type %T\n", medi, pkt)
 	})
 
diff --git a/examples/proxy/client.go b/examples/proxy/client.go
index 8e205ea4..e6b1023c 100644
--- a/examples/proxy/client.go
+++ b/examples/proxy/client.go
@@ -5,8 +5,8 @@ import (
 	"time"
 
 	"github.com/bluenviron/gortsplib/v4"
+	"github.com/bluenviron/gortsplib/v4/pkg/description"
 	"github.com/bluenviron/gortsplib/v4/pkg/format"
-	"github.com/bluenviron/gortsplib/v4/pkg/media"
 	"github.com/bluenviron/gortsplib/v4/pkg/url"
 	"github.com/pion/rtp"
 )
@@ -57,24 +57,24 @@ func (c *client) read() error {
 	defer rc.Close()
 
 	// find published medias
-	medias, baseURL, _, err := rc.Describe(u)
+	desc, _, err := rc.Describe(u)
 	if err != nil {
 		return err
 	}
 
 	// setup all medias
-	err = rc.SetupAll(baseURL, medias)
+	err = rc.SetupAll(desc.BaseURL, desc.Medias)
 	if err != nil {
 		return err
 	}
 
-	stream := c.s.setStreamReady(medias)
+	stream := c.s.setStreamReady(desc)
 	defer c.s.setStreamUnready()
 
 	log.Printf("stream is ready and can be read from the server at rtsp://localhost:8554/stream\n")
 
 	// called when a RTP packet arrives
-	rc.OnPacketRTPAny(func(medi *media.Media, forma format.Format, pkt *rtp.Packet) {
+	rc.OnPacketRTPAny(func(medi *description.Media, forma format.Format, pkt *rtp.Packet) {
 		// route incoming packets to the server stream
 		stream.WritePacketRTP(medi, pkt)
 	})
diff --git a/examples/proxy/server.go b/examples/proxy/server.go
index 5cf0234e..660f9c7a 100644
--- a/examples/proxy/server.go
+++ b/examples/proxy/server.go
@@ -6,7 +6,7 @@ import (
 
 	"github.com/bluenviron/gortsplib/v4"
 	"github.com/bluenviron/gortsplib/v4/pkg/base"
-	"github.com/bluenviron/gortsplib/v4/pkg/media"
+	"github.com/bluenviron/gortsplib/v4/pkg/description"
 )
 
 type server struct {
@@ -99,10 +99,10 @@ func (s *server) OnPlay(ctx *gortsplib.ServerHandlerOnPlayCtx) (*base.Response,
 	}, nil
 }
 
-func (s *server) setStreamReady(medias media.Medias) *gortsplib.ServerStream {
+func (s *server) setStreamReady(desc *description.Session) *gortsplib.ServerStream {
 	s.mutex.Lock()
 	defer s.mutex.Unlock()
-	s.stream = gortsplib.NewServerStream(s.s, medias)
+	s.stream = gortsplib.NewServerStream(s.s, desc)
 	return s.stream
 }
 
diff --git a/examples/server-h264-save-to-disk/main.go b/examples/server-h264-save-to-disk/main.go
index 14258dbf..0ddeaaac 100644
--- a/examples/server-h264-save-to-disk/main.go
+++ b/examples/server-h264-save-to-disk/main.go
@@ -9,9 +9,9 @@ import (
 
 	"github.com/bluenviron/gortsplib/v4"
 	"github.com/bluenviron/gortsplib/v4/pkg/base"
+	"github.com/bluenviron/gortsplib/v4/pkg/description"
 	"github.com/bluenviron/gortsplib/v4/pkg/format"
 	"github.com/bluenviron/gortsplib/v4/pkg/format/rtph264"
-	"github.com/bluenviron/gortsplib/v4/pkg/media"
 )
 
 // This example shows how to
@@ -23,7 +23,7 @@ type serverHandler struct {
 	s           *gortsplib.Server
 	mutex       sync.Mutex
 	publisher   *gortsplib.ServerSession
-	media       *media.Media
+	media       *description.Media
 	format      *format.H264
 	rtpDec      *rtph264.Decoder
 	mpegtsMuxer *mpegtsMuxer
@@ -69,7 +69,7 @@ func (sh *serverHandler) OnAnnounce(ctx *gortsplib.ServerHandlerOnAnnounceCtx) (
 
 	// find the H264 media and format
 	var forma *format.H264
-	medi := ctx.Medias.FindFormat(&forma)
+	medi := ctx.Description.FindFormat(&forma)
 	if medi == nil {
 		return &base.Response{
 			StatusCode: base.StatusBadRequest,
diff --git a/examples/server-tls/main.go b/examples/server-tls/main.go
index bcc971c2..7af7102a 100644
--- a/examples/server-tls/main.go
+++ b/examples/server-tls/main.go
@@ -9,8 +9,8 @@ import (
 
 	"github.com/bluenviron/gortsplib/v4"
 	"github.com/bluenviron/gortsplib/v4/pkg/base"
+	"github.com/bluenviron/gortsplib/v4/pkg/description"
 	"github.com/bluenviron/gortsplib/v4/pkg/format"
-	"github.com/bluenviron/gortsplib/v4/pkg/media"
 )
 
 // This example shows how to
@@ -89,7 +89,7 @@ func (sh *serverHandler) OnAnnounce(ctx *gortsplib.ServerHandlerOnAnnounceCtx) (
 	}
 
 	// create the stream and save the publisher
-	sh.stream = gortsplib.NewServerStream(sh.s, ctx.Medias)
+	sh.stream = gortsplib.NewServerStream(sh.s, ctx.Description)
 	sh.publisher = ctx.Session
 
 	return &base.Response{
@@ -127,7 +127,7 @@ func (sh *serverHandler) OnRecord(ctx *gortsplib.ServerHandlerOnRecordCtx) (*bas
 	log.Printf("record request")
 
 	// called when receiving a RTP packet
-	ctx.Session.OnPacketRTPAny(func(medi *media.Media, forma format.Format, pkt *rtp.Packet) {
+	ctx.Session.OnPacketRTPAny(func(medi *description.Media, forma format.Format, pkt *rtp.Packet) {
 		// route the RTP packet to all readers
 		sh.stream.WritePacketRTP(medi, pkt)
 	})
diff --git a/examples/server/main.go b/examples/server/main.go
index e4df20c2..1714c60b 100644
--- a/examples/server/main.go
+++ b/examples/server/main.go
@@ -8,8 +8,8 @@ import (
 
 	"github.com/bluenviron/gortsplib/v4"
 	"github.com/bluenviron/gortsplib/v4/pkg/base"
+	"github.com/bluenviron/gortsplib/v4/pkg/description"
 	"github.com/bluenviron/gortsplib/v4/pkg/format"
-	"github.com/bluenviron/gortsplib/v4/pkg/media"
 )
 
 // This example shows how to
@@ -88,7 +88,7 @@ func (sh *serverHandler) OnAnnounce(ctx *gortsplib.ServerHandlerOnAnnounceCtx) (
 	}
 
 	// create the stream and save the publisher
-	sh.stream = gortsplib.NewServerStream(sh.s, ctx.Medias)
+	sh.stream = gortsplib.NewServerStream(sh.s, ctx.Description)
 	sh.publisher = ctx.Session
 
 	return &base.Response{
@@ -126,7 +126,7 @@ func (sh *serverHandler) OnRecord(ctx *gortsplib.ServerHandlerOnRecordCtx) (*bas
 	log.Printf("record request")
 
 	// called when receiving a RTP packet
-	ctx.Session.OnPacketRTPAny(func(medi *media.Media, forma format.Format, pkt *rtp.Packet) {
+	ctx.Session.OnPacketRTPAny(func(medi *description.Media, forma format.Format, pkt *rtp.Packet) {
 		// route the RTP packet to all readers
 		sh.stream.WritePacketRTP(medi, pkt)
 	})
diff --git a/pkg/media/media.go b/pkg/description/media.go
similarity index 85%
rename from pkg/media/media.go
rename to pkg/description/media.go
index c67e056e..1f92ee03 100644
--- a/pkg/media/media.go
+++ b/pkg/description/media.go
@@ -1,5 +1,5 @@
-// Package media contains the media stream definition.
-package media
+// Package description contains objects to describe streams.
+package description
 
 import (
 	"fmt"
@@ -26,17 +26,17 @@ func getControlAttribute(attributes []psdp.Attribute) string {
 	return ""
 }
 
-func getDirection(attributes []psdp.Attribute) Direction {
+func getDirection(attributes []psdp.Attribute) MediaDirection {
 	for _, attr := range attributes {
 		switch attr.Key {
 		case "sendonly":
-			return DirectionSendonly
+			return MediaDirectionSendonly
 
 		case "recvonly":
-			return DirectionRecvonly
+			return MediaDirectionRecvonly
 
 		case "sendrecv":
-			return DirectionSendrecv
+			return MediaDirectionSendrecv
 		}
 	}
 	return ""
@@ -92,34 +92,34 @@ func sortedKeys(fmtp map[string]string) []string {
 	return keys
 }
 
-// Direction is the direction of a media stream.
-type Direction string
+// MediaDirection is the direction of a media stream.
+type MediaDirection string
 
 // standard directions.
 const (
-	DirectionSendonly Direction = "sendonly"
-	DirectionRecvonly Direction = "recvonly"
-	DirectionSendrecv Direction = "sendrecv"
+	MediaDirectionSendonly MediaDirection = "sendonly"
+	MediaDirectionRecvonly MediaDirection = "recvonly"
+	MediaDirectionSendrecv MediaDirection = "sendrecv"
 )
 
-// Type is the type of a media stream.
-type Type string
+// MediaType is the type of a media stream.
+type MediaType string
 
 // standard media stream types.
 const (
-	TypeVideo       Type = "video"
-	TypeAudio       Type = "audio"
-	TypeApplication Type = "application"
+	MediaTypeVideo       MediaType = "video"
+	MediaTypeAudio       MediaType = "audio"
+	MediaTypeApplication MediaType = "application"
 )
 
 // Media is a media stream.
 // It contains one or more formats.
 type Media struct {
 	// Media type.
-	Type Type
+	Type MediaType
 
 	// Direction of the stream.
-	Direction Direction
+	Direction MediaDirection
 
 	// Control attribute.
 	Control string
@@ -128,8 +128,9 @@ type Media struct {
 	Formats []format.Format
 }
 
-func (m *Media) unmarshal(md *psdp.MediaDescription) error {
-	m.Type = Type(md.MediaName.Media)
+// Unmarshal decodes the media from the SDP format.
+func (m *Media) Unmarshal(md *psdp.MediaDescription) error {
+	m.Type = MediaType(md.MediaName.Media)
 	m.Direction = getDirection(md.Attributes)
 	m.Control = getControlAttribute(md.Attributes)
 
diff --git a/pkg/media/medias.go b/pkg/description/session.go
similarity index 52%
rename from pkg/media/medias.go
rename to pkg/description/session.go
index 9a4ceaa5..c7f7a7ce 100644
--- a/pkg/media/medias.go
+++ b/pkg/description/session.go
@@ -1,4 +1,4 @@
-package media
+package description
 
 import (
 	"fmt"
@@ -6,29 +6,48 @@ import (
 	psdp "github.com/pion/sdp/v3"
 
 	"github.com/bluenviron/gortsplib/v4/pkg/sdp"
+	"github.com/bluenviron/gortsplib/v4/pkg/url"
 )
 
-// Medias is a list of media streams.
-type Medias []*Media
+// Session is the description of a RTSP session.
+type Session struct {
+	// base URL of the stream (read only).
+	BaseURL *url.URL
 
-// Unmarshal decodes medias from the SDP format.
-func (ms *Medias) Unmarshal(mds []*psdp.MediaDescription) error {
-	*ms = make(Medias, len(mds))
+	// available media streams.
+	Medias []*Media
+}
+
+// FindFormat finds a certain format among all the formats in all the medias of the stream.
+// If the format is found, it is inserted into forma, and its media is returned.
+func (d *Session) FindFormat(forma interface{}) *Media {
+	for _, media := range d.Medias {
+		ok := media.FindFormat(forma)
+		if ok {
+			return media
+		}
+	}
+	return nil
+}
 
-	for i, md := range mds {
+// Unmarshal decodes the description from SDP.
+func (d *Session) Unmarshal(ssd *sdp.SessionDescription) error {
+	d.Medias = make([]*Media, len(ssd.MediaDescriptions))
+
+	for i, md := range ssd.MediaDescriptions {
 		var m Media
-		err := m.unmarshal(md)
+		err := m.Unmarshal(md)
 		if err != nil {
 			return fmt.Errorf("media %d is invalid: %v", i+1, err)
 		}
-		(*ms)[i] = &m
+		d.Medias[i] = &m
 	}
 
 	return nil
 }
 
-// Marshal encodes the medias in SDP format.
-func (ms Medias) Marshal(multicast bool) *sdp.SessionDescription {
+// Marshal encodes the description in SDP.
+func (d Session) Marshal(multicast bool) ([]byte, error) {
 	var address string
 	if multicast {
 		address = "224.1.0.0"
@@ -37,14 +56,14 @@ func (ms Medias) Marshal(multicast bool) *sdp.SessionDescription {
 	}
 
 	sout := &sdp.SessionDescription{
-		SessionName: psdp.SessionName("Stream"),
+		SessionName: psdp.SessionName("Session"),
 		Origin: psdp.Origin{
 			Username:       "-",
 			NetworkType:    "IN",
 			AddressType:    "IP4",
 			UnicastAddress: "127.0.0.1",
 		},
-		// required by Darwin Streaming Server
+		// required by Darwin Sessioning Server
 		ConnectionInformation: &psdp.ConnectionInformation{
 			NetworkType: "IN",
 			AddressType: "IP4",
@@ -53,24 +72,12 @@ func (ms Medias) Marshal(multicast bool) *sdp.SessionDescription {
 		TimeDescriptions: []psdp.TimeDescription{
 			{Timing: psdp.Timing{StartTime: 0, StopTime: 0}},
 		},
-		MediaDescriptions: make([]*psdp.MediaDescription, len(ms)),
+		MediaDescriptions: make([]*psdp.MediaDescription, len(d.Medias)),
 	}
 
-	for i, media := range ms {
+	for i, media := range d.Medias {
 		sout.MediaDescriptions[i] = media.Marshal()
 	}
 
-	return sout
-}
-
-// FindFormat finds a certain format among all the formats in all the medias.
-// If the format is found, it is inserted into forma, and its media is returned.
-func (ms Medias) FindFormat(forma interface{}) *Media {
-	for _, media := range ms {
-		ok := media.FindFormat(forma)
-		if ok {
-			return media
-		}
-	}
-	return nil
+	return sout.Marshal()
 }
diff --git a/server_conn.go b/server_conn.go
index 13b9bc9a..3ed1dc87 100644
--- a/server_conn.go
+++ b/server_conn.go
@@ -12,8 +12,8 @@ import (
 	"github.com/bluenviron/gortsplib/v4/pkg/base"
 	"github.com/bluenviron/gortsplib/v4/pkg/bytecounter"
 	"github.com/bluenviron/gortsplib/v4/pkg/conn"
+	"github.com/bluenviron/gortsplib/v4/pkg/description"
 	"github.com/bluenviron/gortsplib/v4/pkg/liberrors"
-	"github.com/bluenviron/gortsplib/v4/pkg/media"
 	"github.com/bluenviron/gortsplib/v4/pkg/url"
 )
 
@@ -24,14 +24,13 @@ func getSessionID(header base.Header) string {
 	return ""
 }
 
-func mediasForSDP(
-	medias media.Medias,
-	contentBase *url.URL,
-) media.Medias {
-	newMedias := make(media.Medias, len(medias))
+func streamDescCopyForServer(d *description.Session, contentBase *url.URL) *description.Session {
+	out := &description.Session{
+		Medias: make([]*description.Media, len(d.Medias)),
+	}
 
-	for i, medi := range medias {
-		mc := &media.Media{
+	for i, medi := range d.Medias {
+		mc := &description.Media{
 			Type: medi.Type,
 			// Direction: skipped for the moment
 			Formats: medi.Formats,
@@ -47,10 +46,10 @@ func mediasForSDP(
 		u, _ := mc.URL(contentBase)
 		mc.Control = u.String()
 
-		newMedias[i] = mc
+		out.Medias[i] = mc
 	}
 
-	return newMedias
+	return out
 }
 
 type readReq struct {
@@ -294,7 +293,7 @@ func (sc *ServerConn) handleRequestInner(req *base.Request) (*base.Response, err
 				}
 
 				if stream != nil {
-					byts, _ := mediasForSDP(stream.medias, req.URL).Marshal(multicast).Marshal()
+					byts, _ := streamDescCopyForServer(stream.desc, req.URL).Marshal(multicast)
 					res.Body = byts
 				}
 			}
diff --git a/server_handler.go b/server_handler.go
index 0ed4a8b6..d0e49218 100644
--- a/server_handler.go
+++ b/server_handler.go
@@ -2,7 +2,7 @@ package gortsplib
 
 import (
 	"github.com/bluenviron/gortsplib/v4/pkg/base"
-	"github.com/bluenviron/gortsplib/v4/pkg/media"
+	"github.com/bluenviron/gortsplib/v4/pkg/description"
 )
 
 // ServerHandler is the interface implemented by all the server handlers.
@@ -83,12 +83,12 @@ type ServerHandlerOnDescribe interface {
 
 // ServerHandlerOnAnnounceCtx is the context of OnAnnounce.
 type ServerHandlerOnAnnounceCtx struct {
-	Session *ServerSession
-	Conn    *ServerConn
-	Request *base.Request
-	Path    string
-	Query   string
-	Medias  media.Medias
+	Session     *ServerSession
+	Conn        *ServerConn
+	Request     *base.Request
+	Path        string
+	Query       string
+	Description *description.Session
 }
 
 // ServerHandlerOnAnnounce can be implemented by a ServerHandler.
diff --git a/server_session.go b/server_session.go
index 41a3d751..7257fa65 100644
--- a/server_session.go
+++ b/server_session.go
@@ -14,10 +14,10 @@ import (
 	"github.com/pion/rtp"
 
 	"github.com/bluenviron/gortsplib/v4/pkg/base"
+	"github.com/bluenviron/gortsplib/v4/pkg/description"
 	"github.com/bluenviron/gortsplib/v4/pkg/format"
 	"github.com/bluenviron/gortsplib/v4/pkg/headers"
 	"github.com/bluenviron/gortsplib/v4/pkg/liberrors"
-	"github.com/bluenviron/gortsplib/v4/pkg/media"
 	"github.com/bluenviron/gortsplib/v4/pkg/rtptime"
 	"github.com/bluenviron/gortsplib/v4/pkg/sdp"
 	"github.com/bluenviron/gortsplib/v4/pkg/url"
@@ -58,7 +58,7 @@ func serverParseURLForPlay(u *url.URL) (string, string, string, error) {
 	return path, query, trackID, nil
 }
 
-func findMediaByURL(medias media.Medias, baseURL *url.URL, u *url.URL) *media.Media {
+func findMediaByURL(medias []*description.Media, baseURL *url.URL, u *url.URL) *description.Media {
 	for _, media := range medias {
 		u1, err := media.URL(baseURL)
 		if err == nil && u1.String() == u.String() {
@@ -69,9 +69,9 @@ func findMediaByURL(medias media.Medias, baseURL *url.URL, u *url.URL) *media.Me
 	return nil
 }
 
-func findMediaByTrackID(st *ServerStream, trackID string) *media.Media {
+func findMediaByTrackID(medias []*description.Media, trackID string) *description.Media {
 	if trackID == "" {
-		return st.medias[0]
+		return medias[0]
 	}
 
 	tmp, err := strconv.ParseUint(trackID, 10, 31)
@@ -80,11 +80,11 @@ func findMediaByTrackID(st *ServerStream, trackID string) *media.Media {
 	}
 	id := int(tmp)
 
-	if len(st.medias) <= id {
+	if len(medias) <= id {
 		return nil
 	}
 
-	return st.medias[id]
+	return medias[id]
 }
 
 func findFirstSupportedTransportHeader(s *Server, tsh headers.Transports) *headers.Transport {
@@ -144,7 +144,7 @@ type ServerSession struct {
 	userData              interface{}
 	conns                 map[*ServerConn]struct{}
 	state                 ServerSessionState
-	setuppedMedias        map[*media.Media]*serverSessionMedia
+	setuppedMedias        map[*description.Media]*serverSessionMedia
 	setuppedMediasOrdered []*serverSessionMedia
 	tcpCallbackByChannel  map[int]readFunc
 	setuppedTransport     *Transport
@@ -153,8 +153,8 @@ type ServerSession struct {
 	setuppedQuery         string
 	lastRequestTime       time.Time
 	tcpConn               *ServerConn
-	announcedMedias       media.Medias // publish
-	udpLastPacketTime     *int64       // publish
+	announcedDesc         *description.Session // publish
+	udpLastPacketTime     *int64               // publish
 	udpCheckStreamTimer   *time.Timer
 	writer                asyncProcessor
 	timeDecoder           *rtptime.GlobalDecoder
@@ -221,14 +221,14 @@ func (ss *ServerSession) SetuppedTransport() *Transport {
 	return ss.setuppedTransport
 }
 
-// AnnouncedMedias returns the announced medias.
-func (ss *ServerSession) AnnouncedMedias() media.Medias {
-	return ss.announcedMedias
+// AnnouncedDescription returns the announced stream description.
+func (ss *ServerSession) AnnouncedDescription() *description.Session {
+	return ss.announcedDesc
 }
 
 // SetuppedMedias returns the setupped medias.
-func (ss *ServerSession) SetuppedMedias() media.Medias {
-	ret := make(media.Medias, len(ss.setuppedMedias))
+func (ss *ServerSession) SetuppedMedias() []*description.Media {
+	ret := make([]*description.Media, len(ss.setuppedMedias))
 	for i, sm := range ss.setuppedMediasOrdered {
 		ret[i] = sm.media
 	}
@@ -511,23 +511,23 @@ func (ss *ServerSession) handleRequestInner(sc *ServerConn, req *base.Request) (
 			}, liberrors.ErrServerContentTypeUnsupported{CT: ct}
 		}
 
-		var sd sdp.SessionDescription
-		err = sd.Unmarshal(req.Body)
+		var ssd sdp.SessionDescription
+		err = ssd.Unmarshal(req.Body)
 		if err != nil {
 			return &base.Response{
 				StatusCode: base.StatusBadRequest,
 			}, liberrors.ErrServerSDPInvalid{Err: err}
 		}
 
-		var medias media.Medias
-		err = medias.Unmarshal(sd.MediaDescriptions)
+		var desc description.Session
+		err = desc.Unmarshal(&ssd)
 		if err != nil {
 			return &base.Response{
 				StatusCode: base.StatusBadRequest,
 			}, liberrors.ErrServerSDPInvalid{Err: err}
 		}
 
-		for _, medi := range medias {
+		for _, medi := range desc.Medias {
 			mediURL, err := medi.URL(req.URL)
 			if err != nil {
 				return &base.Response{
@@ -551,12 +551,12 @@ func (ss *ServerSession) handleRequestInner(sc *ServerConn, req *base.Request) (
 		}
 
 		res, err := ss.s.Handler.(ServerHandlerOnAnnounce).OnAnnounce(&ServerHandlerOnAnnounceCtx{
-			Session: ss,
-			Conn:    sc,
-			Request: req,
-			Path:    path,
-			Query:   query,
-			Medias:  medias,
+			Session:     ss,
+			Conn:        sc,
+			Request:     req,
+			Path:        path,
+			Query:       query,
+			Description: &desc,
 		})
 
 		if res.StatusCode != base.StatusOK {
@@ -566,7 +566,7 @@ func (ss *ServerSession) handleRequestInner(sc *ServerConn, req *base.Request) (
 		ss.state = ServerSessionStatePreRecord
 		ss.setuppedPath = &path
 		ss.setuppedQuery = query
-		ss.announcedMedias = medias
+		ss.announcedDesc = &desc
 
 		return res, err
 
@@ -706,10 +706,10 @@ func (ss *ServerSession) handleRequestInner(sc *ServerConn, req *base.Request) (
 			return res, err
 		}
 
-		var medi *media.Media
+		var medi *description.Media
 		switch ss.state {
 		case ServerSessionStateInitial, ServerSessionStatePrePlay: // play
-			medi = findMediaByTrackID(stream, trackID)
+			medi = findMediaByTrackID(stream.desc.Medias, trackID)
 		default: // record
 			baseURL := &url.URL{
 				Scheme:   req.URL.Scheme,
@@ -724,7 +724,7 @@ func (ss *ServerSession) handleRequestInner(sc *ServerConn, req *base.Request) (
 				baseURL.Path += "/"
 			}
 
-			medi = findMediaByURL(ss.announcedMedias, baseURL, req.URL)
+			medi = findMediaByURL(ss.announcedDesc.Medias, baseURL, req.URL)
 		}
 
 		if medi == nil {
@@ -819,7 +819,7 @@ func (ss *ServerSession) handleRequestInner(sc *ServerConn, req *base.Request) (
 		}
 
 		if ss.setuppedMedias == nil {
-			ss.setuppedMedias = make(map[*media.Media]*serverSessionMedia)
+			ss.setuppedMedias = make(map[*description.Media]*serverSessionMedia)
 		}
 		ss.setuppedMedias[medi] = sm
 		ss.setuppedMediasOrdered = append(ss.setuppedMediasOrdered, sm)
@@ -934,7 +934,7 @@ func (ss *ServerSession) handleRequestInner(sc *ServerConn, req *base.Request) (
 			}, err
 		}
 
-		if len(ss.setuppedMedias) != len(ss.announcedMedias) {
+		if len(ss.setuppedMedias) != len(ss.announcedDesc.Medias) {
 			return &base.Response{
 				StatusCode: base.StatusBadRequest,
 			}, liberrors.ErrServerNotAllAnnouncedMediasSetup{}
@@ -1148,25 +1148,25 @@ func (ss *ServerSession) OnPacketRTCPAny(cb OnPacketRTCPAnyFunc) {
 }
 
 // OnPacketRTP sets the callback that is called when a RTP packet is read.
-func (ss *ServerSession) OnPacketRTP(medi *media.Media, forma format.Format, cb OnPacketRTPFunc) {
+func (ss *ServerSession) OnPacketRTP(medi *description.Media, forma format.Format, cb OnPacketRTPFunc) {
 	sm := ss.setuppedMedias[medi]
 	st := sm.formats[forma.PayloadType()]
 	st.onPacketRTP = cb
 }
 
 // OnPacketRTCP sets the callback that is called when a RTCP packet is read.
-func (ss *ServerSession) OnPacketRTCP(medi *media.Media, cb OnPacketRTCPFunc) {
+func (ss *ServerSession) OnPacketRTCP(medi *description.Media, cb OnPacketRTCPFunc) {
 	sm := ss.setuppedMedias[medi]
 	sm.onPacketRTCP = cb
 }
 
-func (ss *ServerSession) writePacketRTP(medi *media.Media, byts []byte) {
+func (ss *ServerSession) writePacketRTP(medi *description.Media, byts []byte) {
 	sm := ss.setuppedMedias[medi]
 	sm.writePacketRTP(byts)
 }
 
 // WritePacketRTP writes a RTP packet to the session.
-func (ss *ServerSession) WritePacketRTP(medi *media.Media, pkt *rtp.Packet) error {
+func (ss *ServerSession) WritePacketRTP(medi *description.Media, pkt *rtp.Packet) error {
 	byts := make([]byte, ss.s.MaxPacketSize)
 	n, err := pkt.MarshalTo(byts)
 	if err != nil {
@@ -1178,13 +1178,13 @@ func (ss *ServerSession) WritePacketRTP(medi *media.Media, pkt *rtp.Packet) erro
 	return nil
 }
 
-func (ss *ServerSession) writePacketRTCP(medi *media.Media, byts []byte) {
+func (ss *ServerSession) writePacketRTCP(medi *description.Media, byts []byte) {
 	sm := ss.setuppedMedias[medi]
 	sm.writePacketRTCP(byts)
 }
 
 // WritePacketRTCP writes a RTCP packet to the session.
-func (ss *ServerSession) WritePacketRTCP(medi *media.Media, pkt rtcp.Packet) error {
+func (ss *ServerSession) WritePacketRTCP(medi *description.Media, pkt rtcp.Packet) error {
 	byts, err := pkt.Marshal()
 	if err != nil {
 		return err
@@ -1202,7 +1202,7 @@ func (ss *ServerSession) PacketPTS(forma format.Format, pkt *rtp.Packet) (time.D
 
 // PacketNTP returns the NTP timestamp of an incoming RTP packet.
 // The NTP timestamp is computed from sender reports.
-func (ss *ServerSession) PacketNTP(medi *media.Media, pkt *rtp.Packet) (time.Time, bool) {
+func (ss *ServerSession) PacketNTP(medi *description.Media, pkt *rtp.Packet) (time.Time, bool) {
 	sm := ss.setuppedMedias[medi]
 	sf := sm.formats[pkt.PayloadType]
 	return sf.rtcpReceiver.PacketNTP(pkt.Timestamp)
diff --git a/server_session_media.go b/server_session_media.go
index e959929a..d979793b 100644
--- a/server_session_media.go
+++ b/server_session_media.go
@@ -10,12 +10,12 @@ import (
 	"github.com/pion/rtp"
 
 	"github.com/bluenviron/gortsplib/v4/pkg/base"
-	"github.com/bluenviron/gortsplib/v4/pkg/media"
+	"github.com/bluenviron/gortsplib/v4/pkg/description"
 )
 
 type serverSessionMedia struct {
 	ss                     *ServerSession
-	media                  *media.Media
+	media                  *description.Media
 	tcpChannel             int
 	udpRTPReadPort         int
 	udpRTPWriteAddr        *net.UDPAddr
@@ -30,7 +30,7 @@ type serverSessionMedia struct {
 	onPacketRTCP           OnPacketRTCPFunc
 }
 
-func newServerSessionMedia(ss *ServerSession, medi *media.Media) *serverSessionMedia {
+func newServerSessionMedia(ss *ServerSession, medi *description.Media) *serverSessionMedia {
 	sm := &serverSessionMedia{
 		ss:           ss,
 		media:        medi,
diff --git a/server_stream.go b/server_stream.go
index b90dae2e..01b986f6 100644
--- a/server_stream.go
+++ b/server_stream.go
@@ -8,9 +8,9 @@ import (
 	"github.com/pion/rtcp"
 	"github.com/pion/rtp"
 
+	"github.com/bluenviron/gortsplib/v4/pkg/description"
 	"github.com/bluenviron/gortsplib/v4/pkg/headers"
 	"github.com/bluenviron/gortsplib/v4/pkg/liberrors"
-	"github.com/bluenviron/gortsplib/v4/pkg/media"
 )
 
 func firstFormat(formats map[uint8]*serverStreamFormat) *serverStreamFormat {
@@ -29,27 +29,27 @@ func firstFormat(formats map[uint8]*serverStreamFormat) *serverStreamFormat {
 // - allocating multicast listeners
 // - gathering infos about the stream in order to generate SSRC and RTP-Info
 type ServerStream struct {
-	s      *Server
-	medias media.Medias
+	s    *Server
+	desc *description.Session
 
 	mutex                sync.RWMutex
 	activeUnicastReaders map[*ServerSession]struct{}
 	readers              map[*ServerSession]struct{}
-	streamMedias         map[*media.Media]*serverStreamMedia
+	streamMedias         map[*description.Media]*serverStreamMedia
 	closed               bool
 }
 
 // NewServerStream allocates a ServerStream.
-func NewServerStream(s *Server, medias media.Medias) *ServerStream {
+func NewServerStream(s *Server, desc *description.Session) *ServerStream {
 	st := &ServerStream{
 		s:                    s,
-		medias:               medias,
+		desc:                 desc,
 		activeUnicastReaders: make(map[*ServerSession]struct{}),
 		readers:              make(map[*ServerSession]struct{}),
 	}
 
-	st.streamMedias = make(map[*media.Media]*serverStreamMedia, len(medias))
-	for i, medi := range medias {
+	st.streamMedias = make(map[*description.Media]*serverStreamMedia, len(desc.Medias))
+	for i, medi := range desc.Medias {
 		st.streamMedias[medi] = newServerStreamMedia(st, medi, i)
 	}
 
@@ -71,12 +71,12 @@ func (st *ServerStream) Close() {
 	}
 }
 
-// Medias returns the medias of the stream.
-func (st *ServerStream) Medias() media.Medias {
-	return st.medias
+// Description returns the description of the stream.
+func (st *ServerStream) Description() *description.Session {
+	return st.desc
 }
 
-func (st *ServerStream) senderSSRC(medi *media.Media) (uint32, bool) {
+func (st *ServerStream) senderSSRC(medi *description.Media) (uint32, bool) {
 	st.mutex.Lock()
 	defer st.mutex.Unlock()
 
@@ -92,7 +92,7 @@ func (st *ServerStream) senderSSRC(medi *media.Media) (uint32, bool) {
 	return firstFormat(sm.formats).rtcpSender.SenderSSRC()
 }
 
-func (st *ServerStream) rtpInfoEntry(medi *media.Media, now time.Time) *headers.RTPInfoEntry {
+func (st *ServerStream) rtpInfoEntry(medi *description.Media, now time.Time) *headers.RTPInfoEntry {
 	st.mutex.Lock()
 	defer st.mutex.Unlock()
 
@@ -233,13 +233,13 @@ func (st *ServerStream) readerSetInactive(ss *ServerSession) {
 }
 
 // WritePacketRTP writes a RTP packet to all the readers of the stream.
-func (st *ServerStream) WritePacketRTP(medi *media.Media, pkt *rtp.Packet) error {
+func (st *ServerStream) WritePacketRTP(medi *description.Media, pkt *rtp.Packet) error {
 	return st.WritePacketRTPWithNTP(medi, pkt, st.s.timeNow())
 }
 
 // WritePacketRTPWithNTP writes a RTP packet to all the readers of the stream.
 // ntp is the absolute time of the packet, and is sent with periodic RTCP sender reports.
-func (st *ServerStream) WritePacketRTPWithNTP(medi *media.Media, pkt *rtp.Packet, ntp time.Time) error {
+func (st *ServerStream) WritePacketRTPWithNTP(medi *description.Media, pkt *rtp.Packet, ntp time.Time) error {
 	byts := make([]byte, st.s.MaxPacketSize)
 	n, err := pkt.MarshalTo(byts)
 	if err != nil {
@@ -260,7 +260,7 @@ func (st *ServerStream) WritePacketRTPWithNTP(medi *media.Media, pkt *rtp.Packet
 }
 
 // WritePacketRTCP writes a RTCP packet to all the readers of the stream.
-func (st *ServerStream) WritePacketRTCP(medi *media.Media, pkt rtcp.Packet) error {
+func (st *ServerStream) WritePacketRTCP(medi *description.Media, pkt rtcp.Packet) error {
 	byts, err := pkt.Marshal()
 	if err != nil {
 		return err
diff --git a/server_stream_media.go b/server_stream_media.go
index 4da3b598..c75f5d77 100644
--- a/server_stream_media.go
+++ b/server_stream_media.go
@@ -6,19 +6,19 @@ import (
 	"github.com/pion/rtcp"
 	"github.com/pion/rtp"
 
-	"github.com/bluenviron/gortsplib/v4/pkg/media"
+	"github.com/bluenviron/gortsplib/v4/pkg/description"
 	"github.com/bluenviron/gortsplib/v4/pkg/rtcpsender"
 )
 
 type serverStreamMedia struct {
 	st              *ServerStream
-	media           *media.Media
+	media           *description.Media
 	trackID         int
 	formats         map[uint8]*serverStreamFormat
 	multicastWriter *serverMulticastWriter
 }
 
-func newServerStreamMedia(st *ServerStream, medi *media.Media, trackID int) *serverStreamMedia {
+func newServerStreamMedia(st *ServerStream, medi *description.Media, trackID int) *serverStreamMedia {
 	sm := &serverStreamMedia{
 		st:      st,
 		media:   medi,


Test patch:
diff --git a/client_play_test.go b/client_play_test.go
index ea8887f2..2a4fdb57 100644
--- a/client_play_test.go
+++ b/client_play_test.go
@@ -12,15 +12,16 @@ 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"
 
 	"github.com/bluenviron/gortsplib/v4/pkg/auth"
 	"github.com/bluenviron/gortsplib/v4/pkg/base"
 	"github.com/bluenviron/gortsplib/v4/pkg/conn"
+	"github.com/bluenviron/gortsplib/v4/pkg/description"
 	"github.com/bluenviron/gortsplib/v4/pkg/format"
 	"github.com/bluenviron/gortsplib/v4/pkg/headers"
-	"github.com/bluenviron/gortsplib/v4/pkg/media"
 	"github.com/bluenviron/gortsplib/v4/pkg/url"
 	"github.com/bluenviron/mediacommon/pkg/codecs/mpeg4audio"
 )
@@ -29,8 +30,34 @@ func ipPtr(v net.IP) *net.IP {
 	return &v
 }
 
-func mustMarshalMedias(medias media.Medias) []byte {
-	byts, err := medias.Marshal(false).Marshal()
+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)),
+	}
+
+	for i, media := range medias {
+		sout.MediaDescriptions[i] = media.Marshal()
+	}
+
+	byts, err := sout.Marshal()
 	if err != nil {
 		panic(err)
 	}
@@ -53,7 +80,7 @@ func mustMarshalPacketRTCP(pkt rtcp.Packet) []byte {
 	return byts
 }
 
-func readAll(c *Client, ur string, cb func(*media.Media, format.Format, *rtp.Packet)) error {
+func readAll(c *Client, ur string, cb func(*description.Media, format.Format, *rtp.Packet)) error {
 	u, err := url.Parse(ur)
 	if err != nil {
 		return err
@@ -64,13 +91,13 @@ func readAll(c *Client, ur string, cb func(*media.Media, format.Format, *rtp.Pac
 		return err
 	}
 
-	medias, baseURL, _, err := c.Describe(u)
+	sd, _, err := c.Describe(u)
 	if err != nil {
 		c.Close()
 		return err
 	}
 
-	err = c.SetupAll(baseURL, medias)
+	err = c.SetupAll(sd.BaseURL, sd.Medias)
 	if err != nil {
 		c.Close()
 		return err
@@ -92,8 +119,8 @@ func readAll(c *Client, ur string, cb func(*media.Media, format.Format, *rtp.Pac
 func TestClientPlayFormats(t *testing.T) {
 	media1 := testH264Media
 
-	media2 := &media.Media{
-		Type: media.TypeAudio,
+	media2 := &description.Media{
+		Type: description.MediaTypeAudio,
 		Formats: []format.Format{&format.MPEG4Audio{
 			PayloadTyp: 96,
 			Config: &mpeg4audio.Config{
@@ -107,8 +134,8 @@ func TestClientPlayFormats(t *testing.T) {
 		}},
 	}
 
-	media3 := &media.Media{
-		Type: media.TypeAudio,
+	media3 := &description.Media{
+		Type: description.MediaTypeAudio,
 		Formats: []format.Format{&format.MPEG4Audio{
 			PayloadTyp: 96,
 			Config: &mpeg4audio.Config{
@@ -157,8 +184,7 @@ func TestClientPlayFormats(t *testing.T) {
 		require.Equal(t, base.Describe, req.Method)
 		require.Equal(t, mustParseURL("rtsp://localhost:8554/teststream"), req.URL)
 
-		medias := media.Medias{media1, media2, media3}
-		resetMediaControls(medias)
+		medias := []*description.Media{media1, media2, media3}
 
 		err = conn.WriteResponse(&base.Response{
 			StatusCode: base.StatusOK,
@@ -166,7 +192,7 @@ func TestClientPlayFormats(t *testing.T) {
 				"Content-Type": base.HeaderValue{"application/sdp"},
 				"Content-Base": base.HeaderValue{"rtsp://localhost:8554/teststream/"},
 			},
-			Body: mustMarshalMedias(medias),
+			Body: mediasToSDP(medias),
 		})
 		require.NoError(t, err)
 
@@ -293,17 +319,16 @@ func TestClientPlay(t *testing.T) {
 				err = forma.Init()
 				require.NoError(t, err)
 
-				medias := media.Medias{
-					&media.Media{
+				medias := []*description.Media{
+					{
 						Type:    "application",
 						Formats: []format.Format{forma},
 					},
-					&media.Media{
+					{
 						Type:    "application",
 						Formats: []format.Format{forma},
 					},
 				}
-				resetMediaControls(medias)
 
 				err = conn.WriteResponse(&base.Response{
 					StatusCode: base.StatusOK,
@@ -311,7 +336,7 @@ func TestClientPlay(t *testing.T) {
 						"Content-Type": base.HeaderValue{"application/sdp"},
 						"Content-Base": base.HeaderValue{scheme + "://" + listenIP + ":8554/test/stream?param=value/"},
 					},
-					Body: mustMarshalMedias(medias),
+					Body: mediasToSDP(medias),
 				})
 				require.NoError(t, err)
 
@@ -505,13 +530,13 @@ func TestClientPlay(t *testing.T) {
 			require.NoError(t, err)
 			defer c.Close()
 
-			medias, baseURL, _, err := c.Describe(u)
+			sd, _, err := c.Describe(u)
 			require.NoError(t, err)
 
-			err = c.SetupAll(baseURL, medias)
+			err = c.SetupAll(sd.BaseURL, sd.Medias)
 			require.NoError(t, err)
 
-			c.OnPacketRTPAny(func(medi *media.Media, forma format.Format, pkt *rtp.Packet) {
+			c.OnPacketRTPAny(func(medi *description.Media, forma format.Format, pkt *rtp.Packet) {
 				require.Equal(t, &testRTPPacket, pkt)
 				err := c.WritePacketRTCP(medi, &testRTCPPacket)
 				require.NoError(t, err)
@@ -569,17 +594,16 @@ func TestClientPlayPartial(t *testing.T) {
 		err = forma.Init()
 		require.NoError(t, err)
 
-		medias := media.Medias{
-			&media.Media{
+		medias := []*description.Media{
+			{
 				Type:    "application",
 				Formats: []format.Format{forma},
 			},
-			&media.Media{
+			{
 				Type:    "application",
 				Formats: []format.Format{forma},
 			},
 		}
-		resetMediaControls(medias)
 
 		err = conn.WriteResponse(&base.Response{
 			StatusCode: base.StatusOK,
@@ -587,7 +611,7 @@ func TestClientPlayPartial(t *testing.T) {
 				"Content-Type": base.HeaderValue{"application/sdp"},
 				"Content-Base": base.HeaderValue{"rtsp://" + listenIP + ":8554/teststream/"},
 			},
-			Body: mustMarshalMedias(medias),
+			Body: mediasToSDP(medias),
 		})
 		require.NoError(t, err)
 
@@ -661,15 +685,15 @@ func TestClientPlayPartial(t *testing.T) {
 	require.NoError(t, err)
 	defer c.Close()
 
-	medias, baseURL, _, err := c.Describe(u)
+	sd, _, err := c.Describe(u)
 	require.NoError(t, err)
 
-	_, err = c.Setup(baseURL, medias[1], 0, 0)
+	_, err = c.Setup(sd.BaseURL, sd.Medias[1], 0, 0)
 	require.NoError(t, err)
 
-	c.OnPacketRTPAny(func(medi *media.Media, forma format.Format, pkt *rtp.Packet) {
-		require.Equal(t, medias[1], medi)
-		require.Equal(t, medias[1].Formats[0], forma)
+	c.OnPacketRTPAny(func(medi *description.Media, forma format.Format, pkt *rtp.Packet) {
+		require.Equal(t, sd.Medias[1], medi)
+		require.Equal(t, sd.Medias[1].Formats[0], forma)
 		require.Equal(t, &testRTPPacket, pkt)
 		close(packetRecv)
 	})
@@ -721,8 +745,7 @@ func TestClientPlayContentBase(t *testing.T) {
 				require.Equal(t, base.Describe, req.Method)
 				require.Equal(t, mustParseURL("rtsp://localhost:8554/teststream"), req.URL)
 
-				medias := media.Medias{testH264Media}
-				resetMediaControls(medias)
+				medias := []*description.Media{testH264Media}
 
 				switch ca {
 				case "absent":
@@ -731,12 +754,12 @@ func TestClientPlayContentBase(t *testing.T) {
 						Header: base.Header{
 							"Content-Type": base.HeaderValue{"application/sdp"},
 						},
-						Body: mustMarshalMedias(medias),
+						Body: mediasToSDP(medias),
 					})
 					require.NoError(t, err)
 
 				case "inside control attribute":
-					body := string(mustMarshalMedias(medias))
+					body := string(mediasToSDP(medias))
 					body = strings.Replace(body, "t=0 0", "t=0 0\r\na=control:rtsp://localhost:8554/teststream", 1)
 
 					err = conn.WriteResponse(&base.Response{
@@ -851,8 +874,7 @@ func TestClientPlayAnyPort(t *testing.T) {
 				require.NoError(t, err)
 				require.Equal(t, base.Describe, req.Method)
 
-				medias := media.Medias{testH264Media}
-				resetMediaControls(medias)
+				medias := []*description.Media{testH264Media}
 
 				err = conn.WriteResponse(&base.Response{
 					StatusCode: base.StatusOK,
@@ -860,7 +882,7 @@ func TestClientPlayAnyPort(t *testing.T) {
 						"Content-Type": base.HeaderValue{"application/sdp"},
 						"Content-Base": base.HeaderValue{"rtsp://localhost:8554/teststream/"},
 					},
-					Body: mustMarshalMedias(medias),
+					Body: mediasToSDP(medias),
 				})
 				require.NoError(t, err)
 
@@ -950,9 +972,9 @@ func TestClientPlayAnyPort(t *testing.T) {
 				AnyPortEnable: true,
 			}
 
-			var med *media.Media
+			var med *description.Media
 			err = readAll(&c, "rtsp://localhost:8554/teststream",
-				func(medi *media.Media, forma format.Format, pkt *rtp.Packet) {
+				func(medi *description.Media, forma format.Format, pkt *rtp.Packet) {
 					require.Equal(t, &testRTPPacket, pkt)
 					med = medi
 					close(packetRecv)
@@ -1007,8 +1029,7 @@ func TestClientPlayAutomaticProtocol(t *testing.T) {
 			require.NoError(t, err)
 			require.Equal(t, base.Describe, req.Method)
 
-			medias := media.Medias{testH264Media}
-			resetMediaControls(medias)
+			medias := []*description.Media{testH264Media}
 
 			err = conn.WriteResponse(&base.Response{
 				StatusCode: base.StatusOK,
@@ -1016,7 +1037,7 @@ func TestClientPlayAutomaticProtocol(t *testing.T) {
 					"Content-Type": base.HeaderValue{"application/sdp"},
 					"Content-Base": base.HeaderValue{"rtsp://localhost:8554/teststream/"},
 				},
-				Body: mustMarshalMedias(medias),
+				Body: mediasToSDP(medias),
 			})
 			require.NoError(t, err)
 
@@ -1080,7 +1101,7 @@ func TestClientPlayAutomaticProtocol(t *testing.T) {
 		}
 
 		err = readAll(&c, "rtsp://localhost:8554/teststream",
-			func(medi *media.Media, forma format.Format, pkt *rtp.Packet) {
+			func(medi *description.Media, forma format.Format, pkt *rtp.Packet) {
 				close(packetRecv)
 			})
 		require.NoError(t, err)
@@ -1100,8 +1121,7 @@ func TestClientPlayAutomaticProtocol(t *testing.T) {
 		go func() {
 			defer close(serverDone)
 
-			medias := media.Medias{testH264Media}
-			resetMediaControls(medias)
+			medias := []*description.Media{testH264Media}
 
 			func() {
 				nconn, err := l.Accept()
@@ -1135,7 +1155,7 @@ func TestClientPlayAutomaticProtocol(t *testing.T) {
 						"Content-Type": base.HeaderValue{"application/sdp"},
 						"Content-Base": base.HeaderValue{"rtsp://localhost:8554/teststream/"},
 					},
-					Body: mustMarshalMedias(medias),
+					Body: mediasToSDP(medias),
 				})
 				require.NoError(t, err)
 
@@ -1204,7 +1224,7 @@ func TestClientPlayAutomaticProtocol(t *testing.T) {
 						"Content-Type": base.HeaderValue{"application/sdp"},
 						"Content-Base": base.HeaderValue{"rtsp://localhost:8554/teststream/"},
 					},
-					Body: mustMarshalMedias(medias),
+					Body: mediasToSDP(medias),
 				})
 				require.NoError(t, err)
 
@@ -1260,7 +1280,7 @@ func TestClientPlayAutomaticProtocol(t *testing.T) {
 		}
 
 		err = readAll(&c, "rtsp://localhost:8554/teststream",
-			func(medi *media.Media, forma format.Format, pkt *rtp.Packet) {
+			func(medi *description.Media, forma format.Format, pkt *rtp.Packet) {
 				close(packetRecv)
 			})
 		require.NoError(t, err)
@@ -1280,8 +1300,7 @@ func TestClientPlayAutomaticProtocol(t *testing.T) {
 		go func() {
 			defer close(serverDone)
 
-			medias := media.Medias{testH264Media}
-			resetMediaControls(medias)
+			medias := []*description.Media{testH264Media}
 
 			func() {
 				nconn, err := l.Accept()
@@ -1333,7 +1352,7 @@ func TestClientPlayAutomaticProtocol(t *testing.T) {
 						"Content-Type": base.HeaderValue{"application/sdp"},
 						"Content-Base": base.HeaderValue{"rtsp://localhost:8554/teststream/"},
 					},
-					Body: mustMarshalMedias(medias),
+					Body: mediasToSDP(medias),
 				})
 				require.NoError(t, err)
 
@@ -1415,7 +1434,7 @@ func TestClientPlayAutomaticProtocol(t *testing.T) {
 						"Content-Type": base.HeaderValue{"application/sdp"},
 						"Content-Base": base.HeaderValue{"rtsp://localhost:8554/teststream/"},
 					},
-					Body: mustMarshalMedias(medias),
+					Body: mediasToSDP(medias),
 				})
 				require.NoError(t, err)
 
@@ -1501,7 +1520,7 @@ func TestClientPlayAutomaticProtocol(t *testing.T) {
 		}
 
 		err = readAll(&c, "rtsp://myuser:mypass@localhost:8554/teststream",
-			func(medi *media.Media, forma format.Format, pkt *rtp.Packet) {
+			func(medi *description.Media, forma format.Format, pkt *rtp.Packet) {
 				close(packetRecv)
 			})
 		require.NoError(t, err)
@@ -1548,8 +1567,7 @@ func TestClientPlayDifferentInterleavedIDs(t *testing.T) {
 		require.Equal(t, base.Describe, req.Method)
 		require.Equal(t, mustParseURL("rtsp://localhost:8554/teststream"), req.URL)
 
-		medias := media.Medias{testH264Media}
-		resetMediaControls(medias)
+		medias := []*description.Media{testH264Media}
 
 		err = conn.WriteResponse(&base.Response{
 			StatusCode: base.StatusOK,
@@ -1557,7 +1575,7 @@ func TestClientPlayDifferentInterleavedIDs(t *testing.T) {
 				"Content-Type": base.HeaderValue{"application/sdp"},
 				"Content-Base": base.HeaderValue{"rtsp://localhost:8554/teststream/"},
 			},
-			Body: mustMarshalMedias(medias),
+			Body: mediasToSDP(medias),
 		})
 		require.NoError(t, err)
 
@@ -1624,7 +1642,7 @@ func TestClientPlayDifferentInterleavedIDs(t *testing.T) {
 	}
 
 	err = readAll(&c, "rtsp://localhost:8554/teststream",
-		func(medi *media.Media, forma format.Format, pkt *rtp.Packet) {
+		func(medi *description.Media, forma format.Format, pkt *rtp.Packet) {
 			close(packetRecv)
 		})
 	require.NoError(t, err)
@@ -1743,8 +1761,7 @@ func TestClientPlayRedirect(t *testing.T) {
 						require.Equal(t, base.Describe, req.Method)
 					}
 
-					medias := media.Medias{testH264Media}
-					resetMediaControls(medias)
+					medias := []*description.Media{testH264Media}
 
 					err = conn.WriteResponse(&base.Response{
 						StatusCode: base.StatusOK,
@@ -1752,7 +1769,7 @@ func TestClientPlayRedirect(t *testing.T) {
 							"Content-Type": base.HeaderValue{"application/sdp"},
 							"Content-Base": base.HeaderValue{"rtsp://localhost:8554/teststream/"},
 						},
-						Body: mustMarshalMedias(medias),
+						Body: mediasToSDP(medias),
 					})
 					require.NoError(t, err)
 
@@ -1812,7 +1829,7 @@ func TestClientPlayRedirect(t *testing.T) {
 				ru = "rtsp://testusr:testpwd@localhost:8554/path1"
 			}
 			err = readAll(&c, ru,
-				func(medi *media.Media, forma format.Format, pkt *rtp.Packet) {
+				func(medi *description.Media, forma format.Format, pkt *rtp.Packet) {
 					close(packetRecv)
 				})
 			require.NoError(t, err)
@@ -1907,8 +1924,7 @@ func TestClientPlayPause(t *testing.T) {
 				require.NoError(t, err)
 				require.Equal(t, base.Describe, req.Method)
 
-				medias := media.Medias{testH264Media}
-				resetMediaControls(medias)
+				medias := []*description.Media{testH264Media}
 
 				err = conn.WriteResponse(&base.Response{
 					StatusCode: base.StatusOK,
@@ -1916,7 +1932,7 @@ func TestClientPlayPause(t *testing.T) {
 						"Content-Type": base.HeaderValue{"application/sdp"},
 						"Content-Base": base.HeaderValue{"rtsp://localhost:8554/teststream/"},
 					},
-					Body: mustMarshalMedias(medias),
+					Body: mediasToSDP(medias),
 				})
 				require.NoError(t, err)
 
@@ -2014,7 +2030,7 @@ func TestClientPlayPause(t *testing.T) {
 			}
 
 			err = readAll(&c, "rtsp://localhost:8554/teststream",
-				func(medi *media.Media, forma format.Format, pkt *rtp.Packet) {
+				func(medi *description.Media, forma format.Format, pkt *rtp.Packet) {
 					if atomic.SwapInt32(&firstFrame, 1) == 0 {
 						close(packetRecv)
 					}
@@ -2075,8 +2091,7 @@ func TestClientPlayRTCPReport(t *testing.T) {
 		require.NoError(t, err)
 		require.Equal(t, base.Describe, req.Method)
 
-		medias := media.Medias{testH264Media}
-		resetMediaControls(medias)
+		medias := []*description.Media{testH264Media}
 
 		err = conn.WriteResponse(&base.Response{
 			StatusCode: base.StatusOK,
@@ -2084,7 +2099,7 @@ func TestClientPlayRTCPReport(t *testing.T) {
 				"Content-Type": base.HeaderValue{"application/sdp"},
 				"Content-Base": base.HeaderValue{"rtsp://localhost:8554/teststream/"},
 			},
-			Body: mustMarshalMedias(medias),
+			Body: mediasToSDP(medias),
 		})
 		require.NoError(t, err)
 
@@ -2249,8 +2264,7 @@ func TestClientPlayErrorTimeout(t *testing.T) {
 				require.NoError(t, err)
 				require.Equal(t, base.Describe, req.Method)
 
-				medias := media.Medias{testH264Media}
-				resetMediaControls(medias)
+				medias := []*description.Media{testH264Media}
 
 				err = conn.WriteResponse(&base.Response{
 					StatusCode: base.StatusOK,
@@ -2258,7 +2272,7 @@ func TestClientPlayErrorTimeout(t *testing.T) {
 						"Content-Type": base.HeaderValue{"application/sdp"},
 						"Content-Base": base.HeaderValue{"rtsp://localhost:8554/teststream/"},
 					},
-					Body: mustMarshalMedias(medias),
+					Body: mediasToSDP(medias),
 				})
 				require.NoError(t, err)
 
@@ -2396,8 +2410,7 @@ func TestClientPlayIgnoreTCPInvalidMedia(t *testing.T) {
 		require.NoError(t, err)
 		require.Equal(t, base.Describe, req.Method)
 
-		medias := media.Medias{testH264Media}
-		resetMediaControls(medias)
+		medias := []*description.Media{testH264Media}
 
 		err = conn.WriteResponse(&base.Response{
 			StatusCode: base.StatusOK,
@@ -2405,7 +2418,7 @@ func TestClientPlayIgnoreTCPInvalidMedia(t *testing.T) {
 				"Content-Type": base.HeaderValue{"application/sdp"},
 				"Content-Base": base.HeaderValue{"rtsp://localhost:8554/teststream/"},
 			},
-			Body: mustMarshalMedias(medias),
+			Body: mediasToSDP(medias),
 		})
 		require.NoError(t, err)
 
@@ -2475,7 +2488,7 @@ func TestClientPlayIgnoreTCPInvalidMedia(t *testing.T) {
 	}
 
 	err = readAll(&c, "rtsp://localhost:8554/teststream",
-		func(medi *media.Media, forma format.Format, pkt *rtp.Packet) {
+		func(medi *description.Media, forma format.Format, pkt *rtp.Packet) {
 			close(recv)
 		})
 	require.NoError(t, err)
@@ -2519,8 +2532,7 @@ func TestClientPlaySeek(t *testing.T) {
 		require.NoError(t, err)
 		require.Equal(t, base.Describe, req.Method)
 
-		medias := media.Medias{testH264Media}
-		resetMediaControls(medias)
+		medias := []*description.Media{testH264Media}
 
 		err = conn.WriteResponse(&base.Response{
 			StatusCode: base.StatusOK,
@@ -2528,7 +2540,7 @@ func TestClientPlaySeek(t *testing.T) {
 				"Content-Type": base.HeaderValue{"application/sdp"},
 				"Content-Base": base.HeaderValue{"rtsp://localhost:8554/teststream/"},
 			},
-			Body: mustMarshalMedias(medias),
+			Body: mediasToSDP(medias),
 		})
 		require.NoError(t, err)
 
@@ -2625,10 +2637,10 @@ func TestClientPlaySeek(t *testing.T) {
 	require.NoError(t, err)
 	defer c.Close()
 
-	medias, baseURL, _, err := c.Describe(u)
+	sd, _, err := c.Describe(u)
 	require.NoError(t, err)
 
-	err = c.SetupAll(baseURL, medias)
+	err = c.SetupAll(sd.BaseURL, sd.Medias)
 	require.NoError(t, err)
 
 	_, err = c.Play(&headers.Range{
@@ -2683,8 +2695,7 @@ func TestClientPlayKeepaliveFromSession(t *testing.T) {
 		require.NoError(t, err)
 		require.Equal(t, base.Describe, req.Method)
 
-		medias := media.Medias{testH264Media}
-		resetMediaControls(medias)
+		medias := []*description.Media{testH264Media}
 
 		err = conn.WriteResponse(&base.Response{
 			StatusCode: base.StatusOK,
@@ -2692,7 +2703,7 @@ func TestClientPlayKeepaliveFromSession(t *testing.T) {
 				"Content-Type": base.HeaderValue{"application/sdp"},
 				"Content-Base": base.HeaderValue{"rtsp://localhost:8554/teststream/"},
 			},
-			Body: mustMarshalMedias(medias),
+			Body: mediasToSDP(medias),
 		})
 		require.NoError(t, err)
 
@@ -2803,8 +2814,7 @@ func TestClientPlayDifferentSource(t *testing.T) {
 		require.Equal(t, base.Describe, req.Method)
 		require.Equal(t, mustParseURL("rtsp://localhost:8554/test/stream?param=value"), req.URL)
 
-		medias := media.Medias{testH264Media}
-		resetMediaControls(medias)
+		medias := []*description.Media{testH264Media}
 
 		err = conn.WriteResponse(&base.Response{
 			StatusCode: base.StatusOK,
@@ -2812,7 +2822,7 @@ func TestClientPlayDifferentSource(t *testing.T) {
 				"Content-Type": base.HeaderValue{"application/sdp"},
 				"Content-Base": base.HeaderValue{"rtsp://localhost:8554/test/stream?param=value/"},
 			},
-			Body: mustMarshalMedias(medias),
+			Body: mediasToSDP(medias),
 		})
 		require.NoError(t, err)
 
@@ -2888,7 +2898,7 @@ func TestClientPlayDifferentSource(t *testing.T) {
 	}
 
 	err = readAll(&c, "rtsp://localhost:8554/test/stream?param=value",
-		func(medi *media.Media, forma format.Format, pkt *rtp.Packet) {
+		func(medi *description.Media, forma format.Format, pkt *rtp.Packet) {
 			require.Equal(t, &testRTPPacket, pkt)
 			close(packetRecv)
 		})
@@ -2953,14 +2963,13 @@ func TestClientPlayDecodeErrors(t *testing.T) {
 				require.NoError(t, err)
 				require.Equal(t, base.Describe, req.Method)
 
-				medias := media.Medias{&media.Media{
-					Type: media.TypeApplication,
+				medias := []*description.Media{{
+					Type: description.MediaTypeApplication,
 					Formats: []format.Format{&format.Generic{
 						PayloadTyp: 97,
 						RTPMa:      "private/90000",
 					}},
 				}}
-				resetMediaControls(medias)
 
 				err = conn.WriteResponse(&base.Response{
 					StatusCode: base.StatusOK,
@@ -2968,7 +2977,7 @@ func TestClientPlayDecodeErrors(t *testing.T) {
 						"Content-Type": base.HeaderValue{"application/sdp"},
 						"Content-Base": base.HeaderValue{"rtsp://localhost:8554/stream/"},
 					},
-					Body: mustMarshalMedias(medias),
+					Body: mediasToSDP(medias),
 				})
 				require.NoError(t, err)
 
@@ -3217,8 +3226,7 @@ func TestClientPlayPacketNTP(t *testing.T) {
 		require.NoError(t, err)
 		require.Equal(t, base.Describe, req.Method)
 
-		medias := media.Medias{testH264Media}
-		resetMediaControls(medias)
+		medias := []*description.Media{testH264Media}
 
 		err = conn.WriteResponse(&base.Response{
 			StatusCode: base.StatusOK,
@@ -3226,7 +3234,7 @@ func TestClientPlayPacketNTP(t *testing.T) {
 				"Content-Type": base.HeaderValue{"application/sdp"},
 				"Content-Base": base.HeaderValue{"rtsp://localhost:8554/teststream/"},
 			},
-			Body: mustMarshalMedias(medias),
+			Body: mediasToSDP(medias),
 		})
 		require.NoError(t, err)
 
@@ -3341,7 +3349,7 @@ func TestClientPlayPacketNTP(t *testing.T) {
 	first := false
 
 	err = readAll(&c, "rtsp://localhost:8554/teststream",
-		func(medi *media.Media, forma format.Format, pkt *rtp.Packet) {
+		func(medi *description.Media, forma format.Format, pkt *rtp.Packet) {
 			if !first {
 				first = true
 			} else {
diff --git a/client_record_test.go b/client_record_test.go
index 02251942..34015d8e 100644
--- a/client_record_test.go
+++ b/client_record_test.go
@@ -15,15 +15,15 @@ import (
 
 	"github.com/bluenviron/gortsplib/v4/pkg/base"
 	"github.com/bluenviron/gortsplib/v4/pkg/conn"
+	"github.com/bluenviron/gortsplib/v4/pkg/description"
 	"github.com/bluenviron/gortsplib/v4/pkg/format"
 	"github.com/bluenviron/gortsplib/v4/pkg/headers"
-	"github.com/bluenviron/gortsplib/v4/pkg/media"
 	"github.com/bluenviron/gortsplib/v4/pkg/sdp"
 	"github.com/bluenviron/gortsplib/v4/pkg/url"
 )
 
-var testH264Media = &media.Media{
-	Type: media.TypeVideo,
+var testH264Media = &description.Media{
+	Type: description.MediaTypeVideo,
 	Formats: []format.Format{&format.H264{
 		PayloadTyp:        96,
 		SPS:               []byte{0x01, 0x02, 0x03, 0x04},
@@ -65,7 +65,7 @@ func ntpTimeGoToRTCP(v time.Time) uint64 {
 	return (s/1000000000)<<32 | (s % 1000000000)
 }
 
-func record(c *Client, ur string, medias media.Medias, cb func(*media.Media, rtcp.Packet)) error {
+func record(c *Client, ur string, medias []*description.Media, cb func(*description.Media, rtcp.Packet)) error {
 	u, err := url.Parse(ur)
 	if err != nil {
 		return err
@@ -76,7 +76,7 @@ func record(c *Client, ur string, medias media.Medias, cb func(*media.Media, rtc
 		return err
 	}
 
-	_, err = c.Announce(u, medias)
+	_, err = c.Announce(u, &description.Session{Medias: medias})
 	if err != nil {
 		c.Close()
 		return err
@@ -285,10 +285,10 @@ func TestClientRecordSerial(t *testing.T) {
 			}
 
 			medi := testH264Media
-			medias := media.Medias{medi}
+			medias := []*description.Media{medi}
 
 			err = record(&c, scheme+"://localhost:8554/teststream", medias,
-				func(medi *media.Media, pkt rtcp.Packet) {
+				func(medi *description.Media, pkt rtcp.Packet) {
 					require.Equal(t, &testRTCPPacket, pkt)
 					close(recvDone)
 				})
@@ -440,7 +440,7 @@ func TestClientRecordParallel(t *testing.T) {
 			defer func() { <-writerDone }()
 
 			medi := testH264Media
-			medias := media.Medias{medi}
+			medias := []*description.Media{medi}
 
 			err = record(&c, scheme+"://localhost:8554/teststream", medias, nil)
 			require.NoError(t, err)
@@ -592,7 +592,7 @@ func TestClientRecordPauseSerial(t *testing.T) {
 			}
 
 			medi := testH264Media
-			medias := media.Medias{medi}
+			medias := []*description.Media{medi}
 
 			err = record(&c, "rtsp://localhost:8554/teststream", medias, nil)
 			require.NoError(t, err)
@@ -722,7 +722,7 @@ func TestClientRecordPauseParallel(t *testing.T) {
 			}
 
 			medi := testH264Media
-			medias := media.Medias{medi}
+			medias := []*description.Media{medi}
 
 			err = record(&c, "rtsp://localhost:8554/teststream", medias, nil)
 			require.NoError(t, err)
@@ -861,7 +861,7 @@ func TestClientRecordAutomaticProtocol(t *testing.T) {
 	c := Client{}
 
 	medi := testH264Media
-	medias := media.Medias{medi}
+	medias := []*description.Media{medi}
 
 	err = record(&c, "rtsp://localhost:8554/teststream", medias, nil)
 	require.NoError(t, err)
@@ -1041,7 +1041,7 @@ func TestClientRecordDecodeErrors(t *testing.T) {
 				},
 			}
 
-			medias := media.Medias{testH264Media}
+			medias := []*description.Media{testH264Media}
 
 			err = record(&c, "rtsp://localhost:8554/stream", medias, nil)
 			require.NoError(t, err)
@@ -1207,7 +1207,7 @@ func TestClientRecordRTCPReport(t *testing.T) {
 			}
 
 			medi := testH264Media
-			medias := media.Medias{medi}
+			medias := []*description.Media{medi}
 
 			err = record(&c, "rtsp://localhost:8554/teststream", medias, nil)
 			require.NoError(t, err)
@@ -1345,10 +1345,10 @@ func TestClientRecordIgnoreTCPRTPPackets(t *testing.T) {
 		}(),
 	}
 
-	medias := media.Medias{testH264Media}
+	medias := []*description.Media{testH264Media}
 
 	err = record(&c, "rtsp://localhost:8554/teststream", medias,
-		func(medi *media.Media, pkt rtcp.Packet) {
+		func(medi *description.Media, pkt rtcp.Packet) {
 			close(rtcpReceived)
 		})
 	require.NoError(t, err)
diff --git a/client_test.go b/client_test.go
index 459ff933..24c98bea 100644
--- a/client_test.go
+++ b/client_test.go
@@ -11,7 +11,7 @@ import (
 	"github.com/bluenviron/gortsplib/v4/pkg/auth"
 	"github.com/bluenviron/gortsplib/v4/pkg/base"
 	"github.com/bluenviron/gortsplib/v4/pkg/conn"
-	"github.com/bluenviron/gortsplib/v4/pkg/media"
+	"github.com/bluenviron/gortsplib/v4/pkg/description"
 	"github.com/bluenviron/gortsplib/v4/pkg/url"
 )
 
@@ -134,8 +134,7 @@ func TestClientSession(t *testing.T) {
 		require.Equal(t, base.Describe, req.Method)
 		require.Equal(t, base.HeaderValue{"123456"}, req.Header["Session"])
 
-		medias := media.Medias{testH264Media}
-		resetMediaControls(medias)
+		medias := []*description.Media{testH264Media}
 
 		err = conn.WriteResponse(&base.Response{
 			StatusCode: base.StatusOK,
@@ -143,7 +142,7 @@ func TestClientSession(t *testing.T) {
 				"Content-Type": base.HeaderValue{"application/sdp"},
 				"Session":      base.HeaderValue{"123456"},
 			},
-			Body: mustMarshalMedias(medias),
+			Body: mediasToSDP(medias),
 		})
 		require.NoError(t, err)
 	}()
@@ -157,7 +156,7 @@ func TestClientSession(t *testing.T) {
 	require.NoError(t, err)
 	defer c.Close()
 
-	_, _, _, err = c.Describe(u)
+	_, _, err = c.Describe(u)
 	require.NoError(t, err)
 }
 
@@ -212,15 +211,14 @@ func TestClientAuth(t *testing.T) {
 		err = auth.Validate(req, "myuser", "mypass", nil, nil, "IPCAM", nonce)
 		require.NoError(t, err)
 
-		medias := media.Medias{testH264Media}
-		resetMediaControls(medias)
+		medias := []*description.Media{testH264Media}
 
 		err = conn.WriteResponse(&base.Response{
 			StatusCode: base.StatusOK,
 			Header: base.Header{
 				"Content-Type": base.HeaderValue{"application/sdp"},
 			},
-			Body: mustMarshalMedias(medias),
+			Body: mediasToSDP(medias),
 		})
 		require.NoError(t, err)
 	}()
@@ -234,7 +232,7 @@ func TestClientAuth(t *testing.T) {
 	require.NoError(t, err)
 	defer c.Close()
 
-	_, _, _, err = c.Describe(u)
+	_, _, err = c.Describe(u)
 	require.NoError(t, err)
 }
 
@@ -272,7 +270,7 @@ func TestClientDescribeCharset(t *testing.T) {
 		require.Equal(t, base.Describe, req.Method)
 		require.Equal(t, mustParseURL("rtsp://localhost:8554/teststream"), req.URL)
 
-		medias := media.Medias{testH264Media}
+		medias := []*description.Media{testH264Media}
 
 		err = conn.WriteResponse(&base.Response{
 			StatusCode: base.StatusOK,
@@ -280,7 +278,7 @@ func TestClientDescribeCharset(t *testing.T) {
 				"Content-Type": base.HeaderValue{"application/sdp; charset=utf-8"},
 				"Content-Base": base.HeaderValue{"rtsp://localhost:8554/teststream/"},
 			},
-			Body: mustMarshalMedias(medias),
+			Body: mediasToSDP(medias),
 		})
 		require.NoError(t, err)
 	}()
@@ -294,7 +292,7 @@ func TestClientDescribeCharset(t *testing.T) {
 	require.NoError(t, err)
 	defer c.Close()
 
-	_, _, _, err = c.Describe(u)
+	_, _, err = c.Describe(u)
 	require.NoError(t, err)
 }
 
@@ -312,7 +310,7 @@ func TestClientClose(t *testing.T) {
 	_, err = c.Options(u)
 	require.EqualError(t, err, "terminated")
 
-	_, _, _, err = c.Describe(u)
+	_, _, err = c.Describe(u)
 	require.EqualError(t, err, "terminated")
 
 	_, err = c.Announce(u, nil)
diff --git a/internal/highleveltests/server_test.go b/internal/highleveltests/server_test.go
index a7a5b484..91bb2a37 100644
--- a/internal/highleveltests/server_test.go
+++ b/internal/highleveltests/server_test.go
@@ -19,8 +19,8 @@ import (
 
 	"github.com/bluenviron/gortsplib/v4"
 	"github.com/bluenviron/gortsplib/v4/pkg/base"
+	"github.com/bluenviron/gortsplib/v4/pkg/description"
 	"github.com/bluenviron/gortsplib/v4/pkg/format"
-	"github.com/bluenviron/gortsplib/v4/pkg/media"
 )
 
 var serverCert = []byte(`-----BEGIN CERTIFICATE-----
@@ -329,7 +329,7 @@ func TestServerRecordRead(t *testing.T) {
 							}, fmt.Errorf("someone is already publishing")
 						}
 
-						stream = gortsplib.NewServerStream(s, ctx.Medias)
+						stream = gortsplib.NewServerStream(s, ctx.Description)
 						publisher = ctx.Session
 
 						return &base.Response{
@@ -386,7 +386,7 @@ func TestServerRecordRead(t *testing.T) {
 							}, fmt.Errorf("invalid query (%s)", ctx.Query)
 						}
 
-						ctx.Session.OnPacketRTPAny(func(medi *media.Media, forma format.Format, pkt *rtp.Packet) {
+						ctx.Session.OnPacketRTPAny(func(medi *description.Media, forma format.Format, pkt *rtp.Packet) {
 							stream.WritePacketRTP(medi, pkt)
 						})
 
diff --git a/pkg/media/media_test.go b/pkg/description/media_test.go
similarity index 97%
rename from pkg/media/media_test.go
rename to pkg/description/media_test.go
index a2f3fcd3..2fa6ffd1 100644
--- a/pkg/media/media_test.go
+++ b/pkg/description/media_test.go
@@ -1,4 +1,4 @@
-package media
+package description
 
 import (
 	"testing"
@@ -133,11 +133,11 @@ func TestMediaURL(t *testing.T) {
 			err := sd.Unmarshal(ca.sdp)
 			require.NoError(t, err)
 
-			var medias Medias
-			err = medias.Unmarshal(sd.MediaDescriptions)
+			var media Media
+			err = media.Unmarshal(sd.MediaDescriptions[0])
 			require.NoError(t, err)
 
-			ur, err := medias[0].URL(ca.baseURL)
+			ur, err := media.URL(ca.baseURL)
 			require.NoError(t, err)
 			require.Equal(t, ca.ur, ur)
 		})
diff --git a/pkg/media/medias_test.go b/pkg/description/session_test.go
similarity index 66%
rename from pkg/media/medias_test.go
rename to pkg/description/session_test.go
index e31004c0..590d875b 100644
--- a/pkg/media/medias_test.go
+++ b/pkg/description/session_test.go
@@ -1,4 +1,4 @@
-package media
+package description
 
 import (
 	"testing"
@@ -9,11 +9,11 @@ import (
 	"github.com/bluenviron/gortsplib/v4/pkg/sdp"
 )
 
-var casesMedias = []struct {
-	name   string
-	in     string
-	out    string
-	medias Medias
+var casesSession = []struct {
+	name string
+	in   string
+	out  string
+	desc Session
 }{
 	{
 		"one format for each media, absolute",
@@ -43,7 +43,7 @@ var casesMedias = []struct {
 			"b=AS:8\r\n",
 		"v=0\r\n" +
 			"o=- 0 0 IN IP4 127.0.0.1\r\n" +
-			"s=Stream\r\n" +
+			"s=Session\r\n" +
 			"c=IN IP4 0.0.0.0\r\n" +
 			"t=0 0\r\n" +
 			"m=video 0 RTP/AVP 97\r\n" +
@@ -56,30 +56,32 @@ var casesMedias = []struct {
 			"a=rtpmap:0 PCMU/8000\r\n" +
 			"m=application 0 RTP/AVP 107\r\n" +
 			"a=control\r\n",
-		Medias{
-			{
-				Type:    "video",
-				Control: "rtsp://10.0.100.50/profile5/media.smp/trackID=v",
-				Formats: []format.Format{&format.H264{
-					PayloadTyp:        97,
-					PacketizationMode: 1,
-					SPS:               []byte{0x67, 0x64, 0x00, 0x28, 0xac, 0xb4, 0x03, 0xc0, 0x11, 0x3f, 0x2a},
-					PPS:               []byte{0x68, 0xee, 0x01, 0x9e, 0x2c},
-				}},
-			},
-			{
-				Type:      "audio",
-				Direction: DirectionRecvonly,
-				Control:   "rtsp://10.0.100.50/profile5/media.smp/trackID=a",
-				Formats: []format.Format{&format.G711{
-					MULaw: true,
-				}},
-			},
-			{
-				Type: "application",
-				Formats: []format.Format{&format.Generic{
-					PayloadTyp: 107,
-				}},
+		Session{
+			Medias: []*Media{
+				{
+					Type:    "video",
+					Control: "rtsp://10.0.100.50/profile5/media.smp/trackID=v",
+					Formats: []format.Format{&format.H264{
+						PayloadTyp:        97,
+						PacketizationMode: 1,
+						SPS:               []byte{0x67, 0x64, 0x00, 0x28, 0xac, 0xb4, 0x03, 0xc0, 0x11, 0x3f, 0x2a},
+						PPS:               []byte{0x68, 0xee, 0x01, 0x9e, 0x2c},
+					}},
+				},
+				{
+					Type:      "audio",
+					Direction: MediaDirectionRecvonly,
+					Control:   "rtsp://10.0.100.50/profile5/media.smp/trackID=a",
+					Formats: []format.Format{&format.G711{
+						MULaw: true,
+					}},
+				},
+				{
+					Type: "application",
+					Formats: []format.Format{&format.Generic{
+						PayloadTyp: 107,
+					}},
+				},
 			},
 		},
 	},
@@ -110,7 +112,7 @@ var casesMedias = []struct {
 			"b=AS:8\r\n",
 		"v=0\r\n" +
 			"o=- 0 0 IN IP4 127.0.0.1\r\n" +
-			"s=Stream\r\n" +
+			"s=Session\r\n" +
 			"c=IN IP4 0.0.0.0\r\n" +
 			"t=0 0\r\n" +
 			"m=video 0 RTP/AVP 97\r\n" +
@@ -123,30 +125,32 @@ var casesMedias = []struct {
 			"a=rtpmap:0 PCMU/8000\r\n" +
 			"m=application 0 RTP/AVP 107\r\n" +
 			"a=control\r\n",
-		Medias{
-			{
-				Type:    "video",
-				Control: "trackID=1",
-				Formats: []format.Format{&format.H264{
-					PayloadTyp:        97,
-					PacketizationMode: 1,
-					SPS:               []byte{0x67, 0x64, 0x00, 0x28, 0xac, 0xb4, 0x03, 0xc0, 0x11, 0x3f, 0x2a},
-					PPS:               []byte{0x68, 0xee, 0x01, 0x9e, 0x2c},
-				}},
-			},
-			{
-				Type:      "audio",
-				Direction: DirectionRecvonly,
-				Control:   "trackID=2",
-				Formats: []format.Format{&format.G711{
-					MULaw: true,
-				}},
-			},
-			{
-				Type: "application",
-				Formats: []format.Format{&format.Generic{
-					PayloadTyp: 107,
-				}},
+		Session{
+			Medias: []*Media{
+				{
+					Type:    "video",
+					Control: "trackID=1",
+					Formats: []format.Format{&format.H264{
+						PayloadTyp:        97,
+						PacketizationMode: 1,
+						SPS:               []byte{0x67, 0x64, 0x00, 0x28, 0xac, 0xb4, 0x03, 0xc0, 0x11, 0x3f, 0x2a},
+						PPS:               []byte{0x68, 0xee, 0x01, 0x9e, 0x2c},
+					}},
+				},
+				{
+					Type:      "audio",
+					Direction: MediaDirectionRecvonly,
+					Control:   "trackID=2",
+					Formats: []format.Format{&format.G711{
+						MULaw: true,
+					}},
+				},
+				{
+					Type: "application",
+					Formats: []format.Format{&format.Generic{
+						PayloadTyp: 107,
+					}},
+				},
 			},
 		},
 	},
@@ -157,7 +161,7 @@ var casesMedias = []struct {
 			"s=-\r\n" +
 			"t=0 0\r\n" +
 			"a=group:BUNDLE audio video\r\n" +
-			"a=msid-semantic: WMS mediaStreamLocal\r\n" +
+			"a=msid-semantic: WMS mediaSessionLocal\r\n" +
 			"m=audio 9 UDP/TLS/RTP/SAVPF 111 103 104 9 102 0 8 106 105 13 110 112 113 126\r\n" +
 			"c=IN IP4 0.0.0.0\r\n" +
 			"a=rtcp:9 IN IP4 0.0.0.0\r\n" +
@@ -190,8 +194,8 @@ var casesMedias = []struct {
 			"a=rtpmap:113 telephone-event/16000\r\n" +
 			"a=rtpmap:126 telephone-event/8000\r\n" +
 			"a=ssrc:3754810229 cname:CvU1TYqkVsjj5XOt\r\n" +
-			"a=ssrc:3754810229 msid:mediaStreamLocal 101\r\n" +
-			"a=ssrc:3754810229 mslabel:mediaStreamLocal\r\n" +
+			"a=ssrc:3754810229 msid:mediaSessionLocal 101\r\n" +
+			"a=ssrc:3754810229 mslabel:mediaSessionLocal\r\n" +
 			"a=ssrc:3754810229 label:101\r\n" +
 			"m=video 9 UDP/TLS/RTP/SAVPF 96 97 98 99 100 101 127 124 125\r\n" +
 			"c=IN IP4 0.0.0.0\r\n" +
@@ -245,16 +249,16 @@ var casesMedias = []struct {
 			"a=rtpmap:125 ulpfec/90000\r\n" +
 			"a=ssrc-group:FID 2712436124 1733091158\r\n" +
 			"a=ssrc:2712436124 cname:CvU1TYqkVsjj5XOt\r\n" +
-			"a=ssrc:2712436124 msid:mediaStreamLocal 100\r\n" +
-			"a=ssrc:2712436124 mslabel:mediaStreamLocal\r\n" +
+			"a=ssrc:2712436124 msid:mediaSessionLocal 100\r\n" +
+			"a=ssrc:2712436124 mslabel:mediaSessionLocal\r\n" +
 			"a=ssrc:2712436124 label:100\r\n" +
 			"a=ssrc:1733091158 cname:CvU1TYqkVsjj5XOt\r\n" +
-			"a=ssrc:1733091158 msid:mediaStreamLocal 100\r\n" +
-			"a=ssrc:1733091158 mslabel:mediaStreamLocal\r\n" +
+			"a=ssrc:1733091158 msid:mediaSessionLocal 100\r\n" +
+			"a=ssrc:1733091158 mslabel:mediaSessionLocal\r\n" +
 			"a=ssrc:1733091158 label:100\r\n",
 		"v=0\r\n" +
 			"o=- 0 0 IN IP4 127.0.0.1\r\n" +
-			"s=Stream\r\n" +
+			"s=Session\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" +
@@ -291,129 +295,131 @@ var casesMedias = []struct {
 			"a=rtpmap:127 red/90000\r\n" +
 			"a=rtpmap:124 rtx/90000\r\n" +
 			"a=fmtp:124 apt=127\r\na=rtpmap:125 ulpfec/90000\r\n",
-		Medias{
-			{
-				Type:      "audio",
-				Direction: DirectionSendonly,
-				Formats: []format.Format{
-					&format.Opus{
-						PayloadTyp: 111,
-						IsStereo:   false,
-					},
-					&format.Generic{
-						PayloadTyp: 103,
-						RTPMa:      "ISAC/16000",
-						ClockRat:   16000,
-					},
-					&format.Generic{
-						PayloadTyp: 104,
-						RTPMa:      "ISAC/32000",
-						ClockRat:   32000,
-					},
-					&format.G722{},
-					&format.Generic{
-						PayloadTyp: 102,
-						RTPMa:      "ILBC/8000",
-						ClockRat:   8000,
-					},
-					&format.G711{
-						MULaw: true,
-					},
-					&format.G711{
-						MULaw: false,
-					},
-					&format.Generic{
-						PayloadTyp: 106,
-						RTPMa:      "CN/32000",
-						ClockRat:   32000,
-					},
-					&format.Generic{
-						PayloadTyp: 105,
-						RTPMa:      "CN/16000",
-						ClockRat:   16000,
-					},
-					&format.Generic{
-						PayloadTyp: 13,
-						RTPMa:      "CN/8000",
-						ClockRat:   8000,
-					},
-					&format.Generic{
-						PayloadTyp: 110,
-						RTPMa:      "telephone-event/48000",
-						ClockRat:   48000,
-					},
-					&format.Generic{
-						PayloadTyp: 112,
-						RTPMa:      "telephone-event/32000",
-						ClockRat:   32000,
-					},
-					&format.Generic{
-						PayloadTyp: 113,
-						RTPMa:      "telephone-event/16000",
-						ClockRat:   16000,
-					},
-					&format.Generic{
-						PayloadTyp: 126,
-						RTPMa:      "telephone-event/8000",
-						ClockRat:   8000,
+		Session{
+			Medias: []*Media{
+				{
+					Type:      "audio",
+					Direction: MediaDirectionSendonly,
+					Formats: []format.Format{
+						&format.Opus{
+							PayloadTyp: 111,
+							IsStereo:   false,
+						},
+						&format.Generic{
+							PayloadTyp: 103,
+							RTPMa:      "ISAC/16000",
+							ClockRat:   16000,
+						},
+						&format.Generic{
+							PayloadTyp: 104,
+							RTPMa:      "ISAC/32000",
+							ClockRat:   32000,
+						},
+						&format.G722{},
+						&format.Generic{
+							PayloadTyp: 102,
+							RTPMa:      "ILBC/8000",
+							ClockRat:   8000,
+						},
+						&format.G711{
+							MULaw: true,
+						},
+						&format.G711{
+							MULaw: false,
+						},
+						&format.Generic{
+							PayloadTyp: 106,
+							RTPMa:      "CN/32000",
+							ClockRat:   32000,
+						},
+						&format.Generic{
+							PayloadTyp: 105,
+							RTPMa:      "CN/16000",
+							ClockRat:   16000,
+						},
+						&format.Generic{
+							PayloadTyp: 13,
+							RTPMa:      "CN/8000",
+							ClockRat:   8000,
+						},
+						&format.Generic{
+							PayloadTyp: 110,
+							RTPMa:      "telephone-event/48000",
+							ClockRat:   48000,
+						},
+						&format.Generic{
+							PayloadTyp: 112,
+							RTPMa:      "telephone-event/32000",
+							ClockRat:   32000,
+						},
+						&format.Generic{
+							PayloadTyp: 113,
+							RTPMa:      "telephone-event/16000",
+							ClockRat:   16000,
+						},
+						&format.Generic{
+							PayloadTyp: 126,
+							RTPMa:      "telephone-event/8000",
+							ClockRat:   8000,
+						},
 					},
 				},
-			},
-			{
-				Type:      "video",
-				Direction: DirectionSendonly,
-				Formats: []format.Format{
-					&format.VP8{
-						PayloadTyp: 96,
-					},
-					&format.Generic{
-						PayloadTyp: 97,
-						RTPMa:      "rtx/90000",
-						FMT: map[string]string{
-							"apt": "96",
+				{
+					Type:      "video",
+					Direction: MediaDirectionSendonly,
+					Formats: []format.Format{
+						&format.VP8{
+							PayloadTyp: 96,
 						},
-						ClockRat: 90000,
-					},
-					&format.VP9{
-						PayloadTyp: 98,
-					},
-					&format.Generic{
-						PayloadTyp: 99,
-						RTPMa:      "rtx/90000",
-						FMT: map[string]string{
-							"apt": "98",
+						&format.Generic{
+							PayloadTyp: 97,
+							RTPMa:      "rtx/90000",
+							FMT: map[string]string{
+								"apt": "96",
+							},
+							ClockRat: 90000,
 						},
-						ClockRat: 90000,
-					},
-					&format.H264{
-						PayloadTyp:        100,
-						PacketizationMode: 1,
-					},
-					&format.Generic{
-						PayloadTyp: 101,
-						RTPMa:      "rtx/90000",
-						FMT: map[string]string{
-							"apt": "100",
+						&format.VP9{
+							PayloadTyp: 98,
 						},
-						ClockRat: 90000,
-					},
-					&format.Generic{
-						PayloadTyp: 127,
-						RTPMa:      "red/90000",
-						ClockRat:   90000,
-					},
-					&format.Generic{
-						PayloadTyp: 124,
-						RTPMa:      "rtx/90000",
-						FMT: map[string]string{
-							"apt": "127",
+						&format.Generic{
+							PayloadTyp: 99,
+							RTPMa:      "rtx/90000",
+							FMT: map[string]string{
+								"apt": "98",
+							},
+							ClockRat: 90000,
+						},
+						&format.H264{
+							PayloadTyp:        100,
+							PacketizationMode: 1,
+						},
+						&format.Generic{
+							PayloadTyp: 101,
+							RTPMa:      "rtx/90000",
+							FMT: map[string]string{
+								"apt": "100",
+							},
+							ClockRat: 90000,
+						},
+						&format.Generic{
+							PayloadTyp: 127,
+							RTPMa:      "red/90000",
+							ClockRat:   90000,
+						},
+						&format.Generic{
+							PayloadTyp: 124,
+							RTPMa:      "rtx/90000",
+							FMT: map[string]string{
+								"apt": "127",
+							},
+							ClockRat: 90000,
+						},
+						&format.Generic{
+							PayloadTyp: 125,
+							RTPMa:      "ulpfec/90000",
+							ClockRat:   90000,
 						},
-						ClockRat: 90000,
-					},
-					&format.Generic{
-						PayloadTyp: 125,
-						RTPMa:      "ulpfec/90000",
-						ClockRat:   90000,
 					},
 				},
 			},
@@ -433,7 +439,7 @@ var casesMedias = []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=Stream\r\n" +
+			"s=Session\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" +
@@ -442,22 +448,24 @@ var casesMedias = []struct {
 			"a=fmtp:96 packetization-mode=1; profile-level-id=4D002A; " +
 			"sprop-parameter-sets=Z00AKp2oHgCJ+WbgICAgQA==,aO48gA==\r\n" +
 			"a=rtpmap:98 MetaData\r\n",
-		Medias{
-			{
-				Type: "video",
-				Formats: []format.Format{
-					&format.H264{
-						PayloadTyp: 96,
-						SPS: []byte{
-							0x67, 0x4d, 0x00, 0x2a, 0x9d, 0xa8, 0x1e, 0x00,
-							0x89, 0xf9, 0x66, 0xe0, 0x20, 0x20, 0x20, 0x40,
+		Session{
+			Medias: []*Media{
+				{
+					Type: "video",
+					Formats: []format.Format{
+						&format.H264{
+							PayloadTyp: 96,
+							SPS: []byte{
+								0x67, 0x4d, 0x00, 0x2a, 0x9d, 0xa8, 0x1e, 0x00,
+								0x89, 0xf9, 0x66, 0xe0, 0x20, 0x20, 0x20, 0x40,
+							},
+							PPS:               []byte{0x68, 0xee, 0x3c, 0x80},
+							PacketizationMode: 1,
+						},
+						&format.Generic{
+							PayloadTyp: 98,
+							RTPMa:      "MetaData",
 						},
-						PPS:               []byte{0x68, 0xee, 0x3c, 0x80},
-						PacketizationMode: 1,
-					},
-					&format.Generic{
-						PayloadTyp: 98,
-						RTPMa:      "MetaData",
 					},
 				},
 			},
@@ -480,7 +488,7 @@ var casesMedias = []struct {
 			"a=sendonly\r\n",
 		"v=0\r\n" +
 			"o=- 0 0 IN IP4 127.0.0.1\r\n" +
-			"s=Stream\r\n" +
+			"s=Session\r\n" +
 			"c=IN IP4 0.0.0.0\r\n" +
 			"t=0 0\r\n" +
 			"m=video 0 RTP/AVP 26\r\n" +
@@ -495,24 +503,26 @@ var casesMedias = []struct {
 			"a=control:rtsp://192.168.0.1/audioback\r\n" +
 			"a=sendonly\r\n" +
 			"a=rtpmap:0 PCMU/8000\r\n",
-		Medias{
-			{
-				Type:      "video",
-				Direction: DirectionRecvonly,
-				Control:   "rtsp://192.168.0.1/video",
-				Formats:   []format.Format{&format.MJPEG{}},
-			},
-			{
-				Type:      "audio",
-				Direction: DirectionRecvonly,
-				Control:   "rtsp://192.168.0.1/audio",
-				Formats:   []format.Format{&format.G711{MULaw: true}},
-			},
-			{
-				Type:      "audio",
-				Direction: DirectionSendonly,
-				Control:   "rtsp://192.168.0.1/audioback",
-				Formats:   []format.Format{&format.G711{MULaw: true}},
+		Session{
+			Medias: []*Media{
+				{
+					Type:      "video",
+					Direction: MediaDirectionRecvonly,
+					Control:   "rtsp://192.168.0.1/video",
+					Formats:   []format.Format{&format.MJPEG{}},
+				},
+				{
+					Type:      "audio",
+					Direction: MediaDirectionRecvonly,
+					Control:   "rtsp://192.168.0.1/audio",
+					Formats:   []format.Format{&format.G711{MULaw: true}},
+				},
+				{
+					Type:      "audio",
+					Direction: MediaDirectionSendonly,
+					Control:   "rtsp://192.168.0.1/audioback",
+					Formats:   []format.Format{&format.G711{MULaw: true}},
+				},
 			},
 		},
 	},
@@ -526,20 +536,22 @@ var casesMedias = []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=Stream\r\n" +
+			"s=Session\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",
-		Medias{
-			{
-				Type: "application/TP-LINK",
-				Formats: []format.Format{&format.Generic{
-					PayloadTyp: 95,
-					RTPMa:      "TP-LINK/90000",
-					ClockRat:   90000,
-				}},
+		Session{
+			Medias: []*Media{
+				{
+					Type: "application/TP-LINK",
+					Formats: []format.Format{&format.Generic{
+						PayloadTyp: 95,
+						RTPMa:      "TP-LINK/90000",
+						ClockRat:   90000,
+					}},
+				},
 			},
 		},
 	},
@@ -554,20 +566,22 @@ var casesMedias = []struct {
 			"a=rtpmap:95 MERCURY/90000\n",
 		"v=0\r\n" +
 			"o=- 0 0 IN IP4 127.0.0.1\r\n" +
-			"s=Stream\r\n" +
+			"s=Session\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",
-		Medias{
-			{
-				Type: "application/MERCURY",
-				Formats: []format.Format{&format.Generic{
-					PayloadTyp: 95,
-					RTPMa:      "MERCURY/90000",
-					ClockRat:   90000,
-				}},
+		Session{
+			Medias: []*Media{
+				{
+					Type: "application/MERCURY",
+					Formats: []format.Format{&format.Generic{
+						PayloadTyp: 95,
+						RTPMa:      "MERCURY/90000",
+						ClockRat:   90000,
+					}},
+				},
 			},
 		},
 	},
@@ -582,20 +596,22 @@ var casesMedias = []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=Stream\r\n" +
+			"s=Session\r\n" +
 			"c=IN IP4 0.0.0.0\r\n" +
 			"t=0 0\r\n" +
 			"m=video 0 RTP/AVP 96\r\n" +
 			"a=control\r\n" +
 			"a=rtpmap:96 H264/90000\r\n" +
 			"a=fmtp:96 packetization-mode=1\r\n",
-		Medias{
-			{
-				Type: "video",
-				Formats: []format.Format{
-					&format.H264{
-						PayloadTyp:        96,
-						PacketizationMode: 1,
+		Session{
+			Medias: []*Media{
+				{
+					Type: "video",
+					Formats: []format.Format{
+						&format.H264{
+							PayloadTyp:        96,
+							PacketizationMode: 1,
+						},
 					},
 				},
 			},
@@ -603,22 +619,22 @@ var casesMedias = []struct {
 	},
 }
 
-func TestMediasUnmarshal(t *testing.T) {
-	for _, ca := range casesMedias {
+func TestSessionUnmarshal(t *testing.T) {
+	for _, ca := range casesSession {
 		t.Run(ca.name, func(t *testing.T) {
 			var sdp sdp.SessionDescription
 			err := sdp.Unmarshal([]byte(ca.in))
 			require.NoError(t, err)
 
-			var medias Medias
-			err = medias.Unmarshal(sdp.MediaDescriptions)
+			var desc Session
+			err = desc.Unmarshal(&sdp)
 			require.NoError(t, err)
-			require.Equal(t, ca.medias, medias)
+			require.Equal(t, ca.desc, desc)
 		})
 	}
 }
 
-func TestMediasUnmarshalErrors(t *testing.T) {
+func TestSessionUnmarshalErrors(t *testing.T) {
 	for _, ca := range []struct {
 		name string
 		sdp  string
@@ -646,24 +662,24 @@ func TestMediasUnmarshalErrors(t *testing.T) {
 			err := sd.Unmarshal([]byte(ca.sdp))
 			require.NoError(t, err)
 
-			var medias Medias
-			err = medias.Unmarshal(sd.MediaDescriptions)
+			var desc Session
+			err = desc.Unmarshal(&sd)
 			require.EqualError(t, err, ca.err)
 		})
 	}
 }
 
-func TestMediasMarshal(t *testing.T) {
-	for _, ca := range casesMedias {
+func TestSessionMarshal(t *testing.T) {
+	for _, ca := range casesSession {
 		t.Run(ca.name, func(t *testing.T) {
-			byts, err := ca.medias.Marshal(false).Marshal()
+			byts, err := ca.desc.Marshal(false)
 			require.NoError(t, err)
 			require.Equal(t, ca.out, string(byts))
 		})
 	}
 }
 
-func TestMediasFindFormat(t *testing.T) {
+func TestSessionFindFormat(t *testing.T) {
 	tr := &format.Generic{
 		PayloadTyp: 97,
 		RTPMa:      "rtx/90000",
@@ -674,7 +690,7 @@ func TestMediasFindFormat(t *testing.T) {
 	}
 
 	md := &Media{
-		Type: TypeVideo,
+		Type: MediaTypeVideo,
 		Formats: []format.Format{
 			&format.VP8{
 				PayloadTyp: 96,
@@ -686,21 +702,23 @@ func TestMediasFindFormat(t *testing.T) {
 		},
 	}
 
-	ms := Medias{
-		{
-			Type: TypeAudio,
-			Formats: []format.Format{
-				&format.Opus{
-					PayloadTyp: 111,
-					IsStereo:   true,
+	desc := &Session{
+		Medias: []*Media{
+			{
+				Type: MediaTypeAudio,
+				Formats: []format.Format{
+					&format.Opus{
+						PayloadTyp: 111,
+						IsStereo:   true,
+					},
 				},
 			},
+			md,
 		},
-		md,
 	}
 
 	var forma *format.Generic
-	me := ms.FindFormat(&forma)
+	me := desc.FindFormat(&forma)
 	require.Equal(t, md, me)
 	require.Equal(t, tr, forma)
 }
diff --git a/server_play_test.go b/server_play_test.go
index cfbf835a..fdcc0589 100644
--- a/server_play_test.go
+++ b/server_play_test.go
@@ -19,9 +19,9 @@ import (
 
 	"github.com/bluenviron/gortsplib/v4/pkg/base"
 	"github.com/bluenviron/gortsplib/v4/pkg/conn"
+	"github.com/bluenviron/gortsplib/v4/pkg/description"
 	"github.com/bluenviron/gortsplib/v4/pkg/format"
 	"github.com/bluenviron/gortsplib/v4/pkg/headers"
-	"github.com/bluenviron/gortsplib/v4/pkg/media"
 	"github.com/bluenviron/gortsplib/v4/pkg/sdp"
 	"github.com/bluenviron/gortsplib/v4/pkg/url"
 )
@@ -254,11 +254,13 @@ func TestServerPlayPath(t *testing.T) {
 			require.NoError(t, err)
 			defer s.Close()
 
-			stream = NewServerStream(s, media.Medias{
-				testH264Media,
-				testH264Media,
-				testH264Media,
-				testH264Media,
+			stream = NewServerStream(s, &description.Session{
+				Medias: []*description.Media{
+					testH264Media,
+					testH264Media,
+					testH264Media,
+					testH264Media,
+				},
 			})
 			defer stream.Close()
 
@@ -337,7 +339,7 @@ func TestServerPlaySetupErrors(t *testing.T) {
 			require.NoError(t, err)
 			defer s.Close()
 
-			stream = NewServerStream(s, media.Medias{testH264Media})
+			stream = NewServerStream(s, &description.Session{Medias: []*description.Media{testH264Media}})
 			if ca == "closed stream" {
 				stream.Close()
 			} else {
@@ -463,7 +465,7 @@ func TestServerPlaySetupErrorSameUDPPortsAndIP(t *testing.T) {
 	require.NoError(t, err)
 	defer s.Close()
 
-	stream = NewServerStream(s, media.Medias{testH264Media})
+	stream = NewServerStream(s, &description.Session{Medias: []*description.Media{testH264Media}})
 	defer stream.Close()
 
 	for i := 0; i < 2; i++ {
@@ -553,17 +555,17 @@ func TestServerPlay(t *testing.T) {
 						// send RTCP packets directly to the session.
 						// these are sent after the response, only if onPlay returns StatusOK.
 						if transport != "multicast" {
-							err := ctx.Session.WritePacketRTCP(stream.Medias()[0], &testRTCPPacket)
+							err := ctx.Session.WritePacketRTCP(stream.Description().Medias[0], &testRTCPPacket)
 							require.NoError(t, err)
 						}
 
-						ctx.Session.OnPacketRTCPAny(func(medi *media.Media, pkt rtcp.Packet) {
+						ctx.Session.OnPacketRTCPAny(func(medi *description.Media, pkt rtcp.Packet) {
 							// ignore multicast loopback
 							if transport == "multicast" && atomic.AddUint64(&counter, 1) <= 1 {
 								return
 							}
 
-							require.Equal(t, stream.Medias()[0], medi)
+							require.Equal(t, stream.Description().Medias[0], medi)
 							require.Equal(t, &testRTCPPacket, pkt)
 							close(framesReceived)
 						})
@@ -573,9 +575,9 @@ func TestServerPlay(t *testing.T) {
 						// ServerStream.WritePacket*()
 						go func() {
 							time.Sleep(500 * time.Millisecond)
-							err := stream.WritePacketRTCP(stream.Medias()[0], &testRTCPPacket)
+							err := stream.WritePacketRTCP(stream.Description().Medias[0], &testRTCPPacket)
 							require.NoError(t, err)
-							err = stream.WritePacketRTP(stream.Medias()[0], &testRTPPacket)
+							err = stream.WritePacketRTP(stream.Description().Medias[0], &testRTPPacket)
 							require.NoError(t, err)
 						}()
 
@@ -612,7 +614,7 @@ func TestServerPlay(t *testing.T) {
 			require.NoError(t, err)
 			defer s.Close()
 
-			stream = NewServerStream(s, media.Medias{testH264Media})
+			stream = NewServerStream(s, &description.Session{Medias: []*description.Media{testH264Media}})
 			defer stream.Close()
 
 			nconn, err := net.Dial("tcp", listenIP+":8554")
@@ -890,7 +892,7 @@ func TestServerPlayDecodeErrors(t *testing.T) {
 			require.NoError(t, err)
 			defer s.Close()
 
-			stream = NewServerStream(s, media.Medias{testH264Media})
+			stream = NewServerStream(s, &description.Session{Medias: []*description.Media{testH264Media}})
 			defer stream.Close()
 
 			nconn, err := net.Dial("tcp", "localhost:8554")
@@ -1014,7 +1016,7 @@ func TestServerPlayRTCPReport(t *testing.T) {
 			require.NoError(t, err)
 			defer s.Close()
 
-			stream = NewServerStream(s, media.Medias{testH264Media})
+			stream = NewServerStream(s, &description.Session{Medias: []*description.Media{testH264Media}})
 			defer stream.Close()
 
 			nconn, err := net.Dial("tcp", "localhost:8554")
@@ -1066,7 +1068,7 @@ func TestServerPlayRTCPReport(t *testing.T) {
 			curTimeMutex.Unlock()
 
 			err = stream.WritePacketRTPWithNTP(
-				stream.Medias()[0],
+				stream.Description().Medias[0],
 				&rtp.Packet{
 					Header: rtp.Header{
 						Version:     2,
@@ -1138,7 +1140,7 @@ func TestServerPlayVLCMulticast(t *testing.T) {
 	require.NoError(t, err)
 	defer s.Close()
 
-	stream = NewServerStream(s, media.Medias{testH264Media})
+	stream = NewServerStream(s, &description.Session{Medias: []*description.Media{testH264Media}})
 	defer stream.Close()
 
 	nconn, err := net.Dial("tcp", listenIP+":8554")
@@ -1189,7 +1191,7 @@ func TestServerPlayTCPResponseBeforeFrames(t *testing.T) {
 				go func() {
 					defer close(writerDone)
 
-					err := stream.WritePacketRTP(stream.Medias()[0], &testRTPPacket)
+					err := stream.WritePacketRTP(stream.Description().Medias[0], &testRTPPacket)
 					require.NoError(t, err)
 
 					ti := time.NewTicker(50 * time.Millisecond)
@@ -1198,7 +1200,7 @@ func TestServerPlayTCPResponseBeforeFrames(t *testing.T) {
 					for {
 						select {
 						case <-ti.C:
-							err := stream.WritePacketRTP(stream.Medias()[0], &testRTPPacket)
+							err := stream.WritePacketRTP(stream.Description().Medias[0], &testRTPPacket)
 							require.NoError(t, err)
 						case <-writerTerminate:
 							return
@@ -1219,7 +1221,7 @@ func TestServerPlayTCPResponseBeforeFrames(t *testing.T) {
 	require.NoError(t, err)
 	defer s.Close()
 
-	stream = NewServerStream(s, media.Medias{testH264Media})
+	stream = NewServerStream(s, &description.Session{Medias: []*description.Media{testH264Media}})
 	defer stream.Close()
 
 	nconn, err := net.Dial("tcp", "localhost:8554")
@@ -1282,7 +1284,7 @@ func TestServerPlayPlayPlay(t *testing.T) {
 	require.NoError(t, err)
 	defer s.Close()
 
-	stream = NewServerStream(s, media.Medias{testH264Media})
+	stream = NewServerStream(s, &description.Session{Medias: []*description.Media{testH264Media}})
 	defer stream.Close()
 
 	nconn, err := net.Dial("tcp", "localhost:8554")
@@ -1347,7 +1349,7 @@ func TestServerPlayPlayPausePlay(t *testing.T) {
 						for {
 							select {
 							case <-ti.C:
-								err := stream.WritePacketRTP(stream.Medias()[0], &testRTPPacket)
+								err := stream.WritePacketRTP(stream.Description().Medias[0], &testRTPPacket)
 								require.NoError(t, err)
 							case <-writerTerminate:
 								return
@@ -1373,7 +1375,7 @@ func TestServerPlayPlayPausePlay(t *testing.T) {
 	require.NoError(t, err)
 	defer s.Close()
 
-	stream = NewServerStream(s, media.Medias{testH264Media})
+	stream = NewServerStream(s, &description.Session{Medias: []*description.Media{testH264Media}})
 	defer stream.Close()
 
 	nconn, err := net.Dial("tcp", "localhost:8554")
@@ -1436,7 +1438,7 @@ func TestServerPlayPlayPausePause(t *testing.T) {
 					for {
 						select {
 						case <-ti.C:
-							err := stream.WritePacketRTP(stream.Medias()[0], &testRTPPacket)
+							err := stream.WritePacketRTP(stream.Description().Medias[0], &testRTPPacket)
 							require.NoError(t, err)
 						case <-writerTerminate:
 							return
@@ -1461,7 +1463,7 @@ func TestServerPlayPlayPausePause(t *testing.T) {
 	require.NoError(t, err)
 	defer s.Close()
 
-	stream = NewServerStream(s, media.Medias{testH264Media})
+	stream = NewServerStream(s, &description.Session{Medias: []*description.Media{testH264Media}})
 	defer stream.Close()
 
 	nconn, err := net.Dial("tcp", "localhost:8554")
@@ -1547,7 +1549,7 @@ func TestServerPlayTimeout(t *testing.T) {
 			require.NoError(t, err)
 			defer s.Close()
 
-			stream = NewServerStream(s, media.Medias{testH264Media})
+			stream = NewServerStream(s, &description.Session{Medias: []*description.Media{testH264Media}})
 			defer stream.Close()
 
 			nconn, err := net.Dial("tcp", "localhost:8554")
@@ -1636,7 +1638,7 @@ func TestServerPlayWithoutTeardown(t *testing.T) {
 			require.NoError(t, err)
 			defer s.Close()
 
-			stream = NewServerStream(s, media.Medias{testH264Media})
+			stream = NewServerStream(s, &description.Session{Medias: []*description.Media{testH264Media}})
 			defer stream.Close()
 
 			nconn, err := net.Dial("tcp", "localhost:8554")
@@ -1714,7 +1716,7 @@ func TestServerPlayUDPChangeConn(t *testing.T) {
 	require.NoError(t, err)
 	defer s.Close()
 
-	stream = NewServerStream(s, media.Medias{testH264Media})
+	stream = NewServerStream(s, &description.Session{Medias: []*description.Media{testH264Media}})
 	defer stream.Close()
 
 	sxID := ""
@@ -1786,9 +1788,9 @@ func TestServerPlayPartialMedias(t *testing.T) {
 			onPlay: func(ctx *ServerHandlerOnPlayCtx) (*base.Response, error) {
 				go func() {
 					time.Sleep(500 * time.Millisecond)
-					err := stream.WritePacketRTP(stream.Medias()[0], &testRTPPacket)
+					err := stream.WritePacketRTP(stream.Description().Medias[0], &testRTPPacket)
 					require.NoError(t, err)
-					err = stream.WritePacketRTP(stream.Medias()[1], &testRTPPacket)
+					err = stream.WritePacketRTP(stream.Description().Medias[1], &testRTPPacket)
 					require.NoError(t, err)
 				}()
 
@@ -1804,7 +1806,7 @@ func TestServerPlayPartialMedias(t *testing.T) {
 	require.NoError(t, err)
 	defer s.Close()
 
-	stream = NewServerStream(s, media.Medias{testH264Media, testH264Media})
+	stream = NewServerStream(s, &description.Session{Medias: []*description.Media{testH264Media, testH264Media}})
 	defer stream.Close()
 
 	nconn, err := net.Dial("tcp", "localhost:8554")
@@ -1928,19 +1930,21 @@ func TestServerPlayAdditionalInfos(t *testing.T) {
 	require.NoError(t, err)
 	defer s.Close()
 
-	stream = NewServerStream(s, media.Medias{
-		&media.Media{
-			Type:    "application",
-			Formats: []format.Format{forma},
-		},
-		&media.Media{
-			Type:    "application",
-			Formats: []format.Format{forma},
+	stream = NewServerStream(s, &description.Session{
+		Medias: []*description.Media{
+			{
+				Type:    "application",
+				Formats: []format.Format{forma},
+			},
+			{
+				Type:    "application",
+				Formats: []format.Format{forma},
+			},
 		},
 	})
 	defer stream.Close()
 
-	err = stream.WritePacketRTP(stream.Medias()[0], &rtp.Packet{
+	err = stream.WritePacketRTP(stream.Description().Medias[0], &rtp.Packet{
 		Header: rtp.Header{
 			Version:        2,
 			PayloadType:    96,
@@ -1970,7 +1974,7 @@ func TestServerPlayAdditionalInfos(t *testing.T) {
 		nil,
 	}, ssrcs)
 
-	err = stream.WritePacketRTP(stream.Medias()[1], &rtp.Packet{
+	err = stream.WritePacketRTP(stream.Description().Medias[1], &rtp.Packet{
 		Header: rtp.Header{
 			Version:        2,
 			PayloadType:    96,
@@ -2045,14 +2049,16 @@ func TestServerPlayNoInterleavedIDs(t *testing.T) {
 	require.NoError(t, err)
 	defer s.Close()
 
-	stream = NewServerStream(s, media.Medias{
-		&media.Media{
-			Type:    "application",
-			Formats: []format.Format{forma},
-		},
-		&media.Media{
-			Type:    "application",
-			Formats: []format.Format{forma},
+	stream = NewServerStream(s, &description.Session{
+		Medias: []*description.Media{
+			{
+				Type:    "application",
+				Formats: []format.Format{forma},
+			},
+			{
+				Type:    "application",
+				Formats: []format.Format{forma},
+			},
 		},
 	})
 	defer stream.Close()
@@ -2089,7 +2095,7 @@ func TestServerPlayNoInterleavedIDs(t *testing.T) {
 	doPlay(t, conn, "rtsp://localhost:8554/teststream", session)
 
 	for i := 0; i < 2; i++ {
-		err := stream.WritePacketRTP(stream.Medias()[i], &testRTPPacket)
+		err := stream.WritePacketRTP(stream.Description().Medias[i], &testRTPPacket)
 		require.NoError(t, err)
 
 		f, err := conn.ReadInterleavedFrame()
diff --git a/server_record_test.go b/server_record_test.go
index a9910ac5..850c28e4 100644
--- a/server_record_test.go
+++ b/server_record_test.go
@@ -15,13 +15,13 @@ import (
 
 	"github.com/bluenviron/gortsplib/v4/pkg/base"
 	"github.com/bluenviron/gortsplib/v4/pkg/conn"
+	"github.com/bluenviron/gortsplib/v4/pkg/description"
 	"github.com/bluenviron/gortsplib/v4/pkg/format"
 	"github.com/bluenviron/gortsplib/v4/pkg/headers"
-	"github.com/bluenviron/gortsplib/v4/pkg/media"
 	"github.com/bluenviron/gortsplib/v4/pkg/sdp"
 )
 
-func doAnnounce(t *testing.T, conn *conn.Conn, u string, medias media.Medias) {
+func doAnnounce(t *testing.T, conn *conn.Conn, u string, medias []*description.Media) {
 	res, err := writeReqReadRes(conn, base.Request{
 		Method: base.Announce,
 		URL:    mustParseURL(u),
@@ -29,7 +29,7 @@ func doAnnounce(t *testing.T, conn *conn.Conn, u string, medias media.Medias) {
 			"CSeq":         base.HeaderValue{"1"},
 			"Content-Type": base.HeaderValue{"application/sdp"},
 		},
-		Body: mustMarshalMedias(medias),
+		Body: mediasToSDP(medias),
 	})
 	require.NoError(t, err)
 	require.Equal(t, base.StatusOK, res.StatusCode)
@@ -224,7 +224,7 @@ func TestServerRecordPath(t *testing.T) {
 				Handler: &testServerHandler{
 					onAnnounce: func(ctx *ServerHandlerOnAnnounceCtx) (*base.Response, error) {
 						// make sure that media URLs are not overridden by NewServerStream()
-						stream := NewServerStream(s, ctx.Medias)
+						stream := NewServerStream(s, ctx.Description)
 						defer stream.Close()
 
 						return &base.Response{
@@ -344,8 +344,7 @@ func TestServerRecordErrorSetupMediaTwice(t *testing.T) {
 	defer nconn.Close()
 	conn := conn.NewConn(nconn)
 
-	medias := media.Medias{testH264Media}
-	resetMediaControls(medias)
+	medias := []*description.Media{testH264Media}
 
 	doAnnounce(t, conn, "rtsp://localhost:8554/teststream", medias)
 
@@ -438,17 +437,16 @@ func TestServerRecordErrorRecordPartialMedias(t *testing.T) {
 	err = forma.Init()
 	require.NoError(t, err)
 
-	medias := media.Medias{
-		&media.Media{
+	medias := []*description.Media{
+		{
 			Type:    "application",
 			Formats: []format.Format{forma},
 		},
-		&media.Media{
+		{
 			Type:    "application",
 			Formats: []format.Format{forma},
 		},
 	}
-	resetMediaControls(medias)
 
 	doAnnounce(t, conn, "rtsp://localhost:8554/teststream", medias)
 
@@ -523,25 +521,25 @@ func TestServerRecord(t *testing.T) {
 					onRecord: func(ctx *ServerHandlerOnRecordCtx) (*base.Response, error) {
 						// queue sending of RTCP packets.
 						// these are sent after the response, only if onRecord returns StatusOK.
-						err := ctx.Session.WritePacketRTCP(ctx.Session.AnnouncedMedias()[0], &testRTCPPacket)
+						err := ctx.Session.WritePacketRTCP(ctx.Session.AnnouncedDescription().Medias[0], &testRTCPPacket)
 						require.NoError(t, err)
-						err = ctx.Session.WritePacketRTCP(ctx.Session.AnnouncedMedias()[1], &testRTCPPacket)
+						err = ctx.Session.WritePacketRTCP(ctx.Session.AnnouncedDescription().Medias[1], &testRTCPPacket)
 						require.NoError(t, err)
 
 						for i := 0; i < 2; i++ {
 							ctx.Session.OnPacketRTP(
-								ctx.Session.AnnouncedMedias()[i],
-								ctx.Session.AnnouncedMedias()[i].Formats[0],
+								ctx.Session.AnnouncedDescription().Medias[i],
+								ctx.Session.AnnouncedDescription().Medias[i].Formats[0],
 								func(pkt *rtp.Packet) {
 									require.Equal(t, &testRTPPacket, pkt)
 								})
 
 							ci := i
 							ctx.Session.OnPacketRTCP(
-								ctx.Session.AnnouncedMedias()[i],
+								ctx.Session.AnnouncedDescription().Medias[i],
 								func(pkt rtcp.Packet) {
 									require.Equal(t, &testRTCPPacket, pkt)
-									err := ctx.Session.WritePacketRTCP(ctx.Session.AnnouncedMedias()[ci], &testRTCPPacket)
+									err := ctx.Session.WritePacketRTCP(ctx.Session.AnnouncedDescription().Medias[ci], &testRTCPPacket)
 									require.NoError(t, err)
 								})
 						}
@@ -583,9 +581,9 @@ func TestServerRecord(t *testing.T) {
 
 			<-nconnOpened
 
-			medias := media.Medias{
-				&media.Media{
-					Type: media.TypeVideo,
+			medias := []*description.Media{
+				{
+					Type: description.MediaTypeVideo,
 					Formats: []format.Format{&format.H264{
 						PayloadTyp:        96,
 						SPS:               []byte{0x01, 0x02, 0x03, 0x04},
@@ -593,8 +591,8 @@ func TestServerRecord(t *testing.T) {
 						PacketizationMode: 1,
 					}},
 				},
-				&media.Media{
-					Type: media.TypeVideo,
+				{
+					Type: description.MediaTypeVideo,
 					Formats: []format.Format{&format.H264{
 						PayloadTyp:        96,
 						SPS:               []byte{0x01, 0x02, 0x03, 0x04},
@@ -603,7 +601,6 @@ func TestServerRecord(t *testing.T) {
 					}},
 				},
 			}
-			resetMediaControls(medias)
 
 			doAnnounce(t, conn, "rtsp://localhost:8554/teststream", medias)
 
@@ -766,8 +763,7 @@ func TestServerRecordErrorInvalidProtocol(t *testing.T) {
 	defer nconn.Close()
 	conn := conn.NewConn(nconn)
 
-	medias := media.Medias{testH264Media}
-	resetMediaControls(medias)
+	medias := []*description.Media{testH264Media}
 
 	doAnnounce(t, conn, "rtsp://localhost:8554/teststream", medias)
 
@@ -833,8 +829,7 @@ func TestServerRecordRTCPReport(t *testing.T) {
 	defer nconn.Close()
 	conn := conn.NewConn(nconn)
 
-	medias := media.Medias{testH264Media}
-	resetMediaControls(medias)
+	medias := []*description.Media{testH264Media}
 
 	doAnnounce(t, conn, "rtsp://localhost:8554/teststream", medias)
 
@@ -974,8 +969,7 @@ func TestServerRecordTimeout(t *testing.T) {
 			defer nconn.Close()
 			conn := conn.NewConn(nconn)
 
-			medias := media.Medias{testH264Media}
-			resetMediaControls(medias)
+			medias := []*description.Media{testH264Media}
 
 			doAnnounce(t, conn, "rtsp://localhost:8554/teststream", medias)
 
@@ -1063,8 +1057,7 @@ func TestServerRecordWithoutTeardown(t *testing.T) {
 			require.NoError(t, err)
 			conn := conn.NewConn(nconn)
 
-			medias := media.Medias{testH264Media}
-			resetMediaControls(medias)
+			medias := []*description.Media{testH264Media}
 
 			doAnnounce(t, conn, "rtsp://localhost:8554/teststream", medias)
 
@@ -1142,8 +1135,7 @@ func TestServerRecordUDPChangeConn(t *testing.T) {
 		defer nconn.Close()
 		conn := conn.NewConn(nconn)
 
-		medias := media.Medias{testH264Media}
-		resetMediaControls(medias)
+		medias := []*description.Media{testH264Media}
 
 		doAnnounce(t, conn, "rtsp://localhost:8554/teststream", medias)
 
@@ -1273,14 +1265,13 @@ func TestServerRecordDecodeErrors(t *testing.T) {
 			defer nconn.Close()
 			conn := conn.NewConn(nconn)
 
-			medias := media.Medias{&media.Media{
-				Type: media.TypeApplication,
+			medias := []*description.Media{{
+				Type: description.MediaTypeApplication,
 				Formats: []format.Format{&format.Generic{
 					PayloadTyp: 97,
 					RTPMa:      "private/90000",
 				}},
 			}}
-			resetMediaControls(medias)
 
 			doAnnounce(t, conn, "rtsp://localhost:8554/teststream", medias)
 
@@ -1438,7 +1429,7 @@ func TestServerRecordPacketNTP(t *testing.T) {
 				}, nil, nil
 			},
 			onRecord: func(ctx *ServerHandlerOnRecordCtx) (*base.Response, error) {
-				ctx.Session.OnPacketRTPAny(func(medi *media.Media, forma format.Format, pkt *rtp.Packet) {
+				ctx.Session.OnPacketRTPAny(func(medi *description.Media, forma format.Format, pkt *rtp.Packet) {
 					if !first {
 						first = true
 					} else {
@@ -1468,8 +1459,7 @@ func TestServerRecordPacketNTP(t *testing.T) {
 	defer nconn.Close()
 	conn := conn.NewConn(nconn)
 
-	medias := media.Medias{testH264Media}
-	resetMediaControls(medias)
+	medias := []*description.Media{testH264Media}
 
 	doAnnounce(t, conn, "rtsp://localhost:8554/teststream", medias)
 
diff --git a/server_test.go b/server_test.go
index fce0faf1..e8445f3c 100644
--- a/server_test.go
+++ b/server_test.go
@@ -11,8 +11,8 @@ import (
 	"github.com/bluenviron/gortsplib/v4/pkg/auth"
 	"github.com/bluenviron/gortsplib/v4/pkg/base"
 	"github.com/bluenviron/gortsplib/v4/pkg/conn"
+	"github.com/bluenviron/gortsplib/v4/pkg/description"
 	"github.com/bluenviron/gortsplib/v4/pkg/headers"
-	"github.com/bluenviron/gortsplib/v4/pkg/media"
 )
 
 var serverCert = []byte(`-----BEGIN CERTIFICATE-----
@@ -353,7 +353,7 @@ func TestServerErrorMethodNotImplemented(t *testing.T) {
 			require.NoError(t, err)
 			defer s.Close()
 
-			stream := NewServerStream(s, media.Medias{testH264Media})
+			stream := NewServerStream(s, &description.Session{Medias: []*description.Media{testH264Media}})
 			defer stream.Close()
 
 			h.stream = stream
@@ -452,7 +452,7 @@ func TestServerErrorTCPTwoConnOneSession(t *testing.T) {
 	require.NoError(t, err)
 	defer s.Close()
 
-	stream = NewServerStream(s, media.Medias{testH264Media})
+	stream = NewServerStream(s, &description.Session{Medias: []*description.Media{testH264Media}})
 	defer stream.Close()
 
 	nconn1, err := net.Dial("tcp", "localhost:8554")
@@ -545,7 +545,7 @@ func TestServerErrorTCPOneConnTwoSessions(t *testing.T) {
 	require.NoError(t, err)
 	defer s.Close()
 
-	stream = NewServerStream(s, media.Medias{testH264Media})
+	stream = NewServerStream(s, &description.Session{Medias: []*description.Media{testH264Media}})
 	defer stream.Close()
 
 	nconn, err := net.Dial("tcp", "localhost:8554")
@@ -620,7 +620,7 @@ func TestServerSetupMultipleTransports(t *testing.T) {
 	require.NoError(t, err)
 	defer s.Close()
 
-	stream = NewServerStream(s, media.Medias{testH264Media})
+	stream = NewServerStream(s, &description.Session{Medias: []*description.Media{testH264Media}})
 	defer stream.Close()
 
 	nconn, err := net.Dial("tcp", "localhost:8554")
@@ -731,7 +731,7 @@ func TestServerGetSetParameter(t *testing.T) {
 			require.NoError(t, err)
 			defer s.Close()
 
-			stream = NewServerStream(s, media.Medias{testH264Media})
+			stream = NewServerStream(s, &description.Session{Medias: []*description.Media{testH264Media}})
 			defer stream.Close()
 
 			nconn, err := net.Dial("tcp", "localhost:8554")
@@ -881,7 +881,7 @@ func TestServerSessionClose(t *testing.T) {
 	require.NoError(t, err)
 	defer s.Close()
 
-	stream = NewServerStream(s, media.Medias{testH264Media})
+	stream = NewServerStream(s, &description.Session{Medias: []*description.Media{testH264Media}})
 	defer stream.Close()
 
 	nconn, err := net.Dial("tcp", "localhost:8554")
@@ -962,7 +962,7 @@ func TestServerSessionAutoClose(t *testing.T) {
 			require.NoError(t, err)
 			defer s.Close()
 
-			stream = NewServerStream(s, media.Medias{testH264Media})
+			stream = NewServerStream(s, &description.Session{Medias: []*description.Media{testH264Media}})
 			defer stream.Close()
 
 			nconn, err := net.Dial("tcp", "localhost:8554")
@@ -1030,7 +1030,7 @@ func TestServerSessionTeardown(t *testing.T) {
 	require.NoError(t, err)
 	defer s.Close()
 
-	stream = NewServerStream(s, media.Medias{testH264Media})
+	stream = NewServerStream(s, &description.Session{Medias: []*description.Media{testH264Media}})
 	defer stream.Close()
 
 	nconn, err := net.Dial("tcp", "localhost:8554")
@@ -1104,7 +1104,7 @@ func TestServerAuth(t *testing.T) {
 	defer nconn.Close()
 	conn := conn.NewConn(nconn)
 
-	medias := media.Medias{testH264Media}
+	medias := []*description.Media{testH264Media}
 
 	req := base.Request{
 		Method: base.Announce,
@@ -1113,7 +1113,7 @@ func TestServerAuth(t *testing.T) {
 			"CSeq":         base.HeaderValue{"1"},
 			"Content-Type": base.HeaderValue{"application/sdp"},
 		},
-		Body: mustMarshalMedias(medias),
+		Body: mediasToSDP(medias),
 	}
 
 	res, err := writeReqReadRes(conn, req)