Repository: bluenviron/gortsplib
Pull request #459 (2023-11-06 10:01:15)

move url.URL into base.URL

codecov[bot]: ## [Codecov](https://app.codecov.io/gh/bluenviron/gortsplib/pull/459?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=bluenviron) Report
> Merging [#459](https://app.codecov.io/gh/bluenviron/gortsplib/pull/459?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=bluenviron) (46b82e1) into [main](https://app.codecov.io/gh/bluenviron/gortsplib/commit/0933bf997556ccd22bf62e1f7d25552d7525244d?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=bluenviron) (0933bf9) will **decrease** coverage by `0.01%`.
> The diff coverage is `94.68%`.

```diff
@@            Coverage Diff             @@
##             main     #459      +/-   ##
==========================================
- Coverage   84.55%   84.54%   -0.01%     
==========================================
  Files         106      106              
  Lines       10921    10921              
==========================================
- Hits         9234     9233       -1     
  Misses       1291     1291              
- Partials      396      397       +1     
```


| [Files](https://app.codecov.io/gh/bluenviron/gortsplib/pull/459?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=bluenviron) | Coverage Δ | |
|---|---|---|
| [pkg/auth/validate.go](https://app.codecov.io/gh/bluenviron/gortsplib/pull/459?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=bluenviron#diff-cGtnL2F1dGgvdmFsaWRhdGUuZ28=) | `94.00% <ø> (ø)` | |
| [pkg/base/path.go](https://app.codecov.io/gh/bluenviron/gortsplib/pull/459?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=bluenviron#diff-cGtnL2Jhc2UvcGF0aC5nbw==) | `100.00% <100.00%> (ø)` | |
| [pkg/base/request.go](https://app.codecov.io/gh/bluenviron/gortsplib/pull/459?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=bluenviron#diff-cGtnL2Jhc2UvcmVxdWVzdC5nbw==) | `100.00% <100.00%> (ø)` | |
| [pkg/description/media.go](https://app.codecov.io/gh/bluenviron/gortsplib/pull/459?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=bluenviron#diff-cGtnL2Rlc2NyaXB0aW9uL21lZGlhLmdv) | `92.44% <100.00%> (ø)` | |
| [pkg/description/session.go](https://app.codecov.io/gh/bluenviron/gortsplib/pull/459?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=bluenviron#diff-cGtnL2Rlc2NyaXB0aW9uL3Nlc3Npb24uZ28=) | `96.00% <ø> (ø)` | |
| [server\_conn.go](https://app.codecov.io/gh/bluenviron/gortsplib/pull/459?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=bluenviron#diff-c2VydmVyX2Nvbm4uZ28=) | `90.93% <100.00%> (ø)` | |
| [server\_session.go](https://app.codecov.io/gh/bluenviron/gortsplib/pull/459?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=bluenviron#diff-c2VydmVyX3Nlc3Npb24uZ28=) | `79.23% <100.00%> (-0.11%)` | :arrow_down: |
| [client.go](https://app.codecov.io/gh/bluenviron/gortsplib/pull/459?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=bluenviron#diff-Y2xpZW50Lmdv) | `72.11% <93.75%> (ø)` | |
| [pkg/base/url.go](https://app.codecov.io/gh/bluenviron/gortsplib/pull/459?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=bluenviron#diff-cGtnL2Jhc2UvdXJsLmdv) | `93.10% <93.10%> (ø)` | |

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

:mega: Codecov offers a browser extension for seamless coverage viewing on GitHub. Try it in [Chrome](https://chrome.google.com/webstore/detail/codecov/gedikamndpbemklijjkncpnolildpbgo) or [Firefox](https://addons.mozilla.org/en-US/firefox/addon/codecov/) today!


Patch:
diff --git a/client.go b/client.go
index 41dbdc7a..fea11857 100644
--- a/client.go
+++ b/client.go
@@ -29,7 +29,6 @@ import (
 	"github.com/bluenviron/gortsplib/v4/pkg/liberrors"
 	"github.com/bluenviron/gortsplib/v4/pkg/rtptime"
 	"github.com/bluenviron/gortsplib/v4/pkg/sdp"
-	"github.com/bluenviron/gortsplib/v4/pkg/url"
 )
 
 // convert an URL into an address, in particular:
@@ -37,7 +36,7 @@ import (
 // * handle IPv6 with or without square brackets.
 // Adapted from net/http:
 // https://cs.opensource.google/go/go/+/refs/tags/go1.20.5:src/net/http/transport.go;l=2747
-func canonicalAddr(u *url.URL) string {
+func canonicalAddr(u *base.URL) string {
 	addr := u.Hostname()
 
 	port := u.Port()
@@ -56,10 +55,10 @@ func isAnyPort(p int) bool {
 	return p == 0 || p == 1
 }
 
-func findBaseURL(sd *sdp.SessionDescription, res *base.Response, u *url.URL) (*url.URL, error) {
+func findBaseURL(sd *sdp.SessionDescription, res *base.Response, u *base.URL) (*base.URL, error) {
 	// use global control attribute
 	if control, ok := sd.Attribute("control"); ok && control != "*" {
-		ret, err := url.Parse(control)
+		ret, err := base.ParseURL(control)
 		if err != nil {
 			return nil, fmt.Errorf("invalid control attribute: '%v'", control)
 		}
@@ -76,7 +75,7 @@ func findBaseURL(sd *sdp.SessionDescription, res *base.Response, u *url.URL) (*u
 			return nil, fmt.Errorf("invalid Content-Base: '%v'", cb)
 		}
 
-		ret, err := url.Parse(cb[0])
+		ret, err := base.ParseURL(cb[0])
 		if err != nil {
 			return nil, fmt.Errorf("invalid Content-Base: '%v'", cb)
 		}
@@ -138,23 +137,23 @@ func (s clientState) String() string {
 }
 
 type optionsReq struct {
-	url *url.URL
+	url *base.URL
 	res chan clientRes
 }
 
 type describeReq struct {
-	url *url.URL
+	url *base.URL
 	res chan clientRes
 }
 
 type announceReq struct {
-	url  *url.URL
+	url  *base.URL
 	desc *description.Session
 	res  chan clientRes
 }
 
 type setupReq struct {
-	baseURL  *url.URL
+	baseURL  *base.URL
 	media    *description.Media
 	rtpPort  int
 	rtcpPort int
@@ -288,7 +287,7 @@ type Client struct {
 	receiverReportPeriod time.Duration
 	checkTimeoutPeriod   time.Duration
 
-	connURL              *url.URL
+	connURL              *base.URL
 	ctx                  context.Context
 	ctxCancel            func()
 	state                clientState
@@ -299,8 +298,8 @@ type Client struct {
 	cseq                 int
 	optionsSent          bool
 	useGetParameter      bool
-	lastDescribeURL      *url.URL
-	baseURL              *url.URL
+	lastDescribeURL      *base.URL
+	baseURL              *base.URL
 	effectiveTransport   *Transport
 	medias               map[*description.Media]*clientMedia
 	tcpCallbackByChannel map[int]readFunc
@@ -422,7 +421,7 @@ func (c *Client) Start(scheme string, host string) error {
 
 	ctx, ctxCancel := context.WithCancel(context.Background())
 
-	c.connURL = &url.URL{
+	c.connURL = &base.URL{
 		Scheme: scheme,
 		Host:   host,
 	}
@@ -450,7 +449,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, desc *description.Session) error {
-	u, err := url.Parse(address)
+	u, err := base.ParseURL(address)
 	if err != nil {
 		return err
 	}
@@ -755,7 +754,7 @@ func (c *Client) trySwitchingProtocol() error {
 	return nil
 }
 
-func (c *Client) trySwitchingProtocol2(medi *description.Media, baseURL *url.URL) (*base.Response, error) {
+func (c *Client) trySwitchingProtocol2(medi *description.Media, baseURL *base.URL) (*base.Response, error) {
 	c.OnTransportSwitch(liberrors.ErrClientSwitchToTCP2{})
 
 	prevConnURL := c.connURL
@@ -1027,7 +1026,7 @@ func (c *Client) doKeepAlive() error {
 	return err
 }
 
-func (c *Client) doOptions(u *url.URL) (*base.Response, error) {
+func (c *Client) doOptions(u *base.URL) (*base.Response, error) {
 	err := c.checkState(map[clientState]struct{}{
 		clientStateInitial:   {},
 		clientStatePrePlay:   {},
@@ -1066,7 +1065,7 @@ func (c *Client) doOptions(u *url.URL) (*base.Response, error) {
 }
 
 // Options sends an OPTIONS request.
-func (c *Client) Options(u *url.URL) (*base.Response, error) {
+func (c *Client) Options(u *base.URL) (*base.Response, error) {
 	cres := make(chan clientRes)
 	select {
 	case c.chOptions <- optionsReq{url: u, res: cres}:
@@ -1078,7 +1077,7 @@ func (c *Client) Options(u *url.URL) (*base.Response, error) {
 	}
 }
 
-func (c *Client) doDescribe(u *url.URL) (*description.Session, *base.Response, error) {
+func (c *Client) doDescribe(u *base.URL) (*description.Session, *base.Response, error) {
 	err := c.checkState(map[clientState]struct{}{
 		clientStateInitial:   {},
 		clientStatePrePlay:   {},
@@ -1111,7 +1110,7 @@ func (c *Client) doDescribe(u *url.URL) (*description.Session, *base.Response, e
 			len(res.Header["Location"]) == 1 {
 			c.reset()
 
-			ru, err := url.Parse(res.Header["Location"][0])
+			ru, err := base.ParseURL(res.Header["Location"][0])
 			if err != nil {
 				return nil, nil, err
 			}
@@ -1120,7 +1119,7 @@ func (c *Client) doDescribe(u *url.URL) (*description.Session, *base.Response, e
 				ru.User = u.User
 			}
 
-			c.connURL = &url.URL{
+			c.connURL = &base.URL{
 				Scheme: ru.Scheme,
 				Host:   ru.Host,
 			}
@@ -1167,7 +1166,7 @@ func (c *Client) doDescribe(u *url.URL) (*description.Session, *base.Response, e
 }
 
 // Describe sends a DESCRIBE request.
-func (c *Client) Describe(u *url.URL) (*description.Session, *base.Response, error) {
+func (c *Client) Describe(u *base.URL) (*description.Session, *base.Response, error) {
 	cres := make(chan clientRes)
 	select {
 	case c.chDescribe <- describeReq{url: u, res: cres}:
@@ -1179,7 +1178,7 @@ func (c *Client) Describe(u *url.URL) (*description.Session, *base.Response, err
 	}
 }
 
-func (c *Client) doAnnounce(u *url.URL, desc *description.Session) (*base.Response, error) {
+func (c *Client) doAnnounce(u *base.URL, desc *description.Session) (*base.Response, error) {
 	err := c.checkState(map[clientState]struct{}{
 		clientStateInitial: {},
 	})
@@ -1224,7 +1223,7 @@ func (c *Client) doAnnounce(u *url.URL, desc *description.Session) (*base.Respon
 }
 
 // Announce sends an ANNOUNCE request.
-func (c *Client) Announce(u *url.URL, desc *description.Session) (*base.Response, error) {
+func (c *Client) Announce(u *base.URL, desc *description.Session) (*base.Response, error) {
 	cres := make(chan clientRes)
 	select {
 	case c.chAnnounce <- announceReq{url: u, desc: desc, res: cres}:
@@ -1237,7 +1236,7 @@ func (c *Client) Announce(u *url.URL, desc *description.Session) (*base.Response
 }
 
 func (c *Client) doSetup(
-	baseURL *url.URL,
+	baseURL *base.URL,
 	medi *description.Media,
 	rtpPort int,
 	rtcpPort int,
@@ -1538,7 +1537,7 @@ func (c *Client) findFreeChannelPair() int {
 // 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,
+	baseURL *base.URL,
 	media *description.Media,
 	rtpPort int,
 	rtcpPort int,
@@ -1561,7 +1560,7 @@ func (c *Client) Setup(
 }
 
 // SetupAll setups all the given medias.
-func (c *Client) SetupAll(baseURL *url.URL, medias []*description.Media) error {
+func (c *Client) SetupAll(baseURL *base.URL, medias []*description.Media) error {
 	for _, m := range medias {
 		_, err := c.Setup(baseURL, m, 0, 0)
 		if err != nil {
diff --git a/examples/client-query/main.go b/examples/client-query/main.go
index d199dc60..a3f6e50a 100644
--- a/examples/client-query/main.go
+++ b/examples/client-query/main.go
@@ -4,7 +4,7 @@ import (
 	"log"
 
 	"github.com/bluenviron/gortsplib/v4"
-	"github.com/bluenviron/gortsplib/v4/pkg/url"
+	"github.com/bluenviron/gortsplib/v4/pkg/base"
 )
 
 // This example shows how to
@@ -14,7 +14,7 @@ import (
 func main() {
 	c := gortsplib.Client{}
 
-	u, err := url.Parse("rtsp://localhost:8554/mypath")
+	u, err := base.ParseURL("rtsp://localhost:8554/mypath")
 	if err != nil {
 		panic(err)
 	}
diff --git a/examples/client-read-format-av1/main.go b/examples/client-read-format-av1/main.go
index bf4dc62f..b434a854 100644
--- a/examples/client-read-format-av1/main.go
+++ b/examples/client-read-format-av1/main.go
@@ -4,9 +4,9 @@ import (
 	"log"
 
 	"github.com/bluenviron/gortsplib/v4"
+	"github.com/bluenviron/gortsplib/v4/pkg/base"
 	"github.com/bluenviron/gortsplib/v4/pkg/format"
 	"github.com/bluenviron/gortsplib/v4/pkg/format/rtpvp9"
-	"github.com/bluenviron/gortsplib/v4/pkg/url"
 	"github.com/pion/rtp"
 )
 
@@ -19,7 +19,7 @@ func main() {
 	c := gortsplib.Client{}
 
 	// parse URL
-	u, err := url.Parse("rtsp://localhost:8554/mystream")
+	u, err := base.ParseURL("rtsp://localhost:8554/mystream")
 	if err != nil {
 		panic(err)
 	}
diff --git a/examples/client-read-format-g711/main.go b/examples/client-read-format-g711/main.go
index f643617f..f5548811 100644
--- a/examples/client-read-format-g711/main.go
+++ b/examples/client-read-format-g711/main.go
@@ -4,8 +4,8 @@ import (
 	"log"
 
 	"github.com/bluenviron/gortsplib/v4"
+	"github.com/bluenviron/gortsplib/v4/pkg/base"
 	"github.com/bluenviron/gortsplib/v4/pkg/format"
-	"github.com/bluenviron/gortsplib/v4/pkg/url"
 	"github.com/pion/rtp"
 )
 
@@ -18,7 +18,7 @@ func main() {
 	c := gortsplib.Client{}
 
 	// parse URL
-	u, err := url.Parse("rtsp://localhost:8554/mystream")
+	u, err := base.ParseURL("rtsp://localhost:8554/mystream")
 	if err != nil {
 		panic(err)
 	}
diff --git a/examples/client-read-format-g722/main.go b/examples/client-read-format-g722/main.go
index ae14e6b9..a6a34238 100644
--- a/examples/client-read-format-g722/main.go
+++ b/examples/client-read-format-g722/main.go
@@ -4,8 +4,8 @@ import (
 	"log"
 
 	"github.com/bluenviron/gortsplib/v4"
+	"github.com/bluenviron/gortsplib/v4/pkg/base"
 	"github.com/bluenviron/gortsplib/v4/pkg/format"
-	"github.com/bluenviron/gortsplib/v4/pkg/url"
 	"github.com/pion/rtp"
 )
 
@@ -18,7 +18,7 @@ func main() {
 	c := gortsplib.Client{}
 
 	// parse URL
-	u, err := url.Parse("rtsp://localhost:8554/mystream")
+	u, err := base.ParseURL("rtsp://localhost:8554/mystream")
 	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 a1fc3306..0df413d9 100644
--- a/examples/client-read-format-h264-convert-to-jpeg/main.go
+++ b/examples/client-read-format-h264-convert-to-jpeg/main.go
@@ -9,9 +9,9 @@ import (
 	"time"
 
 	"github.com/bluenviron/gortsplib/v4"
+	"github.com/bluenviron/gortsplib/v4/pkg/base"
 	"github.com/bluenviron/gortsplib/v4/pkg/format"
 	"github.com/bluenviron/gortsplib/v4/pkg/format/rtph264"
-	"github.com/bluenviron/gortsplib/v4/pkg/url"
 	"github.com/bluenviron/mediacommon/pkg/codecs/h264"
 	"github.com/pion/rtp"
 )
@@ -46,7 +46,7 @@ func main() {
 	c := gortsplib.Client{}
 
 	// parse URL
-	u, err := url.Parse("rtsp://localhost:8554/mystream")
+	u, err := base.ParseURL("rtsp://localhost:8554/mystream")
 	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 46c562c5..d0a2682c 100644
--- a/examples/client-read-format-h264-save-to-disk/main.go
+++ b/examples/client-read-format-h264-save-to-disk/main.go
@@ -4,9 +4,9 @@ import (
 	"log"
 
 	"github.com/bluenviron/gortsplib/v4"
+	"github.com/bluenviron/gortsplib/v4/pkg/base"
 	"github.com/bluenviron/gortsplib/v4/pkg/format"
 	"github.com/bluenviron/gortsplib/v4/pkg/format/rtph264"
-	"github.com/bluenviron/gortsplib/v4/pkg/url"
 	"github.com/pion/rtp"
 )
 
@@ -19,7 +19,7 @@ func main() {
 	c := gortsplib.Client{}
 
 	// parse URL
-	u, err := url.Parse("rtsp://localhost:8554/mystream")
+	u, err := base.ParseURL("rtsp://localhost:8554/mystream")
 	if err != nil {
 		panic(err)
 	}
diff --git a/examples/client-read-format-h264/main.go b/examples/client-read-format-h264/main.go
index 3ffb1301..fcdb7eaf 100644
--- a/examples/client-read-format-h264/main.go
+++ b/examples/client-read-format-h264/main.go
@@ -4,9 +4,9 @@ import (
 	"log"
 
 	"github.com/bluenviron/gortsplib/v4"
+	"github.com/bluenviron/gortsplib/v4/pkg/base"
 	"github.com/bluenviron/gortsplib/v4/pkg/format"
 	"github.com/bluenviron/gortsplib/v4/pkg/format/rtph264"
-	"github.com/bluenviron/gortsplib/v4/pkg/url"
 	"github.com/pion/rtp"
 )
 
@@ -22,7 +22,7 @@ func main() {
 	c := gortsplib.Client{}
 
 	// parse URL
-	u, err := url.Parse("rtsp://localhost:8554/mystream")
+	u, err := base.ParseURL("rtsp://localhost:8554/mystream")
 	if err != nil {
 		panic(err)
 	}
diff --git a/examples/client-read-format-h265-convert-to-jpeg/main.go b/examples/client-read-format-h265-convert-to-jpeg/main.go
index 60946ea2..6fdc057e 100644
--- a/examples/client-read-format-h265-convert-to-jpeg/main.go
+++ b/examples/client-read-format-h265-convert-to-jpeg/main.go
@@ -9,9 +9,9 @@ import (
 	"time"
 
 	"github.com/bluenviron/gortsplib/v4"
+	"github.com/bluenviron/gortsplib/v4/pkg/base"
 	"github.com/bluenviron/gortsplib/v4/pkg/format"
 	"github.com/bluenviron/gortsplib/v4/pkg/format/rtph265"
-	"github.com/bluenviron/gortsplib/v4/pkg/url"
 	"github.com/bluenviron/mediacommon/pkg/codecs/h265"
 	"github.com/pion/rtp"
 )
@@ -46,7 +46,7 @@ func main() {
 	c := gortsplib.Client{}
 
 	// parse URL
-	u, err := url.Parse("rtsp://localhost:8554/mystream")
+	u, err := base.ParseURL("rtsp://localhost:8554/mystream")
 	if err != nil {
 		panic(err)
 	}
diff --git a/examples/client-read-format-h265-save-to-disk/main.go b/examples/client-read-format-h265-save-to-disk/main.go
index d85bbde7..49e12503 100644
--- a/examples/client-read-format-h265-save-to-disk/main.go
+++ b/examples/client-read-format-h265-save-to-disk/main.go
@@ -4,9 +4,9 @@ import (
 	"log"
 
 	"github.com/bluenviron/gortsplib/v4"
+	"github.com/bluenviron/gortsplib/v4/pkg/base"
 	"github.com/bluenviron/gortsplib/v4/pkg/format"
 	"github.com/bluenviron/gortsplib/v4/pkg/format/rtph265"
-	"github.com/bluenviron/gortsplib/v4/pkg/url"
 	"github.com/pion/rtp"
 )
 
@@ -19,7 +19,7 @@ func main() {
 	c := gortsplib.Client{}
 
 	// parse URL
-	u, err := url.Parse("rtsp://localhost:8554/mystream")
+	u, err := base.ParseURL("rtsp://localhost:8554/mystream")
 	if err != nil {
 		panic(err)
 	}
diff --git a/examples/client-read-format-h265/main.go b/examples/client-read-format-h265/main.go
index 8773a323..2326f6e2 100644
--- a/examples/client-read-format-h265/main.go
+++ b/examples/client-read-format-h265/main.go
@@ -4,9 +4,9 @@ import (
 	"log"
 
 	"github.com/bluenviron/gortsplib/v4"
+	"github.com/bluenviron/gortsplib/v4/pkg/base"
 	"github.com/bluenviron/gortsplib/v4/pkg/format"
 	"github.com/bluenviron/gortsplib/v4/pkg/format/rtph265"
-	"github.com/bluenviron/gortsplib/v4/pkg/url"
 	"github.com/pion/rtp"
 )
 
@@ -22,7 +22,7 @@ func main() {
 	c := gortsplib.Client{}
 
 	// parse URL
-	u, err := url.Parse("rtsp://localhost:8554/mystream")
+	u, err := base.ParseURL("rtsp://localhost:8554/mystream")
 	if err != nil {
 		panic(err)
 	}
diff --git a/examples/client-read-format-lpcm/main.go b/examples/client-read-format-lpcm/main.go
index 4db53936..0f7a221e 100644
--- a/examples/client-read-format-lpcm/main.go
+++ b/examples/client-read-format-lpcm/main.go
@@ -4,8 +4,8 @@ import (
 	"log"
 
 	"github.com/bluenviron/gortsplib/v4"
+	"github.com/bluenviron/gortsplib/v4/pkg/base"
 	"github.com/bluenviron/gortsplib/v4/pkg/format"
-	"github.com/bluenviron/gortsplib/v4/pkg/url"
 	"github.com/pion/rtp"
 )
 
@@ -18,7 +18,7 @@ func main() {
 	c := gortsplib.Client{}
 
 	// parse URL
-	u, err := url.Parse("rtsp://localhost:8554/mystream")
+	u, err := base.ParseURL("rtsp://localhost:8554/mystream")
 	if err != nil {
 		panic(err)
 	}
diff --git a/examples/client-read-format-mjpeg/main.go b/examples/client-read-format-mjpeg/main.go
index 7a0b3e0b..866acabf 100644
--- a/examples/client-read-format-mjpeg/main.go
+++ b/examples/client-read-format-mjpeg/main.go
@@ -6,9 +6,9 @@ import (
 	"log"
 
 	"github.com/bluenviron/gortsplib/v4"
+	"github.com/bluenviron/gortsplib/v4/pkg/base"
 	"github.com/bluenviron/gortsplib/v4/pkg/format"
 	"github.com/bluenviron/gortsplib/v4/pkg/format/rtpmjpeg"
-	"github.com/bluenviron/gortsplib/v4/pkg/url"
 	"github.com/pion/rtp"
 )
 
@@ -22,7 +22,7 @@ func main() {
 	c := gortsplib.Client{}
 
 	// parse URL
-	u, err := url.Parse("rtsp://localhost:8554/mystream")
+	u, err := base.ParseURL("rtsp://localhost:8554/mystream")
 	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 15fd17a8..67772927 100644
--- a/examples/client-read-format-mpeg4audio-save-to-disk/main.go
+++ b/examples/client-read-format-mpeg4audio-save-to-disk/main.go
@@ -4,8 +4,8 @@ import (
 	"log"
 
 	"github.com/bluenviron/gortsplib/v4"
+	"github.com/bluenviron/gortsplib/v4/pkg/base"
 	"github.com/bluenviron/gortsplib/v4/pkg/format"
-	"github.com/bluenviron/gortsplib/v4/pkg/url"
 	"github.com/pion/rtp"
 )
 
@@ -18,7 +18,7 @@ func main() {
 	c := gortsplib.Client{}
 
 	// parse URL
-	u, err := url.Parse("rtsp://localhost:8554/mystream")
+	u, err := base.ParseURL("rtsp://localhost:8554/mystream")
 	if err != nil {
 		panic(err)
 	}
diff --git a/examples/client-read-format-mpeg4audio/main.go b/examples/client-read-format-mpeg4audio/main.go
index 1b539a03..57428e2d 100644
--- a/examples/client-read-format-mpeg4audio/main.go
+++ b/examples/client-read-format-mpeg4audio/main.go
@@ -4,8 +4,8 @@ import (
 	"log"
 
 	"github.com/bluenviron/gortsplib/v4"
+	"github.com/bluenviron/gortsplib/v4/pkg/base"
 	"github.com/bluenviron/gortsplib/v4/pkg/format"
-	"github.com/bluenviron/gortsplib/v4/pkg/url"
 	"github.com/pion/rtp"
 )
 
@@ -18,7 +18,7 @@ func main() {
 	c := gortsplib.Client{}
 
 	// parse URL
-	u, err := url.Parse("rtsp://localhost:8554/mystream")
+	u, err := base.ParseURL("rtsp://localhost:8554/mystream")
 	if err != nil {
 		panic(err)
 	}
diff --git a/examples/client-read-format-opus/main.go b/examples/client-read-format-opus/main.go
index 7a027bf3..b64d7c7c 100644
--- a/examples/client-read-format-opus/main.go
+++ b/examples/client-read-format-opus/main.go
@@ -4,8 +4,8 @@ import (
 	"log"
 
 	"github.com/bluenviron/gortsplib/v4"
+	"github.com/bluenviron/gortsplib/v4/pkg/base"
 	"github.com/bluenviron/gortsplib/v4/pkg/format"
-	"github.com/bluenviron/gortsplib/v4/pkg/url"
 	"github.com/pion/rtp"
 )
 
@@ -18,7 +18,7 @@ func main() {
 	c := gortsplib.Client{}
 
 	// parse URL
-	u, err := url.Parse("rtsp://localhost:8554/mystream")
+	u, err := base.ParseURL("rtsp://localhost:8554/mystream")
 	if err != nil {
 		panic(err)
 	}
diff --git a/examples/client-read-format-vp8/main.go b/examples/client-read-format-vp8/main.go
index 45a260a3..5ec4f689 100644
--- a/examples/client-read-format-vp8/main.go
+++ b/examples/client-read-format-vp8/main.go
@@ -4,9 +4,9 @@ import (
 	"log"
 
 	"github.com/bluenviron/gortsplib/v4"
+	"github.com/bluenviron/gortsplib/v4/pkg/base"
 	"github.com/bluenviron/gortsplib/v4/pkg/format"
 	"github.com/bluenviron/gortsplib/v4/pkg/format/rtpvp8"
-	"github.com/bluenviron/gortsplib/v4/pkg/url"
 	"github.com/pion/rtp"
 )
 
@@ -19,7 +19,7 @@ func main() {
 	c := gortsplib.Client{}
 
 	// parse URL
-	u, err := url.Parse("rtsp://localhost:8554/mystream")
+	u, err := base.ParseURL("rtsp://localhost:8554/mystream")
 	if err != nil {
 		panic(err)
 	}
diff --git a/examples/client-read-format-vp9/main.go b/examples/client-read-format-vp9/main.go
index 529fcf68..beb6bfa7 100644
--- a/examples/client-read-format-vp9/main.go
+++ b/examples/client-read-format-vp9/main.go
@@ -4,9 +4,9 @@ import (
 	"log"
 
 	"github.com/bluenviron/gortsplib/v4"
+	"github.com/bluenviron/gortsplib/v4/pkg/base"
 	"github.com/bluenviron/gortsplib/v4/pkg/format"
 	"github.com/bluenviron/gortsplib/v4/pkg/format/rtpvp9"
-	"github.com/bluenviron/gortsplib/v4/pkg/url"
 	"github.com/pion/rtp"
 )
 
@@ -19,7 +19,7 @@ func main() {
 	c := gortsplib.Client{}
 
 	// parse URL
-	u, err := url.Parse("rtsp://localhost:8554/mystream")
+	u, err := base.ParseURL("rtsp://localhost:8554/mystream")
 	if err != nil {
 		panic(err)
 	}
diff --git a/examples/client-read-options/main.go b/examples/client-read-options/main.go
index c1c1a251..be350391 100644
--- a/examples/client-read-options/main.go
+++ b/examples/client-read-options/main.go
@@ -5,9 +5,9 @@ import (
 	"time"
 
 	"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/url"
 	"github.com/pion/rtcp"
 	"github.com/pion/rtp"
 )
@@ -28,7 +28,7 @@ func main() {
 	}
 
 	// parse URL
-	u, err := url.Parse("rtsp://localhost:8554/mystream")
+	u, err := base.ParseURL("rtsp://localhost:8554/mystream")
 	if err != nil {
 		panic(err)
 	}
diff --git a/examples/client-read-pause/main.go b/examples/client-read-pause/main.go
index faa9aa17..6d97ff96 100644
--- a/examples/client-read-pause/main.go
+++ b/examples/client-read-pause/main.go
@@ -5,9 +5,9 @@ import (
 	"time"
 
 	"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/url"
 	"github.com/pion/rtcp"
 	"github.com/pion/rtp"
 )
@@ -22,7 +22,7 @@ func main() {
 	c := gortsplib.Client{}
 
 	// parse URL
-	u, err := url.Parse("rtsp://localhost:8554/mystream")
+	u, err := base.ParseURL("rtsp://localhost:8554/mystream")
 	if err != nil {
 		panic(err)
 	}
diff --git a/examples/client-read-republish/main.go b/examples/client-read-republish/main.go
index 148ba62b..ab9f2483 100644
--- a/examples/client-read-republish/main.go
+++ b/examples/client-read-republish/main.go
@@ -4,9 +4,9 @@ import (
 	"log"
 
 	"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/url"
 	"github.com/pion/rtp"
 )
 
@@ -19,7 +19,7 @@ func main() {
 	reader := gortsplib.Client{}
 
 	// parse source URL
-	sourceURL, err := url.Parse("rtsp://localhost:8554/mystream")
+	sourceURL, err := base.ParseURL("rtsp://localhost:8554/mystream")
 	if err != nil {
 		panic(err)
 	}
diff --git a/examples/client-read-timestamp/main.go b/examples/client-read-timestamp/main.go
index 8d164f67..4c177bad 100644
--- a/examples/client-read-timestamp/main.go
+++ b/examples/client-read-timestamp/main.go
@@ -4,9 +4,9 @@ import (
 	"log"
 
 	"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/url"
 	"github.com/pion/rtp"
 )
 
@@ -19,7 +19,7 @@ func main() {
 	c := gortsplib.Client{}
 
 	// parse URL
-	u, err := url.Parse("rtsp://localhost:8554/mystream")
+	u, err := base.ParseURL("rtsp://localhost:8554/mystream")
 	if err != nil {
 		panic(err)
 	}
diff --git a/examples/client-read/main.go b/examples/client-read/main.go
index 057baeab..6303ff27 100644
--- a/examples/client-read/main.go
+++ b/examples/client-read/main.go
@@ -4,9 +4,9 @@ import (
 	"log"
 
 	"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/url"
 	"github.com/pion/rtcp"
 	"github.com/pion/rtp"
 )
@@ -19,7 +19,7 @@ func main() {
 	c := gortsplib.Client{}
 
 	// parse URL
-	u, err := url.Parse("rtsp://localhost:8554/mystream")
+	u, err := base.ParseURL("rtsp://localhost:8554/mystream")
 	if err != nil {
 		panic(err)
 	}
diff --git a/examples/proxy/client.go b/examples/proxy/client.go
index e6b1023c..3b932760 100644
--- a/examples/proxy/client.go
+++ b/examples/proxy/client.go
@@ -5,9 +5,9 @@ import (
 	"time"
 
 	"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/url"
 	"github.com/pion/rtp"
 )
 
@@ -44,7 +44,7 @@ func (c *client) read() error {
 	rc := gortsplib.Client{}
 
 	// parse URL
-	u, err := url.Parse(existingStream)
+	u, err := base.ParseURL(existingStream)
 	if err != nil {
 		return err
 	}
diff --git a/pkg/auth/validate.go b/pkg/auth/validate.go
index 913be3fa..e43d52b9 100644
--- a/pkg/auth/validate.go
+++ b/pkg/auth/validate.go
@@ -8,7 +8,6 @@ import (
 
 	"github.com/bluenviron/gortsplib/v4/pkg/base"
 	"github.com/bluenviron/gortsplib/v4/pkg/headers"
-	"github.com/bluenviron/gortsplib/v4/pkg/url"
 )
 
 func md5Hex(in string) string {
@@ -68,7 +67,7 @@ func Validate(
 	req *base.Request,
 	user string,
 	pass string,
-	baseURL *url.URL,
+	baseURL *base.URL,
 	methods []headers.AuthMethod,
 	realm string,
 	nonce string,
diff --git a/pkg/base/path.go b/pkg/base/path.go
new file mode 100644
index 00000000..d6fa6d9f
--- /dev/null
+++ b/pkg/base/path.go
@@ -0,0 +1,15 @@
+package base
+
+import (
+	"strings"
+)
+
+// PathSplitQuery splits a path from a query.
+func PathSplitQuery(pathAndQuery string) (string, string) {
+	i := strings.Index(pathAndQuery, "?")
+	if i >= 0 {
+		return pathAndQuery[:i], pathAndQuery[i+1:]
+	}
+
+	return pathAndQuery, ""
+}
diff --git a/pkg/base/request.go b/pkg/base/request.go
index d859f19b..1c1fd979 100644
--- a/pkg/base/request.go
+++ b/pkg/base/request.go
@@ -5,8 +5,6 @@ import (
 	"bufio"
 	"fmt"
 	"strconv"
-
-	"github.com/bluenviron/gortsplib/v4/pkg/url"
 )
 
 const (
@@ -39,7 +37,7 @@ type Request struct {
 	Method Method
 
 	// request url
-	URL *url.URL
+	URL *URL
 
 	// map of header values
 	Header Header
@@ -67,7 +65,7 @@ func (req *Request) Unmarshal(br *bufio.Reader) error {
 	rawURL := string(byts[:len(byts)-1])
 
 	if rawURL != "*" {
-		ur, err := url.Parse(rawURL)
+		ur, err := ParseURL(rawURL)
 		if err != nil {
 			return fmt.Errorf("invalid URL (%v)", rawURL)
 		}
diff --git a/pkg/base/url.go b/pkg/base/url.go
new file mode 100644
index 00000000..1205f1f1
--- /dev/null
+++ b/pkg/base/url.go
@@ -0,0 +1,105 @@
+package base
+
+import (
+	"fmt"
+	"net/url"
+	"regexp"
+	"strings"
+)
+
+// URL is a RTSP URL.
+// This is basically an HTTP URL with some additional functions to handle
+// control attributes.
+type URL url.URL
+
+var escapeRegexp = regexp.MustCompile(`^(.+?)://(.*?)@(.*?)/(.*?)$`)
+
+// ParseURL parses a RTSP URL.
+func ParseURL(s string) (*URL, error) {
+	// https://github.com/golang/go/issues/30611
+	m := escapeRegexp.FindStringSubmatch(s)
+	if m != nil {
+		m[3] = strings.ReplaceAll(m[3], "%25", "%")
+		m[3] = strings.ReplaceAll(m[3], "%", "%25")
+		s = m[1] + "://" + m[2] + "@" + m[3] + "/" + m[4]
+	}
+
+	u, err := url.Parse(s)
+	if err != nil {
+		return nil, err
+	}
+
+	if u.Scheme != "rtsp" && u.Scheme != "rtsps" {
+		return nil, fmt.Errorf("unsupported scheme '%s'", u.Scheme)
+	}
+
+	if u.Opaque != "" {
+		return nil, fmt.Errorf("URLs with opaque data are not supported")
+	}
+
+	if u.Fragment != "" {
+		return nil, fmt.Errorf("URLs with fragments are not supported")
+	}
+
+	return (*URL)(u), nil
+}
+
+// String implements fmt.Stringer.
+func (u *URL) String() string {
+	return (*url.URL)(u).String()
+}
+
+// Clone clones a URL.
+func (u *URL) Clone() *URL {
+	return (*URL)(&url.URL{
+		Scheme:     u.Scheme,
+		User:       u.User,
+		Host:       u.Host,
+		Path:       u.Path,
+		RawPath:    u.RawPath,
+		ForceQuery: u.ForceQuery,
+		RawQuery:   u.RawQuery,
+	})
+}
+
+// CloneWithoutCredentials clones a URL without its credentials.
+func (u *URL) CloneWithoutCredentials() *URL {
+	return (*URL)(&url.URL{
+		Scheme:     u.Scheme,
+		Host:       u.Host,
+		Path:       u.Path,
+		RawPath:    u.RawPath,
+		ForceQuery: u.ForceQuery,
+		RawQuery:   u.RawQuery,
+	})
+}
+
+// RTSPPathAndQuery returns the path and query of a RTSP URL.
+func (u *URL) RTSPPathAndQuery() (string, bool) {
+	var pathAndQuery string
+	if u.RawPath != "" {
+		pathAndQuery = u.RawPath
+	} else {
+		pathAndQuery = u.Path
+	}
+	if u.RawQuery != "" {
+		pathAndQuery += "?" + u.RawQuery
+	}
+
+	return pathAndQuery, true
+}
+
+// Hostname returns u.Host, stripping any valid port number if present.
+//
+// If the result is enclosed in square brackets, as literal IPv6 addresses are,
+// the square brackets are removed from the result.
+func (u *URL) Hostname() string {
+	return (*url.URL)(u).Hostname()
+}
+
+// Port returns the port part of u.Host, without the leading colon.
+//
+// If u.Host doesn't contain a valid numeric port, Port returns an empty string.
+func (u *URL) Port() string {
+	return (*url.URL)(u).Port()
+}
diff --git a/pkg/description/media.go b/pkg/description/media.go
index 88d06efd..11a42c50 100644
--- a/pkg/description/media.go
+++ b/pkg/description/media.go
@@ -12,8 +12,8 @@ import (
 
 	psdp "github.com/pion/sdp/v3"
 
+	"github.com/bluenviron/gortsplib/v4/pkg/base"
 	"github.com/bluenviron/gortsplib/v4/pkg/format"
-	"github.com/bluenviron/gortsplib/v4/pkg/url"
 )
 
 var smartRegexp = regexp.MustCompile("^([0-9]+) (.*?)/90000")
@@ -252,7 +252,7 @@ func (m Media) Marshal() *psdp.MediaDescription {
 }
 
 // URL returns the absolute URL of the media.
-func (m Media) URL(contentBase *url.URL) (*url.URL, error) {
+func (m Media) URL(contentBase *base.URL) (*base.URL, error) {
 	if contentBase == nil {
 		return nil, fmt.Errorf("Content-Base header not provided")
 	}
@@ -265,7 +265,7 @@ func (m Media) URL(contentBase *url.URL) (*url.URL, error) {
 	// control attribute contains an absolute path
 	if strings.HasPrefix(m.Control, "rtsp://") ||
 		strings.HasPrefix(m.Control, "rtsps://") {
-		ur, err := url.Parse(m.Control)
+		ur, err := base.ParseURL(m.Control)
 		if err != nil {
 			return nil, err
 		}
@@ -285,7 +285,7 @@ func (m Media) URL(contentBase *url.URL) (*url.URL, error) {
 		strURL += "/"
 	}
 
-	ur, _ := url.Parse(strURL + m.Control)
+	ur, _ := base.ParseURL(strURL + m.Control)
 	return ur, nil
 }
 
diff --git a/pkg/description/session.go b/pkg/description/session.go
index 778865a3..aa4f6911 100644
--- a/pkg/description/session.go
+++ b/pkg/description/session.go
@@ -6,8 +6,8 @@ import (
 
 	psdp "github.com/pion/sdp/v3"
 
+	"github.com/bluenviron/gortsplib/v4/pkg/base"
 	"github.com/bluenviron/gortsplib/v4/pkg/sdp"
-	"github.com/bluenviron/gortsplib/v4/pkg/url"
 )
 
 func atLeastOneHasMID(medias []*Media) bool {
@@ -43,7 +43,7 @@ type SessionFECGroup []string
 // Session is the description of a RTSP stream.
 type Session struct {
 	// Base URL of the stream (read only).
-	BaseURL *url.URL
+	BaseURL *base.URL
 
 	// Title of the stream (optional).
 	Title string
diff --git a/pkg/url/path.go b/pkg/url/path.go
index 85e880a8..5376c42e 100644
--- a/pkg/url/path.go
+++ b/pkg/url/path.go
@@ -1,15 +1,12 @@
 package url
 
 import (
-	"strings"
+	"github.com/bluenviron/gortsplib/v4/pkg/base"
 )
 
 // PathSplitQuery splits a path from a query.
+//
+// Deprecated: replaced by base.PathSplitQuery
 func PathSplitQuery(pathAndQuery string) (string, string) {
-	i := strings.Index(pathAndQuery, "?")
-	if i >= 0 {
-		return pathAndQuery[:i], pathAndQuery[i+1:]
-	}
-
-	return pathAndQuery, ""
+	return base.PathSplitQuery(pathAndQuery)
 }
diff --git a/pkg/url/url.go b/pkg/url/url.go
index ff4601e3..af25e117 100644
--- a/pkg/url/url.go
+++ b/pkg/url/url.go
@@ -1,106 +1,20 @@
-// Package url contains the URL structure.
+// Package url is deprecated.
 package url
 
 import (
-	"fmt"
-	"net/url"
-	"regexp"
-	"strings"
+	"github.com/bluenviron/gortsplib/v4/pkg/base"
 )
 
 // URL is a RTSP URL.
 // This is basically an HTTP URL with some additional functions to handle
 // control attributes.
-type URL url.URL
-
-var escapeRegexp = regexp.MustCompile(`^(.+?)://(.*?)@(.*?)/(.*?)$`)
-
-// Parse parses a RTSP URL.
-func Parse(s string) (*URL, error) {
-	// https://github.com/golang/go/issues/30611
-	m := escapeRegexp.FindStringSubmatch(s)
-	if m != nil {
-		m[3] = strings.ReplaceAll(m[3], "%25", "%")
-		m[3] = strings.ReplaceAll(m[3], "%", "%25")
-		s = m[1] + "://" + m[2] + "@" + m[3] + "/" + m[4]
-	}
-
-	u, err := url.Parse(s)
-	if err != nil {
-		return nil, err
-	}
-
-	if u.Scheme != "rtsp" && u.Scheme != "rtsps" {
-		return nil, fmt.Errorf("unsupported scheme '%s'", u.Scheme)
-	}
-
-	if u.Opaque != "" {
-		return nil, fmt.Errorf("URLs with opaque data are not supported")
-	}
-
-	if u.Fragment != "" {
-		return nil, fmt.Errorf("URLs with fragments are not supported")
-	}
-
-	return (*URL)(u), nil
-}
-
-// String implements fmt.Stringer.
-func (u *URL) String() string {
-	return (*url.URL)(u).String()
-}
-
-// Clone clones a URL.
-func (u *URL) Clone() *URL {
-	return (*URL)(&url.URL{
-		Scheme:     u.Scheme,
-		User:       u.User,
-		Host:       u.Host,
-		Path:       u.Path,
-		RawPath:    u.RawPath,
-		ForceQuery: u.ForceQuery,
-		RawQuery:   u.RawQuery,
-	})
-}
-
-// CloneWithoutCredentials clones a URL without its credentials.
-func (u *URL) CloneWithoutCredentials() *URL {
-	return (*URL)(&url.URL{
-		Scheme:     u.Scheme,
-		Host:       u.Host,
-		Path:       u.Path,
-		RawPath:    u.RawPath,
-		ForceQuery: u.ForceQuery,
-		RawQuery:   u.RawQuery,
-	})
-}
-
-// RTSPPathAndQuery returns the path and query of a RTSP URL.
-func (u *URL) RTSPPathAndQuery() (string, bool) {
-	var pathAndQuery string
-	if u.RawPath != "" {
-		pathAndQuery = u.RawPath
-	} else {
-		pathAndQuery = u.Path
-	}
-	if u.RawQuery != "" {
-		pathAndQuery += "?" + u.RawQuery
-	}
-
-	return pathAndQuery, true
-}
-
-// Hostname returns u.Host, stripping any valid port number if present.
 //
-// If the result is enclosed in square brackets, as literal IPv6 addresses are,
-// the square brackets are removed from the result.
-func (u *URL) Hostname() string {
-	return (*url.URL)(u).Hostname()
-}
+// Deprecated: replaced by base.URL
+type URL = base.URL
 
-// Port returns the port part of u.Host, without the leading colon.
+// Parse parses a RTSP URL.
 //
-// If u.Host doesn't contain a valid numeric port, Port returns an empty string.
-func (u *URL) Port() string {
-	return (*url.URL)(u).Port()
+// Deprecated: replaced by base.ParseURL
+func Parse(s string) (*URL, error) {
+	return base.ParseURL(s)
 }
diff --git a/server_conn.go b/server_conn.go
index eac113bf..64a5cd93 100644
--- a/server_conn.go
+++ b/server_conn.go
@@ -14,7 +14,6 @@ import (
 	"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/url"
 )
 
 func getSessionID(header base.Header) string {
@@ -24,7 +23,7 @@ func getSessionID(header base.Header) string {
 	return ""
 }
 
-func serverSideDescription(d *description.Session, contentBase *url.URL) *description.Session {
+func serverSideDescription(d *description.Session, contentBase *base.URL) *description.Session {
 	out := &description.Session{
 		Title:     d.Title,
 		FECGroups: d.FECGroups,
@@ -208,10 +207,15 @@ func (sc *ServerConn) handleRequestInner(req *base.Request) (*base.Response, err
 	if cseq, ok := req.Header["CSeq"]; !ok || len(cseq) != 1 {
 		return &base.Response{
 			StatusCode: base.StatusBadRequest,
-			Header:     base.Header{},
 		}, liberrors.ErrServerCSeqMissing{}
 	}
 
+	if req.Method != base.Options && req.URL == nil {
+		return &base.Response{
+			StatusCode: base.StatusBadRequest,
+		}, liberrors.ErrServerInvalidPath{}
+	}
+
 	sxID := getSessionID(req.Header)
 
 	var path string
@@ -225,7 +229,7 @@ func (sc *ServerConn) handleRequestInner(req *base.Request) (*base.Response, err
 			}, liberrors.ErrServerInvalidPath{}
 		}
 
-		path, query = url.PathSplitQuery(pathAndQuery)
+		path, query = base.PathSplitQuery(pathAndQuery)
 	}
 
 	switch req.Method {
diff --git a/server_session.go b/server_session.go
index 862407c6..bfe8163e 100644
--- a/server_session.go
+++ b/server_session.go
@@ -21,7 +21,6 @@ import (
 	"github.com/bluenviron/gortsplib/v4/pkg/liberrors"
 	"github.com/bluenviron/gortsplib/v4/pkg/rtptime"
 	"github.com/bluenviron/gortsplib/v4/pkg/sdp"
-	"github.com/bluenviron/gortsplib/v4/pkg/url"
 )
 
 type readFunc func([]byte)
@@ -35,7 +34,7 @@ func stringsReverseIndex(s, substr string) int {
 	return -1
 }
 
-func serverParseURLForPlay(u *url.URL) (string, string, string, error) {
+func serverParseURLForPlay(u *base.URL) (string, string, string, error) {
 	pathAndQuery, ok := u.RTSPPathAndQuery()
 	if !ok {
 		return "", "", "", liberrors.ErrServerInvalidPath{}
@@ -47,18 +46,18 @@ func serverParseURLForPlay(u *url.URL) (string, string, string, error) {
 			return "", "", "", liberrors.ErrServerPathNoSlash{}
 		}
 
-		path, query := url.PathSplitQuery(pathAndQuery[:len(pathAndQuery)-1])
+		path, query := base.PathSplitQuery(pathAndQuery[:len(pathAndQuery)-1])
 		return path, query, "", nil
 	}
 
 	var trackID string
 	pathAndQuery, trackID = pathAndQuery[:i], pathAndQuery[i+len("/trackID="):]
-	path, query := url.PathSplitQuery(pathAndQuery)
+	path, query := base.PathSplitQuery(pathAndQuery)
 	return path, query, trackID, nil
 }
 
-func recordBaseURL(u *url.URL, path string, query string) *url.URL {
-	baseURL := &url.URL{
+func recordBaseURL(u *base.URL, path string, query string) *base.URL {
+	baseURL := &base.URL{
 		Scheme:   u.Scheme,
 		Host:     u.Host,
 		Path:     path,
@@ -74,7 +73,7 @@ func recordBaseURL(u *url.URL, path string, query string) *url.URL {
 	return baseURL
 }
 
-func findMediaByURL(medias []*description.Media, baseURL *url.URL, u *url.URL) *description.Media {
+func findMediaByURL(medias []*description.Media, baseURL *base.URL, u *base.URL) *description.Media {
 	for _, media := range medias {
 		u1, err := media.URL(baseURL)
 		if err == nil && u1.String() == u.String() {
@@ -123,14 +122,14 @@ func generateRTPInfo(
 	setuppedMediasOrdered []*serverSessionMedia,
 	setuppedStream *ServerStream,
 	setuppedPath string,
-	u *url.URL,
+	u *base.URL,
 ) (headers.RTPInfo, bool) {
 	var ri headers.RTPInfo
 
 	for _, sm := range setuppedMediasOrdered {
 		entry := setuppedStream.rtpInfoEntry(sm.media, now)
 		if entry != nil {
-			entry.URL = (&url.URL{
+			entry.URL = (&base.URL{
 				Scheme: u.Scheme,
 				Host:   u.Host,
 				Path: setuppedPath + "/trackID=" +
@@ -526,7 +525,7 @@ func (ss *ServerSession) handleRequestInner(sc *ServerConn, req *base.Request) (
 			pathAndQuery = strings.TrimSuffix(pathAndQuery, "/")
 		}
 
-		path, query = url.PathSplitQuery(pathAndQuery)
+		path, query = base.PathSplitQuery(pathAndQuery)
 	}
 
 	switch req.Method {


Test patch:
diff --git a/client_play_test.go b/client_play_test.go
index 8a86bcc2..127feefc 100644
--- a/client_play_test.go
+++ b/client_play_test.go
@@ -21,7 +21,6 @@ import (
 	"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/url"
 	"github.com/bluenviron/mediacommon/pkg/codecs/mpeg4audio"
 )
 
@@ -61,7 +60,7 @@ func mustMarshalPacketRTCP(pkt rtcp.Packet) []byte {
 }
 
 func readAll(c *Client, ur string, cb func(*description.Media, format.Format, *rtp.Packet)) error {
-	u, err := url.Parse(ur)
+	u, err := base.ParseURL(ur)
 	if err != nil {
 		return err
 	}
@@ -503,7 +502,7 @@ func TestClientPlay(t *testing.T) {
 				}(),
 			}
 
-			u, err := url.Parse(scheme + "://" + listenIP + ":8554/test/stream?param=value")
+			u, err := base.ParseURL(scheme + "://" + listenIP + ":8554/test/stream?param=value")
 			require.NoError(t, err)
 
 			err = c.Start(u.Scheme, u.Host)
@@ -658,7 +657,7 @@ func TestClientPlayPartial(t *testing.T) {
 		}(),
 	}
 
-	u, err := url.Parse("rtsp://" + listenIP + ":8554/teststream")
+	u, err := base.ParseURL("rtsp://" + listenIP + ":8554/teststream")
 	require.NoError(t, err)
 
 	err = c.Start(u.Scheme, u.Host)
@@ -2610,7 +2609,7 @@ func TestClientPlaySeek(t *testing.T) {
 		}(),
 	}
 
-	u, err := url.Parse("rtsp://localhost:8554/teststream")
+	u, err := base.ParseURL("rtsp://localhost:8554/teststream")
 	require.NoError(t, err)
 
 	err = c.Start(u.Scheme, u.Host)
diff --git a/client_record_test.go b/client_record_test.go
index 21716a49..039dbe57 100644
--- a/client_record_test.go
+++ b/client_record_test.go
@@ -20,7 +20,6 @@ import (
 	"github.com/bluenviron/gortsplib/v4/pkg/format"
 	"github.com/bluenviron/gortsplib/v4/pkg/headers"
 	"github.com/bluenviron/gortsplib/v4/pkg/sdp"
-	"github.com/bluenviron/gortsplib/v4/pkg/url"
 )
 
 var testH264Media = &description.Media{
@@ -74,7 +73,7 @@ func ntpTimeGoToRTCP(v time.Time) uint64 {
 }
 
 func record(c *Client, ur string, medias []*description.Media, cb func(*description.Media, rtcp.Packet)) error {
-	u, err := url.Parse(ur)
+	u, err := base.ParseURL(ur)
 	if err != nil {
 		return err
 	}
diff --git a/client_test.go b/client_test.go
index e9469170..54af718f 100644
--- a/client_test.go
+++ b/client_test.go
@@ -12,11 +12,10 @@ 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/url"
 )
 
-func mustParseURL(s string) *url.URL {
-	u, err := url.Parse(s)
+func mustParseURL(s string) *base.URL {
+	u, err := base.ParseURL(s)
 	if err != nil {
 		panic(err)
 	}
@@ -82,7 +81,7 @@ func TestClientTLSSetServerName(t *testing.T) {
 		require.EqualError(t, err, "remote error: tls: bad certificate")
 	}()
 
-	u, err := url.Parse("rtsps://localhost:8554/stream")
+	u, err := base.ParseURL("rtsps://localhost:8554/stream")
 	require.NoError(t, err)
 
 	c := Client{
@@ -100,7 +99,7 @@ func TestClientTLSSetServerName(t *testing.T) {
 }
 
 func TestClientClose(t *testing.T) {
-	u, err := url.Parse("rtsp://localhost:8554/teststream")
+	u, err := base.ParseURL("rtsp://localhost:8554/teststream")
 	require.NoError(t, err)
 
 	c := Client{}
@@ -158,7 +157,7 @@ func TestClientCloseDuringRequest(t *testing.T) {
 		<-releaseConn
 	}()
 
-	u, err := url.Parse("rtsp://localhost:8554/teststream")
+	u, err := base.ParseURL("rtsp://localhost:8554/teststream")
 	require.NoError(t, err)
 
 	c := Client{}
@@ -227,7 +226,7 @@ func TestClientSession(t *testing.T) {
 		require.NoError(t, err)
 	}()
 
-	u, err := url.Parse("rtsp://localhost:8554/stream")
+	u, err := base.ParseURL("rtsp://localhost:8554/stream")
 	require.NoError(t, err)
 
 	c := Client{}
@@ -303,7 +302,7 @@ func TestClientAuth(t *testing.T) {
 		require.NoError(t, err)
 	}()
 
-	u, err := url.Parse("rtsp://myuser:mypass@localhost:8554/stream")
+	u, err := base.ParseURL("rtsp://myuser:mypass@localhost:8554/stream")
 	require.NoError(t, err)
 
 	c := Client{}
@@ -378,7 +377,7 @@ func TestClientCSeq(t *testing.T) {
 				}
 			}()
 
-			u, err := url.Parse("rtsp://localhost:8554/teststream")
+			u, err := base.ParseURL("rtsp://localhost:8554/teststream")
 			require.NoError(t, err)
 
 			c := Client{}
@@ -440,7 +439,7 @@ func TestClientDescribeCharset(t *testing.T) {
 		require.NoError(t, err)
 	}()
 
-	u, err := url.Parse("rtsp://localhost:8554/teststream")
+	u, err := base.ParseURL("rtsp://localhost:8554/teststream")
 	require.NoError(t, err)
 
 	c := Client{}
@@ -525,7 +524,7 @@ func TestClientReplyToServerRequest(t *testing.T) {
 				}
 			}()
 
-			u, err := url.Parse("rtsp://localhost:8554/stream")
+			u, err := base.ParseURL("rtsp://localhost:8554/stream")
 			require.NoError(t, err)
 
 			c := Client{}
diff --git a/pkg/auth/auth_test.go b/pkg/auth/auth_test.go
index 55df2e83..2b0beffd 100644
--- a/pkg/auth/auth_test.go
+++ b/pkg/auth/auth_test.go
@@ -7,11 +7,10 @@ import (
 
 	"github.com/bluenviron/gortsplib/v4/pkg/base"
 	"github.com/bluenviron/gortsplib/v4/pkg/headers"
-	"github.com/bluenviron/gortsplib/v4/pkg/url"
 )
 
-func mustParseURL(s string) *url.URL {
-	u, err := url.Parse(s)
+func mustParseURL(s string) *base.URL {
+	u, err := base.ParseURL(s)
 	if err != nil {
 		panic(err)
 	}
diff --git a/pkg/url/path_test.go b/pkg/base/path_test.go
similarity index 96%
rename from pkg/url/path_test.go
rename to pkg/base/path_test.go
index 7485197d..c110d412 100644
--- a/pkg/url/path_test.go
+++ b/pkg/base/path_test.go
@@ -1,4 +1,4 @@
-package url
+package base
 
 import (
 	"testing"
diff --git a/pkg/base/request_test.go b/pkg/base/request_test.go
index d39cdf11..88ad6c80 100644
--- a/pkg/base/request_test.go
+++ b/pkg/base/request_test.go
@@ -6,18 +6,8 @@ import (
 	"testing"
 
 	"github.com/stretchr/testify/require"
-
-	"github.com/bluenviron/gortsplib/v4/pkg/url"
 )
 
-func mustParseURL(s string) *url.URL {
-	u, err := url.Parse(s)
-	if err != nil {
-		panic(err)
-	}
-	return u
-}
-
 var casesRequest = []struct {
 	name string
 	byts []byte
diff --git a/pkg/url/url_test.go b/pkg/base/url_test.go
similarity index 74%
rename from pkg/url/url_test.go
rename to pkg/base/url_test.go
index 7bc12985..84878649 100644
--- a/pkg/url/url_test.go
+++ b/pkg/base/url_test.go
@@ -1,4 +1,4 @@
-package url
+package base
 
 import (
 	"net/url"
@@ -7,15 +7,15 @@ import (
 	"github.com/stretchr/testify/require"
 )
 
-func mustParse(s string) *URL {
-	u, err := Parse(s)
+func mustParseURL(s string) *URL {
+	u, err := ParseURL(s)
 	if err != nil {
 		panic(err)
 	}
 	return u
 }
 
-func TestURLParse(t *testing.T) {
+func TestParseURL(t *testing.T) {
 	for _, ca := range []struct {
 		name string
 		enc  string
@@ -33,7 +33,7 @@ func TestURLParse(t *testing.T) {
 		},
 	} {
 		t.Run(ca.name, func(t *testing.T) {
-			u, err := Parse(ca.enc)
+			u, err := ParseURL(ca.enc)
 			require.NoError(t, err)
 			require.Equal(t, ca.u, u)
 		})
@@ -68,14 +68,14 @@ func TestURLParseErrors(t *testing.T) {
 		},
 	} {
 		t.Run(ca.name, func(t *testing.T) {
-			_, err := Parse(ca.enc)
+			_, err := ParseURL(ca.enc)
 			require.EqualError(t, err, ca.err)
 		})
 	}
 }
 
 func TestURLClone(t *testing.T) {
-	u := mustParse("rtsp://localhost:8554/test/stream")
+	u := mustParseURL("rtsp://localhost:8554/test/stream")
 	u2 := u.Clone()
 	u.Host = "otherhost"
 
@@ -93,7 +93,7 @@ func TestURLClone(t *testing.T) {
 }
 
 func TestURLCloneWithoutCredentials(t *testing.T) {
-	u := mustParse("rtsp://user:pass@localhost:8554/test/stream")
+	u := mustParseURL("rtsp://user:pass@localhost:8554/test/stream")
 	u2 := u.CloneWithoutCredentials()
 	u.Host = "otherhost"
 
@@ -119,42 +119,42 @@ func TestURLRTSPPathAndQuery(t *testing.T) {
 	}{
 		{
 			"standard",
-			mustParse("rtsp://localhost:8554/teststream/trackID=1"),
+			mustParseURL("rtsp://localhost:8554/teststream/trackID=1"),
 			"/teststream/trackID=1",
 		},
 		{
 			"subpath",
-			mustParse("rtsp://localhost:8554/test/stream/trackID=1"),
+			mustParseURL("rtsp://localhost:8554/test/stream/trackID=1"),
 			"/test/stream/trackID=1",
 		},
 		{
 			"path and query",
-			mustParse("rtsp://192.168.1.99:554/test?user=tmp&password=BagRep1&channel=1&stream=0.sdp/trackID=1"),
+			mustParseURL("rtsp://192.168.1.99:554/test?user=tmp&password=BagRep1&channel=1&stream=0.sdp/trackID=1"),
 			"/test?user=tmp&password=BagRep1&channel=1&stream=0.sdp/trackID=1",
 		},
 		{
 			"path and query with special chars",
-			mustParse("rtsp://192.168.1.99:554/te!st?user=tmp&password=BagRep1!&channel=1&stream=0.sdp/trackID=1"),
+			mustParseURL("rtsp://192.168.1.99:554/te!st?user=tmp&password=BagRep1!&channel=1&stream=0.sdp/trackID=1"),
 			"/te!st?user=tmp&password=BagRep1!&channel=1&stream=0.sdp/trackID=1",
 		},
 		{
 			"path and query attached",
-			mustParse("rtsp://192.168.1.99:554/user=tmp&password=BagRep1!&channel=1&stream=0.sdp/trackID=1"),
+			mustParseURL("rtsp://192.168.1.99:554/user=tmp&password=BagRep1!&channel=1&stream=0.sdp/trackID=1"),
 			"/user=tmp&password=BagRep1!&channel=1&stream=0.sdp/trackID=1",
 		},
 		{
 			"no path",
-			mustParse("rtsp://192.168.1.99:554"),
+			mustParseURL("rtsp://192.168.1.99:554"),
 			"",
 		},
 		{
 			"single slash",
-			mustParse("rtsp://192.168.1.99:554/"),
+			mustParseURL("rtsp://192.168.1.99:554/"),
 			"/",
 		},
 		{
 			"no slash and query",
-			mustParse("rtsp://192.168.1.99:554?testing"),
+			mustParseURL("rtsp://192.168.1.99:554?testing"),
 			"?testing",
 		},
 	} {
diff --git a/pkg/conn/conn_test.go b/pkg/conn/conn_test.go
index c9b499cb..77b6670d 100644
--- a/pkg/conn/conn_test.go
+++ b/pkg/conn/conn_test.go
@@ -7,11 +7,10 @@ import (
 	"github.com/stretchr/testify/require"
 
 	"github.com/bluenviron/gortsplib/v4/pkg/base"
-	"github.com/bluenviron/gortsplib/v4/pkg/url"
 )
 
-func mustParseURL(s string) *url.URL {
-	u, err := url.Parse(s)
+func mustParseURL(s string) *base.URL {
+	u, err := base.ParseURL(s)
 	if err != nil {
 		panic(err)
 	}
@@ -32,7 +31,7 @@ func TestRead(t *testing.T) {
 				"\r\n"),
 			&base.Request{
 				Method: base.Describe,
-				URL: &url.URL{
+				URL: &base.URL{
 					Scheme: "rtsp",
 					Host:   "example.com",
 					Path:   "/media.mp4",
diff --git a/pkg/description/media_test.go b/pkg/description/media_test.go
index 73be8344..3af21991 100644
--- a/pkg/description/media_test.go
+++ b/pkg/description/media_test.go
@@ -5,13 +5,13 @@ import (
 
 	"github.com/stretchr/testify/require"
 
+	"github.com/bluenviron/gortsplib/v4/pkg/base"
 	"github.com/bluenviron/gortsplib/v4/pkg/format"
 	"github.com/bluenviron/gortsplib/v4/pkg/sdp"
-	"github.com/bluenviron/gortsplib/v4/pkg/url"
 )
 
-func mustParseURL(s string) *url.URL {
-	u, err := url.Parse(s)
+func mustParseURL(s string) *base.URL {
+	u, err := base.ParseURL(s)
 	if err != nil {
 		panic(err)
 	}
@@ -22,8 +22,8 @@ func TestMediaURL(t *testing.T) {
 	for _, ca := range []struct {
 		name    string
 		sdp     []byte
-		baseURL *url.URL
-		ur      *url.URL
+		baseURL *base.URL
+		ur      *base.URL
 	}{
 		{
 			"missing control",
diff --git a/server_play_test.go b/server_play_test.go
index c03616e2..12aeb0a4 100644
--- a/server_play_test.go
+++ b/server_play_test.go
@@ -23,7 +23,6 @@ import (
 	"github.com/bluenviron/gortsplib/v4/pkg/format"
 	"github.com/bluenviron/gortsplib/v4/pkg/headers"
 	"github.com/bluenviron/gortsplib/v4/pkg/sdp"
-	"github.com/bluenviron/gortsplib/v4/pkg/url"
 )
 
 func uintPtr(v uint) *uint {
@@ -1978,7 +1977,7 @@ func TestServerPlayAdditionalInfos(t *testing.T) {
 	require.True(t, strings.HasPrefix(mustParseURL((*rtpInfo)[0].URL).Path, "/teststream/trackID="))
 	require.Equal(t, &headers.RTPInfo{
 		&headers.RTPInfoEntry{
-			URL: (&url.URL{
+			URL: (&base.URL{
 				Scheme: "rtsp",
 				Host:   "localhost:8554",
 				Path:   mustParseURL((*rtpInfo)[0].URL).Path,
@@ -2008,7 +2007,7 @@ func TestServerPlayAdditionalInfos(t *testing.T) {
 	require.True(t, strings.HasPrefix(mustParseURL((*rtpInfo)[0].URL).Path, "/teststream/trackID="))
 	require.Equal(t, &headers.RTPInfo{
 		&headers.RTPInfoEntry{
-			URL: (&url.URL{
+			URL: (&base.URL{
 				Scheme: "rtsp",
 				Host:   "localhost:8554",
 				Path:   mustParseURL((*rtpInfo)[0].URL).Path,
@@ -2017,7 +2016,7 @@ func TestServerPlayAdditionalInfos(t *testing.T) {
 			Timestamp:      (*rtpInfo)[0].Timestamp,
 		},
 		&headers.RTPInfoEntry{
-			URL: (&url.URL{
+			URL: (&base.URL{
 				Scheme: "rtsp",
 				Host:   "localhost:8554",
 				Path:   mustParseURL((*rtpInfo)[1].URL).Path,
diff --git a/server_test.go b/server_test.go
index e8445f3c..4f0a4234 100644
--- a/server_test.go
+++ b/server_test.go
@@ -311,7 +311,40 @@ func TestServerErrorCSeqMissing(t *testing.T) {
 	res, err := writeReqReadRes(conn, base.Request{
 		Method: base.Options,
 		URL:    mustParseURL("rtsp://localhost:8554/"),
-		Header: base.Header{},
+	})
+	require.NoError(t, err)
+	require.Equal(t, base.StatusBadRequest, res.StatusCode)
+
+	<-nconnClosed
+}
+
+func TestServerErrorNilURL(t *testing.T) {
+	nconnClosed := make(chan struct{})
+
+	s := &Server{
+		Handler: &testServerHandler{
+			onConnClose: func(ctx *ServerHandlerOnConnCloseCtx) {
+				require.EqualError(t, ctx.Error, "invalid path")
+				close(nconnClosed)
+			},
+		},
+		RTSPAddress: "localhost:8554",
+	}
+	err := s.Start()
+	require.NoError(t, err)
+	defer s.Close()
+
+	nconn, err := net.Dial("tcp", "localhost:8554")
+	require.NoError(t, err)
+	defer nconn.Close()
+	conn := conn.NewConn(nconn)
+
+	res, err := writeReqReadRes(conn, base.Request{
+		Method: base.Describe,
+		URL:    nil,
+		Header: base.Header{
+			"CSeq": base.HeaderValue{"1"},
+		},
 	})
 	require.NoError(t, err)
 	require.Equal(t, base.StatusBadRequest, res.StatusCode)