Repository: bluenviron/gortsplib

Issue:
Get ephemeral port number after binding to port 0
Listening on port 0 will ask the system to bind on an unused port in the ephemeral port range. For example, I can run MediaMTX with `rtspAddress: :0` which logs

```
2025/07/17 22:48:57 INF [RTSP] listener opened on :0 (TCP)
```

In reality the operating system assigned it port 60780:

```
% sudo lsof -nP -iTCP -sTCP:LISTEN
COMMAND     PID   USER   FD   TYPE             DEVICE SIZE/OFF NODE NAME
mediamtx  53024    rgov   4u  IPv6   0x4c4d05e23c32f8      0t0  TCP *:60780 (LISTEN)
```

I think in [`server_tcp_listener:initialize()`](https://github.com/bluenviron/gortsplib/blob/bd2e4c1bac574fdffed6b472edf46b98b322a368/server_tcp_listener.go#L13-L25) after calling `Server.Listen()`, you would extract the port number like this:

```go
// I don't write Go...
port := sl.ln.Addr().(*net.TCPAddr).Port
```

There probably should be an interface for exposing the port back to functions like [`printAddresses()`](https://github.com/bluenviron/mediamtx/blob/aa2141f8cc9a46f1088315ed79768ff8e44420bc/internal/servers/rtsp/server.go#L39-L53) rather than have them rummage around in `srv.serverTCPListener.ln` which is marked private.

**Why?** I'd like to run an ephemeral RTSP server as part of a unit test of a video streaming application. It wouldn't do to use a static port; it could be in use (consider a buildbot that is running tests on multiple branches at once). If I pick a random port, there's a chance of collision / race condition. Allowing the system to choose by setting port = 0 and parsing the log message is the robust solution.

Discussion:
@aler9: Hello, in #934 i've added `Server.NetListener()` that allows to access the underlying net listener.@rgov: Nice! But is this completed yet? There's an accessor function but nothing calls it.@aler9: @rgov the accessor function is all that is needed for users to access the underlying net.Listener and consequently the ephemeral port and any other socket-related thing, what else do you need?@rgov: Mm yeah, maybe I haven't had my coffee yet. bluenviron/mediamtx needs to use it, not this project.

Solution patch:
diff --git a/client.go b/client.go
index 15d422eb..3a5fc2de 100644
--- a/client.go
+++ b/client.go
@@ -281,7 +281,7 @@ func supportsGetParameter(header base.Header) bool {
 		return false
 	}
 
-	for _, m := range strings.Split(pub[0], ",") {
+	for m := range strings.SplitSeq(pub[0], ",") {
 		if base.Method(strings.Trim(m, " ")) == base.GetParameter {
 			return true
 		}
@@ -1229,7 +1229,10 @@ func (c *Client) do(req *base.Request, skipResponse bool) (*base.Response, error
 		c.session = sx.Session
 
 		if sx.Timeout != nil && *sx.Timeout > 0 {
-			c.keepAlivePeriod = time.Duration(*sx.Timeout) * time.Second * 8 / 10
+			c.keepAlivePeriod = max(
+				(time.Duration(*sx.Timeout)*time.Second)-5*time.Second,
+				1*time.Second,
+			)
 		}
 	}
 
diff --git a/examples/client-play-backchannel/dummy_audio.go b/examples/client-play-backchannel/dummy_audio.go
index c56bad21..25fbd407 100644
--- a/examples/client-play-backchannel/dummy_audio.go
+++ b/examples/client-play-backchannel/dummy_audio.go
@@ -13,7 +13,7 @@ func createDummyAudio(pts int64, prevPTS int64) []byte {
 	n := 0
 	ret := make([]byte, sampleCount*2)
 
-	for i := int64(0); i < sampleCount; i++ {
+	for i := range sampleCount {
 		v := int16(amplitude * math.Sin((float64(prevPTS+i)*frequency*math.Pi*2)/sampleRate))
 		ret[n] = byte(v >> 8)
 		ret[n+1] = byte(v)
diff --git a/examples/client-record-format-g711/dummy_audio.go b/examples/client-record-format-g711/dummy_audio.go
index c56bad21..25fbd407 100644
--- a/examples/client-record-format-g711/dummy_audio.go
+++ b/examples/client-record-format-g711/dummy_audio.go
@@ -13,7 +13,7 @@ func createDummyAudio(pts int64, prevPTS int64) []byte {
 	n := 0
 	ret := make([]byte, sampleCount*2)
 
-	for i := int64(0); i < sampleCount; i++ {
+	for i := range sampleCount {
 		v := int16(amplitude * math.Sin((float64(prevPTS+i)*frequency*math.Pi*2)/sampleRate))
 		ret[n] = byte(v >> 8)
 		ret[n+1] = byte(v)
diff --git a/examples/client-record-format-lpcm/dummy_audio.go b/examples/client-record-format-lpcm/dummy_audio.go
index 267263b2..7e2b971d 100644
--- a/examples/client-record-format-lpcm/dummy_audio.go
+++ b/examples/client-record-format-lpcm/dummy_audio.go
@@ -13,7 +13,7 @@ func createDummyAudio(pts int64, prevPTS int64) []byte {
 	n := 0
 	ret := make([]byte, sampleCount*2)
 
-	for i := int64(0); i < sampleCount; i++ {
+	for i := range sampleCount {
 		v := int16(amplitude * math.Sin((float64(prevPTS+i)*frequency*math.Pi*2)/sampleRate))
 		ret[n] = byte(v >> 8)
 		ret[n+1] = byte(v)
diff --git a/examples/server-play-backchannel/dummy_audio.go b/examples/server-play-backchannel/dummy_audio.go
index c56bad21..25fbd407 100644
--- a/examples/server-play-backchannel/dummy_audio.go
+++ b/examples/server-play-backchannel/dummy_audio.go
@@ -13,7 +13,7 @@ func createDummyAudio(pts int64, prevPTS int64) []byte {
 	n := 0
 	ret := make([]byte, sampleCount*2)
 
-	for i := int64(0); i < sampleCount; i++ {
+	for i := range sampleCount {
 		v := int16(amplitude * math.Sin((float64(prevPTS+i)*frequency*math.Pi*2)/sampleRate))
 		ret[n] = byte(v >> 8)
 		ret[n+1] = byte(v)
diff --git a/pkg/conn/conn.go b/pkg/conn/conn.go
index d42d4885..fab7d534 100644
--- a/pkg/conn/conn.go
+++ b/pkg/conn/conn.go
@@ -26,7 +26,7 @@ func NewConn(br *bufio.Reader, w io.Writer) *Conn {
 }
 
 // Read reads a Request, a Response or an Interleaved frame.
-func (c *Conn) Read() (interface{}, error) {
+func (c *Conn) Read() (any, error) {
 	for {
 		byts, err := c.br.Peek(2)
 		if err != nil {
diff --git a/pkg/description/media.go b/pkg/description/media.go
index 78fe1eae..03d8d946 100644
--- a/pkg/description/media.go
+++ b/pkg/description/media.go
@@ -261,7 +261,7 @@ func (m Media) URL(contentBase *base.URL) (*base.URL, error) {
 }
 
 // FindFormat finds a certain format among all the formats in the media.
-func (m Media) FindFormat(forma interface{}) bool {
+func (m Media) FindFormat(forma any) bool {
 	for _, formak := range m.Formats {
 		if reflect.TypeOf(formak) == reflect.TypeOf(forma).Elem() {
 			reflect.ValueOf(forma).Elem().Set(reflect.ValueOf(formak))
diff --git a/pkg/description/session.go b/pkg/description/session.go
index b0d62ef8..4116ab74 100644
--- a/pkg/description/session.go
+++ b/pkg/description/session.go
@@ -65,7 +65,7 @@ type Session struct {
 
 // 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 {
+func (d *Session) FindFormat(forma any) *Media {
 	for _, media := range d.Medias {
 		ok := media.FindFormat(forma)
 		if ok {
diff --git a/pkg/format/format.go b/pkg/format/format.go
index be344192..14a81a35 100644
--- a/pkg/format/format.go
+++ b/pkg/format/format.go
@@ -60,7 +60,7 @@ func decodeFMTP(enc string) map[string]string {
 
 	ret := make(map[string]string)
 
-	for _, kv := range strings.Split(enc, ";") {
+	for kv := range strings.SplitSeq(enc, ";") {
 		kv = strings.Trim(kv, " ")
 
 		if len(kv) == 0 {
diff --git a/pkg/headers/rtp_info.go b/pkg/headers/rtp_info.go
index 46d2f963..00b8918f 100644
--- a/pkg/headers/rtp_info.go
+++ b/pkg/headers/rtp_info.go
@@ -28,7 +28,7 @@ func (h *RTPInfo) Unmarshal(v base.HeaderValue) error {
 		return fmt.Errorf("value provided multiple times (%v)", v)
 	}
 
-	for _, part := range strings.Split(v[0], ",") {
+	for part := range strings.SplitSeq(v[0], ",") {
 		e := &RTPInfoEntry{}
 
 		// remove leading spaces
diff --git a/pkg/ringbuffer/ringbuffer.go b/pkg/ringbuffer/ringbuffer.go
index 364aea31..91cba959 100644
--- a/pkg/ringbuffer/ringbuffer.go
+++ b/pkg/ringbuffer/ringbuffer.go
@@ -11,7 +11,7 @@ type RingBuffer struct {
 	size       uint64
 	mutex      sync.Mutex
 	cond       *sync.Cond
-	buffer     []interface{}
+	buffer     []any
 	readIndex  uint64
 	writeIndex uint64
 	closed     bool
@@ -27,7 +27,7 @@ func New(size uint64) (*RingBuffer, error) {
 
 	r := &RingBuffer{
 		size:   size,
-		buffer: make([]interface{}, size),
+		buffer: make([]any, size),
 	}
 
 	r.cond = sync.NewCond(&r.mutex)
@@ -62,7 +62,7 @@ func (r *RingBuffer) Reset() {
 }
 
 // Push pushes data at the end of the buffer.
-func (r *RingBuffer) Push(data interface{}) bool {
+func (r *RingBuffer) Push(data any) bool {
 	r.mutex.Lock()
 
 	if r.buffer[r.writeIndex] != nil {
@@ -81,7 +81,7 @@ func (r *RingBuffer) Push(data interface{}) bool {
 }
 
 // Pull pulls data from the beginning of the buffer.
-func (r *RingBuffer) Pull() (interface{}, bool) {
+func (r *RingBuffer) Pull() (any, bool) {
 	for {
 		r.mutex.Lock()
 
diff --git a/pkg/sdp/sdp.go b/pkg/sdp/sdp.go
index 7b63d61c..16ba2ebc 100644
--- a/pkg/sdp/sdp.go
+++ b/pkg/sdp/sdp.go
@@ -5,6 +5,7 @@ import (
 	"errors"
 	"fmt"
 	"net/url"
+	"slices"
 	"strconv"
 	"strings"
 
@@ -41,12 +42,7 @@ func indexOf(element string, data []string) int {
 }
 
 func anyOf(element string, data ...string) bool {
-	for _, v := range data {
-		if element == v {
-			return true
-		}
-	}
-	return false
+	return slices.Contains(data, element)
 }
 
 func parsePort(value string) (int, error) {
@@ -467,7 +463,7 @@ func (s *SessionDescription) unmarshalMediaDescription(value string) error {
 	// <proto>
 	// Set according to currently registered with IANA
 	// https://tools.ietf.org/html/rfc4566#section-5.14
-	for _, proto := range strings.Split(fields[2], "/") {
+	for proto := range strings.SplitSeq(fields[2], "/") {
 		if i := indexOf(proto, []string{
 			"UDP", "RTP", "AVP", "SAVP", "SAVPF",
 			"MP2T", "TLS", "DTLS", "SCTP", "AVPF", "TCP",
@@ -691,7 +687,7 @@ func (s *SessionDescription) Unmarshal(byts []byte) error {
 
 	state := stateInitial
 
-	for _, line := range strings.Split(strings.ReplaceAll(str, "\r", ""), "\n") {
+	for line := range strings.SplitSeq(strings.ReplaceAll(str, "\r", ""), "\n") {
 		if line == "" {
 			continue
 		}
diff --git a/server.go b/server.go
index 9d59845e..f5719e37 100644
--- a/server.go
+++ b/server.go
@@ -362,6 +362,11 @@ func (s *Server) Wait() error {
 	return s.closeError
 }
 
+// NetListener returns the underlying net.Listener
+func (s *Server) NetListener() net.Listener {
+	return s.tcpListener.ln
+}
+
 func (s *Server) run() {
 	defer s.wg.Done()
 
diff --git a/server_conn.go b/server_conn.go
index 6da07beb..2cb3de3b 100644
--- a/server_conn.go
+++ b/server_conn.go
@@ -8,6 +8,7 @@ import (
 	"errors"
 	"net"
 	gourl "net/url"
+	"slices"
 	"strconv"
 	"strings"
 	"sync"
@@ -47,10 +48,8 @@ func checkMulticastEnabled(multicastIPRange string, query string) bool {
 
 func checkBackChannelsEnabled(header base.Header) bool {
 	if vals, ok := header["Require"]; ok {
-		for _, val := range vals {
-			if val == "www.onvif.org/ver20/backchannel" {
-				return true
-			}
+		if slices.Contains(vals, "www.onvif.org/ver20/backchannel") {
+			return true
 		}
 	}
 	return false
@@ -210,7 +209,7 @@ type ServerConn struct {
 	ctx              context.Context
 	ctxCancel        func()
 	propsMutex       sync.RWMutex
-	userData         interface{}
+	userData         any
 	remoteAddr       *net.TCPAddr
 	bc               *bytecounter.ByteCounter
 	conn             *conn.Conn
@@ -259,12 +258,12 @@ func (sc *ServerConn) NetConn() net.Conn {
 }
 
 // SetUserData sets some user data associated with the connection.
-func (sc *ServerConn) SetUserData(v interface{}) {
+func (sc *ServerConn) SetUserData(v any) {
 	sc.userData = v
 }
 
 // UserData returns some user data associated with the connection.
-func (sc *ServerConn) UserData() interface{} {
+func (sc *ServerConn) UserData() any {
 	return sc.userData
 }
 
diff --git a/server_handler.go b/server_handler.go
index 8d090b5c..1a86d278 100644
--- a/server_handler.go
+++ b/server_handler.go
@@ -6,7 +6,7 @@ import (
 )
 
 // ServerHandler is the interface implemented by all the server handlers.
-type ServerHandler interface{}
+type ServerHandler any
 
 // ServerHandlerOnConnOpenCtx is the context of OnConnOpen.
 type ServerHandlerOnConnOpenCtx struct {
diff --git a/server_session.go b/server_session.go
index c7c5fbcd..777afd67 100644
--- a/server_session.go
+++ b/server_session.go
@@ -436,7 +436,7 @@ type ServerSession struct {
 	ctxCancel             func()
 	propsMutex            sync.RWMutex
 	conns                 map[*ServerConn]struct{}
-	userData              interface{}
+	userData              any
 	state                 ServerSessionState
 	setuppedMedias        map[*description.Media]*serverSessionMedia
 	setuppedMediasOrdered []*serverSessionMedia
@@ -543,12 +543,12 @@ func (ss *ServerSession) Medias() []*description.Media {
 }
 
 // SetUserData sets some user data associated with the session.
-func (ss *ServerSession) SetUserData(v interface{}) {
+func (ss *ServerSession) SetUserData(v any) {
 	ss.userData = v
 }
 
 // UserData returns some user data associated with the session.
-func (ss *ServerSession) UserData() interface{} {
+func (ss *ServerSession) UserData() any {
 	return ss.userData
 }
 
@@ -882,9 +882,14 @@ func (ss *ServerSession) runInner() error {
 						res.Header = make(base.Header)
 					}
 
+					// Media Foundation-based software, like Windows Media Player,
+					// send keepalives at an interval equal the timeout value.
+					// prevent timeouts by subtracting 5 seconds from the value.
+					timeout := max(int(ss.s.IdleTimeout/time.Second)-5, 1)
+
 					res.Header["Session"] = headers.Session{
 						Session: ss.secretID,
-						Timeout: ptrOf(uint(ss.s.IdleTimeout / time.Second)),
+						Timeout: ptrOf(uint(timeout)),
 					}.Marshal()
 				}
 



Test patch:
diff --git a/client_play_test.go b/client_play_test.go
index 2e8653fd..382ca011 100644
--- a/client_play_test.go
+++ b/client_play_test.go
@@ -182,7 +182,7 @@ func TestClientPlayFormats(t *testing.T) {
 		})
 		require.NoError(t, err2)
 
-		for i := 0; i < 3; i++ {
+		for i := range 3 {
 			req, err2 = conn.ReadRequest()
 			require.NoError(t, err2)
 			require.Equal(t, base.Setup, req.Method)
@@ -375,7 +375,7 @@ func TestClientPlay(t *testing.T) {
 				var srtpInCtx [2]*wrappedSRTPContext
 				var srtpOutCtx [2]*wrappedSRTPContext
 
-				for i := 0; i < 2; i++ {
+				for i := range 2 {
 					req, err2 = conn.ReadRequest()
 					require.NoError(t, err2)
 					require.Equal(t, base.Setup, req.Method)
@@ -514,7 +514,7 @@ func TestClientPlay(t *testing.T) {
 
 				// server -> client
 
-				for i := 0; i < 2; i++ {
+				for i := range 2 {
 					buf := testRTPPacketMarshaled
 
 					if ca.secure == "secure" {
@@ -551,7 +551,7 @@ func TestClientPlay(t *testing.T) {
 				// skip firewall opening
 
 				if ca.transport == "udp" {
-					for i := 0; i < 2; i++ {
+					for i := range 2 {
 						buf := make([]byte, 2048)
 						_, _, err2 = l2s[i].ReadFrom(buf)
 						require.NoError(t, err2)
@@ -560,7 +560,7 @@ func TestClientPlay(t *testing.T) {
 
 				// client -> server
 
-				for i := 0; i < 2; i++ {
+				for i := range 2 {
 					var buf []byte
 
 					switch ca.transport {
@@ -3711,7 +3711,7 @@ func TestClientPlayBackChannel(t *testing.T) {
 				var l2s [2]net.PacketConn
 				var clientPorts [2]*[2]int
 
-				for i := 0; i < 2; i++ {
+				for i := range 2 {
 					req, err2 = conn.ReadRequest()
 					require.NoError(t, err2)
 					require.Equal(t, base.Setup, req.Method)
@@ -3908,7 +3908,7 @@ func TestClientPlayBackChannel(t *testing.T) {
 					}
 					return ptrOf(ProtocolUDP)
 				}(),
-				senderReportPeriod:   500 * time.Millisecond,
+				senderReportPeriod:   750 * time.Millisecond,
 				receiverReportPeriod: 750 * time.Millisecond,
 			}
 
diff --git a/client_record_test.go b/client_record_test.go
index 03837a8f..68077132 100644
--- a/client_record_test.go
+++ b/client_record_test.go
@@ -1349,7 +1349,7 @@ func TestClientRecordRTCPReport(t *testing.T) {
 					require.NoError(t, err2)
 					buf = buf[:n]
 				} else {
-					for i := 0; i < 2; i++ {
+					for range 2 {
 						_, err2 = conn.ReadInterleavedFrame()
 						require.NoError(t, err2)
 					}
diff --git a/internal/teste2e/server_vs_external_test.go b/internal/teste2e/server_vs_external_test.go
index bf5f88a3..b7f9dc39 100644
--- a/internal/teste2e/server_vs_external_test.go
+++ b/internal/teste2e/server_vs_external_test.go
@@ -79,12 +79,10 @@ func TestServerVsExternal(t *testing.T) {
 
 	for _, file := range files {
 		cfile := file
-		wg.Add(1)
-		go func() {
+		wg.Go(func() {
 			err := buildImage(cfile.Name())
 			require.NoError(t, err)
-			wg.Done()
-		}()
+		})
 	}
 
 	wg.Wait()
diff --git a/pkg/base/header_test.go b/pkg/base/header_test.go
index 96205bc6..a3d4bcc4 100644
--- a/pkg/base/header_test.go
+++ b/pkg/base/header_test.go
@@ -134,7 +134,7 @@ func FuzzHeaderUnmarshal(f *testing.F) {
 	}
 
 	str := ""
-	for i := 0; i < 300; i++ {
+	for range 300 {
 		str += "Key: val\r\n"
 	}
 	f.Add([]byte(str))
diff --git a/pkg/conn/conn_test.go b/pkg/conn/conn_test.go
index e287a64e..30c14058 100644
--- a/pkg/conn/conn_test.go
+++ b/pkg/conn/conn_test.go
@@ -22,7 +22,7 @@ func TestRead(t *testing.T) {
 	for _, ca := range []struct {
 		name string
 		enc  []byte
-		dec  interface{}
+		dec  any
 	}{
 		{
 			"request",
diff --git a/pkg/ringbuffer/ringbuffer_test.go b/pkg/ringbuffer/ringbuffer_test.go
index 9c747a84..a7a0e8d7 100644
--- a/pkg/ringbuffer/ringbuffer_test.go
+++ b/pkg/ringbuffer/ringbuffer_test.go
@@ -79,15 +79,15 @@ func TestOverflow(t *testing.T) {
 	r, err := New(32)
 	require.NoError(t, err)
 
-	for i := 0; i < 32; i++ {
+	for range 32 {
 		r.Push([]byte{1, 2, 3, 4})
 	}
 
 	ok := r.Push([]byte{5, 6, 7, 8})
 	require.Equal(t, false, ok)
 
-	for i := 0; i < 32; i++ {
-		var data interface{}
+	for range 32 {
+		var data any
 		data, ok = r.Pull()
 		require.Equal(t, true, ok)
 		require.Equal(t, []byte{1, 2, 3, 4}, data)
@@ -100,16 +100,16 @@ func BenchmarkPushPullContinuous(b *testing.B) {
 
 	data := make([]byte, 1024)
 
-	for n := 0; n < b.N; n++ {
+	for b.Loop() {
 		done := make(chan struct{})
 		go func() {
 			defer close(done)
-			for i := 0; i < 1024*8; i++ {
+			for range 1024 * 8 {
 				r.Push(data)
 			}
 		}()
 
-		for i := 0; i < 1024*8; i++ {
+		for range 1024 * 8 {
 			r.Pull()
 		}
 
@@ -123,17 +123,17 @@ func BenchmarkPushPullPaused5(b *testing.B) {
 
 	data := make([]byte, 1024)
 
-	for n := 0; n < b.N; n++ {
+	for b.Loop() {
 		done := make(chan struct{})
 		go func() {
 			defer close(done)
-			for i := 0; i < 128; i++ {
+			for range 128 {
 				r.Push(data)
 				time.Sleep(5 * time.Millisecond)
 			}
 		}()
 
-		for i := 0; i < 128; i++ {
+		for range 128 {
 			r.Pull()
 		}
 
@@ -147,17 +147,17 @@ func BenchmarkPushPullPaused10(b *testing.B) {
 
 	data := make([]byte, 1024)
 
-	for n := 0; n < b.N; n++ {
+	for b.Loop() {
 		done := make(chan struct{})
 		go func() {
 			defer close(done)
-			for i := 0; i < 128; i++ {
+			for range 128 {
 				r.Push(data)
 				time.Sleep(10 * time.Millisecond)
 			}
 		}()
 
-		for i := 0; i < 128; i++ {
+		for range 128 {
 			r.Pull()
 		}
 
diff --git a/server_play_test.go b/server_play_test.go
index 73622318..8633d30d 100644
--- a/server_play_test.go
+++ b/server_play_test.go
@@ -535,7 +535,7 @@ func TestServerPlaySetupErrorSameUDPPortsAndIP(t *testing.T) {
 	require.NoError(t, err)
 	defer stream.Close()
 
-	for i := 0; i < 2; i++ {
+	for i := range 2 {
 		var nconn net.Conn
 		nconn, err = net.Dial("tcp", "localhost:8554")
 		require.NoError(t, err)
@@ -2738,7 +2738,7 @@ func TestServerPlayBackChannel(t *testing.T) {
 			var l1s [2]net.PacketConn
 			var l2s [2]net.PacketConn
 
-			for i := 0; i < 2; i++ {
+			for i := range 2 {
 				inTH := &headers.Transport{
 					Mode: ptrOf(headers.TransportModePlay),
 				}
diff --git a/server_record_test.go b/server_record_test.go
index 7e615ca0..04bf6f39 100644
--- a/server_record_test.go
+++ b/server_record_test.go
@@ -652,7 +652,7 @@ func TestServerRecord(t *testing.T) {
 						err = ctx.Session.WritePacketRTCP(ctx.Session.AnnouncedDescription().Medias[1], &testRTCPPacket)
 						require.NoError(t, err)
 
-						for i := 0; i < 2; i++ {
+						for i := range 2 {
 							ctx.Session.OnPacketRTP(
 								ctx.Session.AnnouncedDescription().Medias[i],
 								ctx.Session.AnnouncedDescription().Medias[i].Formats[0],
@@ -740,7 +740,7 @@ func TestServerRecord(t *testing.T) {
 			var srtpOutCtx [2]*wrappedSRTPContext
 			var srtpInCtx [2]*wrappedSRTPContext
 
-			for i := 0; i < 2; i++ {
+			for i := range 2 {
 				inTH := &headers.Transport{
 					Delivery: ptrOf(headers.TransportDeliveryUnicast),
 					Mode:     ptrOf(headers.TransportModeRecord),
@@ -835,7 +835,7 @@ func TestServerRecord(t *testing.T) {
 			// skip firewall opening
 
 			if ca.transport == "udp" {
-				for i := 0; i < 2; i++ {
+				for i := range 2 {
 					buf := make([]byte, 2048)
 					_, _, err = l2s[i].ReadFrom(buf)
 					require.NoError(t, err)
@@ -844,7 +844,7 @@ func TestServerRecord(t *testing.T) {
 
 			// server -> client
 
-			for i := 0; i < 2; i++ {
+			for i := range 2 {
 				var buf []byte
 
 				if ca.transport == "udp" {
@@ -871,7 +871,7 @@ func TestServerRecord(t *testing.T) {
 
 			// client -> server
 
-			for i := 0; i < 2; i++ {
+			for i := range 2 {
 				buf1 := testRTPPacketMarshaled
 
 				if ca.secure == "secure" {
@@ -919,7 +919,7 @@ func TestServerRecord(t *testing.T) {
 
 			// server -> client
 
-			for i := 0; i < 2; i++ {
+			for i := range 2 {
 				var buf []byte
 
 				if ca.transport == "udp" {
diff --git a/server_test.go b/server_test.go
index 57dad2a3..b9c84bcd 100644
--- a/server_test.go
+++ b/server_test.go
@@ -240,6 +240,20 @@ func TestServerClose(t *testing.T) {
 	s.Close()
 }
 
+func TestServerNetListener(t *testing.T) {
+	s := &Server{
+		Handler:     &testServerHandler{},
+		RTSPAddress: "127.0.0.1:8554",
+	}
+
+	err := s.Start()
+	require.NoError(t, err)
+	defer s.Close()
+
+	ln := s.NetListener()
+	require.Equal(t, "127.0.0.1:8554", ln.Addr().String())
+}
+
 func TestServerErrorInvalidUDPPorts(t *testing.T) {
 	t.Run("non consecutive", func(t *testing.T) {
 		s := &Server{