Repository: bluenviron/gortsplib
Pull request #304 (2023-06-08 21:09:02)

server, client: allow using odd interleaved IDs
Fixes https://github.com/bluenviron/mediamtx/issues/1762

codecov-commenter: ## [Codecov](https://app.codecov.io/gh/bluenviron/gortsplib/pull/304?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=bluenviron) Report
> Merging [#304](https://app.codecov.io/gh/bluenviron/gortsplib/pull/304?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=bluenviron) (1b2d7c0) into [main](https://app.codecov.io/gh/bluenviron/gortsplib/commit/6cdee49e5fa2a31245613e0a330a76f740d7e933?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=bluenviron) (6cdee49) will **decrease** coverage by `0.08%`.
> The diff coverage is `81.13%`.

:exclamation: Your organization is not using the GitHub App Integration. As a result you may experience degraded service beginning May 15th. Please [install the Github App Integration](https://github.com/apps/codecov) for your organization. [Read more](https://about.codecov.io/blog/codecov-is-updating-its-github-integration/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=bluenviron).

```diff
@@            Coverage Diff             @@
##             main     #304      +/-   ##
==========================================
- Coverage   84.65%   84.57%   -0.08%     
==========================================
  Files          99       99              
  Lines       10164    10112      -52     
==========================================
- Hits         8604     8552      -52     
- Misses       1231     1232       +1     
+ Partials      329      328       -1     
```


| [Impacted Files](https://app.codecov.io/gh/bluenviron/gortsplib/pull/304?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=bluenviron) | Coverage Δ | |
|---|---|---|
| [server.go](https://app.codecov.io/gh/bluenviron/gortsplib/pull/304?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=bluenviron#diff-c2VydmVyLmdv) | `58.27% <ø> (-0.29%)` | :arrow_down: |
| [server\_session.go](https://app.codecov.io/gh/bluenviron/gortsplib/pull/304?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=bluenviron#diff-c2VydmVyX3Nlc3Npb24uZ28=) | `77.23% <61.53%> (-0.01%)` | :arrow_down: |
| [client.go](https://app.codecov.io/gh/bluenviron/gortsplib/pull/304?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=bluenviron#diff-Y2xpZW50Lmdv) | `73.02% <82.35%> (+0.14%)` | :arrow_up: |
| [client\_media.go](https://app.codecov.io/gh/bluenviron/gortsplib/pull/304?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=bluenviron#diff-Y2xpZW50X21lZGlhLmdv) | `87.86% <83.33%> (-0.30%)` | :arrow_down: |
| [server\_session\_media.go](https://app.codecov.io/gh/bluenviron/gortsplib/pull/304?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=bluenviron#diff-c2VydmVyX3Nlc3Npb25fbWVkaWEuZ28=) | `93.40% <86.66%> (-0.29%)` | :arrow_down: |
| [client\_reader.go](https://app.codecov.io/gh/bluenviron/gortsplib/pull/304?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=bluenviron#diff-Y2xpZW50X3JlYWRlci5nbw==) | `94.87% <100.00%> (+4.30%)` | :arrow_up: |
| [client\_udp\_listener.go](https://app.codecov.io/gh/bluenviron/gortsplib/pull/304?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=bluenviron#diff-Y2xpZW50X3VkcF9saXN0ZW5lci5nbw==) | `75.89% <100.00%> (-3.62%)` | :arrow_down: |
| [server\_conn\_reader.go](https://app.codecov.io/gh/bluenviron/gortsplib/pull/304?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=bluenviron#diff-c2VydmVyX2Nvbm5fcmVhZGVyLmdv) | `97.46% <100.00%> (-0.26%)` | :arrow_down: |
| [server\_stream.go](https://app.codecov.io/gh/bluenviron/gortsplib/pull/304?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=bluenviron#diff-c2VydmVyX3N0cmVhbS5nbw==) | `83.87% <100.00%> (ø)` | |
| [server\_udp\_listener.go](https://app.codecov.io/gh/bluenviron/gortsplib/pull/304?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=bluenviron#diff-c2VydmVyX3VkcF9saXN0ZW5lci5nbw==) | `76.51% <100.00%> (-1.76%)` | :arrow_down: |

: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 994d72a4..9583638c 100644
--- a/client.go
+++ b/client.go
@@ -249,32 +249,32 @@ type Client struct {
 	checkTimeoutPeriod      time.Duration
 	keepalivePeriod         time.Duration
 
-	scheme              string
-	host                string
-	ctx                 context.Context
-	ctxCancel           func()
-	state               clientState
-	nconn               net.Conn
-	conn                *conn.Conn
-	session             string
-	sender              *auth.Sender
-	cseq                int
-	optionsSent         bool
-	useGetParameter     bool
-	lastDescribeURL     *url.URL
-	baseURL             *url.URL
-	effectiveTransport  *Transport
-	medias              map[*media.Media]*clientMedia
-	tcpMediasByChannel  map[int]*clientMedia
-	lastRange           *headers.Range
-	checkTimeoutTimer   *time.Timer
-	checkTimeoutInitial bool
-	tcpLastFrameTime    *int64
-	keepaliveTimer      *time.Timer
-	closeError          error
-	writer              asyncProcessor
-	reader              *clientReader
-	connCloser          *clientConnCloser
+	scheme               string
+	host                 string
+	ctx                  context.Context
+	ctxCancel            func()
+	state                clientState
+	nconn                net.Conn
+	conn                 *conn.Conn
+	session              string
+	sender               *auth.Sender
+	cseq                 int
+	optionsSent          bool
+	useGetParameter      bool
+	lastDescribeURL      *url.URL
+	baseURL              *url.URL
+	effectiveTransport   *Transport
+	medias               map[*media.Media]*clientMedia
+	tcpCallbackByChannel map[int]readFunc
+	lastRange            *headers.Range
+	checkTimeoutTimer    *time.Timer
+	checkTimeoutInitial  bool
+	tcpLastFrameTime     *int64
+	keepaliveTimer       *time.Timer
+	closeError           error
+	writer               asyncProcessor
+	reader               *clientReader
+	connCloser           *clientConnCloser
 
 	// in
 	options   chan optionsReq
@@ -556,7 +556,7 @@ func (c *Client) reset() {
 	c.baseURL = nil
 	c.effectiveTransport = nil
 	c.medias = nil
-	c.tcpMediasByChannel = nil
+	c.tcpCallbackByChannel = nil
 }
 
 func (c *Client) checkState(allowed map[clientState]struct{}) error {
@@ -1190,8 +1190,8 @@ func (c *Client) doSetup(
 		v1 := headers.TransportDeliveryUnicast
 		th.Delivery = &v1
 		th.Protocol = headers.TransportProtocolTCP
-		mediaCount := len(c.medias)
-		th.InterleavedIDs = &[2]int{(mediaCount * 2), (mediaCount * 2) + 1}
+		ch := c.findFreeChannelPair()
+		th.InterleavedIDs = &[2]int{ch, ch + 1}
 	}
 
 	mediaURL, err := medi.URL(baseURL)
@@ -1348,22 +1348,16 @@ func (c *Client) doSetup(
 			return nil, liberrors.ErrClientTransportHeaderNoInterleavedIDs{}
 		}
 
-		if (thRes.InterleavedIDs[0]%2) != 0 ||
-			(thRes.InterleavedIDs[0]+1) != thRes.InterleavedIDs[1] {
+		if (thRes.InterleavedIDs[0] + 1) != thRes.InterleavedIDs[1] {
 			return nil, liberrors.ErrClientTransportHeaderInvalidInterleavedIDs{}
 		}
 
-		if _, ok := c.tcpMediasByChannel[thRes.InterleavedIDs[0]]; ok {
+		if c.isChannelPairInUse(thRes.InterleavedIDs[0]) {
 			return &base.Response{
 				StatusCode: base.StatusBadRequest,
-			}, liberrors.ErrClientTransportHeaderInterleavedIDsAlreadyUsed{}
+			}, liberrors.ErrClientTransportHeaderInterleavedIDsInUse{}
 		}
 
-		if c.tcpMediasByChannel == nil {
-			c.tcpMediasByChannel = make(map[int]*clientMedia)
-		}
-
-		c.tcpMediasByChannel[thRes.InterleavedIDs[0]] = cm
 		cm.tcpChannel = thRes.InterleavedIDs[0]
 	}
 
@@ -1386,6 +1380,23 @@ func (c *Client) doSetup(
 	return res, nil
 }
 
+func (c *Client) isChannelPairInUse(channel int) bool {
+	for _, cm := range c.medias {
+		if cm.tcpChannel == channel || cm.tcpChannel == (channel+1) {
+			return true
+		}
+	}
+	return false
+}
+
+func (c *Client) findFreeChannelPair() int {
+	for i := 0; ; i += 2 { // prefer even channels
+		if !c.isChannelPairInUse(i) {
+			return i
+		}
+	}
+}
+
 // Setup writes a SETUP request and reads a Response.
 // rtpPort and rtcpPort are used only if transport is UDP.
 // if rtpPort and rtcpPort are zero, they are chosen automatically.
diff --git a/client_media.go b/client_media.go
index f3503269..dabd47dc 100644
--- a/client_media.go
+++ b/client_media.go
@@ -24,8 +24,6 @@ type clientMedia struct {
 	tcpBuffer              []byte
 	writePacketRTPInQueue  func([]byte)
 	writePacketRTCPInQueue func([]byte)
-	readRTP                func([]byte) error
-	readRTCP               func([]byte) error
 	onPacketRTCP           func(rtcp.Packet)
 }
 
@@ -51,8 +49,7 @@ func (cm *clientMedia) allocateUDPListeners(multicast bool, rtpAddress string, r
 			cm.c.WriteTimeout,
 			multicast,
 			rtpAddress,
-			cm,
-			true)
+		)
 		if err != nil {
 			return err
 		}
@@ -63,8 +60,7 @@ func (cm *clientMedia) allocateUDPListeners(multicast bool, rtpAddress string, r
 			cm.c.WriteTimeout,
 			multicast,
 			rtcpAddress,
-			cm,
-			false)
+		)
 		if err != nil {
 			l1.close()
 			return err
@@ -78,7 +74,6 @@ func (cm *clientMedia) allocateUDPListeners(multicast bool, rtpAddress string, r
 		cm.c.ListenPacket,
 		cm.c.AnyPortEnable,
 		cm.c.WriteTimeout,
-		cm,
 	)
 	return nil
 }
@@ -98,22 +93,26 @@ func (cm *clientMedia) start() {
 		cm.writePacketRTCPInQueue = cm.writePacketRTCPInQueueUDP
 
 		if cm.c.state == clientStatePlay {
-			cm.readRTP = cm.readRTPUDPPlay
-			cm.readRTCP = cm.readRTCPUDPPlay
+			cm.udpRTPListener.readFunc = cm.readRTPUDPPlay
+			cm.udpRTCPListener.readFunc = cm.readRTCPUDPPlay
 		} else {
-			cm.readRTP = cm.readRTPUDPRecord
-			cm.readRTCP = cm.readRTCPUDPRecord
+			cm.udpRTPListener.readFunc = cm.readRTPUDPRecord
+			cm.udpRTCPListener.readFunc = cm.readRTCPUDPRecord
 		}
 	} else {
 		cm.writePacketRTPInQueue = cm.writePacketRTPInQueueTCP
 		cm.writePacketRTCPInQueue = cm.writePacketRTCPInQueueTCP
 
+		if cm.c.tcpCallbackByChannel == nil {
+			cm.c.tcpCallbackByChannel = make(map[int]readFunc)
+		}
+
 		if cm.c.state == clientStatePlay {
-			cm.readRTP = cm.readRTPTCPPlay
-			cm.readRTCP = cm.readRTCPTCPPlay
+			cm.c.tcpCallbackByChannel[cm.tcpChannel] = cm.readRTPTCPPlay
+			cm.c.tcpCallbackByChannel[cm.tcpChannel+1] = cm.readRTCPTCPPlay
 		} else {
-			cm.readRTP = cm.readRTPTCPRecord
-			cm.readRTCP = cm.readRTCPTCPRecord
+			cm.c.tcpCallbackByChannel[cm.tcpChannel] = cm.readRTPTCPRecord
+			cm.c.tcpCallbackByChannel[cm.tcpChannel+1] = cm.readRTCPTCPRecord
 		}
 
 		cm.tcpRTPFrame = &base.InterleavedFrame{Channel: cm.tcpChannel}
@@ -199,101 +198,95 @@ func (cm *clientMedia) writePacketRTCP(pkt rtcp.Packet) error {
 	return nil
 }
 
-func (cm *clientMedia) readRTPTCPPlay(payload []byte) error {
+func (cm *clientMedia) readRTPTCPPlay(payload []byte) {
 	now := time.Now()
 	atomic.StoreInt64(cm.c.tcpLastFrameTime, now.Unix())
 
 	pkt := &rtp.Packet{}
 	err := pkt.Unmarshal(payload)
 	if err != nil {
-		return err
+		cm.c.OnDecodeError(err)
+		return
 	}
 
 	forma, ok := cm.formats[pkt.PayloadType]
 	if !ok {
 		cm.c.OnDecodeError(fmt.Errorf("received RTP packet with unknown format: %d", pkt.PayloadType))
-		return nil
+		return
 	}
 
 	forma.readRTPTCP(pkt)
-	return nil
 }
 
-func (cm *clientMedia) readRTCPTCPPlay(payload []byte) error {
+func (cm *clientMedia) readRTCPTCPPlay(payload []byte) {
 	now := time.Now()
 	atomic.StoreInt64(cm.c.tcpLastFrameTime, now.Unix())
 
 	if len(payload) > udpMaxPayloadSize {
 		cm.c.OnDecodeError(fmt.Errorf("RTCP packet size (%d) is greater than maximum allowed (%d)",
 			len(payload), udpMaxPayloadSize))
-		return nil
+		return
 	}
 
 	packets, err := rtcp.Unmarshal(payload)
 	if err != nil {
 		cm.c.OnDecodeError(err)
-		return nil
+		return
 	}
 
 	for _, pkt := range packets {
 		cm.onPacketRTCP(pkt)
 	}
-
-	return nil
 }
 
-func (cm *clientMedia) readRTPTCPRecord(_ []byte) error {
-	return nil
+func (cm *clientMedia) readRTPTCPRecord(_ []byte) {
 }
 
-func (cm *clientMedia) readRTCPTCPRecord(payload []byte) error {
+func (cm *clientMedia) readRTCPTCPRecord(payload []byte) {
 	if len(payload) > udpMaxPayloadSize {
 		cm.c.OnDecodeError(fmt.Errorf("RTCP packet size (%d) is greater than maximum allowed (%d)",
 			len(payload), udpMaxPayloadSize))
-		return nil
+		return
 	}
 
 	packets, err := rtcp.Unmarshal(payload)
 	if err != nil {
 		cm.c.OnDecodeError(err)
-		return nil
+		return
 	}
 
 	for _, pkt := range packets {
 		cm.onPacketRTCP(pkt)
 	}
-
-	return nil
 }
 
-func (cm *clientMedia) readRTPUDPPlay(payload []byte) error {
+func (cm *clientMedia) readRTPUDPPlay(payload []byte) {
 	plen := len(payload)
 
 	atomic.AddUint64(cm.c.BytesReceived, uint64(plen))
 
 	if plen == (udpMaxPayloadSize + 1) {
 		cm.c.OnDecodeError(fmt.Errorf("RTP packet is too big to be read with UDP"))
-		return nil
+		return
 	}
 
 	pkt := &rtp.Packet{}
 	err := pkt.Unmarshal(payload)
 	if err != nil {
 		cm.c.OnDecodeError(err)
-		return nil
+		return
 	}
 
 	forma, ok := cm.formats[pkt.PayloadType]
 	if !ok {
 		cm.c.OnDecodeError(fmt.Errorf("received RTP packet with unknown format: %d", pkt.PayloadType))
-		return nil
+		return
 	}
 
 	forma.readRTPUDP(pkt)
-	return nil
 }
 
-func (cm *clientMedia) readRTCPUDPPlay(payload []byte) error {
+func (cm *clientMedia) readRTCPUDPPlay(payload []byte) {
 	now := time.Now()
 	plen := len(payload)
 
@@ -301,13 +294,13 @@ func (cm *clientMedia) readRTCPUDPPlay(payload []byte) error {
 
 	if plen == (udpMaxPayloadSize + 1) {
 		cm.c.OnDecodeError(fmt.Errorf("RTCP packet is too big to be read with UDP"))
-		return nil
+		return
 	}
 
 	packets, err := rtcp.Unmarshal(payload)
 	if err != nil {
 		cm.c.OnDecodeError(err)
-		return nil
+		return
 	}
 
 	for _, pkt := range packets {
@@ -320,33 +313,28 @@ func (cm *clientMedia) readRTCPUDPPlay(payload []byte) error {
 
 		cm.onPacketRTCP(pkt)
 	}
-
-	return nil
 }
 
-func (cm *clientMedia) readRTPUDPRecord(_ []byte) error {
-	return nil
+func (cm *clientMedia) readRTPUDPRecord(_ []byte) {
 }
 
-func (cm *clientMedia) readRTCPUDPRecord(payload []byte) error {
+func (cm *clientMedia) readRTCPUDPRecord(payload []byte) {
 	plen := len(payload)
 
 	atomic.AddUint64(cm.c.BytesReceived, uint64(plen))
 
 	if plen == (udpMaxPayloadSize + 1) {
 		cm.c.OnDecodeError(fmt.Errorf("RTCP packet is too big to be read with UDP"))
-		return nil
+		return
 	}
 
 	packets, err := rtcp.Unmarshal(payload)
 	if err != nil {
 		cm.c.OnDecodeError(err)
-		return nil
+		return
 	}
 
 	for _, pkt := range packets {
 		cm.onPacketRTCP(pkt)
 	}
-
-	return nil
 }
diff --git a/client_reader.go b/client_reader.go
index 1218b6ba..77590371 100644
--- a/client_reader.go
+++ b/client_reader.go
@@ -57,25 +57,8 @@ func (r *clientReader) runInner() error {
 				r.c.OnResponse(what)
 
 			case *base.InterleavedFrame:
-				channel := what.Channel
-				isRTP := true
-				if (channel % 2) != 0 {
-					channel--
-					isRTP = false
-				}
-
-				media, ok := r.c.tcpMediasByChannel[channel]
-				if !ok {
-					continue
-				}
-
-				if isRTP {
-					err = media.readRTP(what.Payload)
-				} else {
-					err = media.readRTCP(what.Payload)
-				}
-				if err != nil {
-					return err
+				if cb, ok := r.c.tcpCallbackByChannel[what.Channel]; ok {
+					cb(what.Payload)
 				}
 			}
 		}
diff --git a/client_udp_listener.go b/client_udp_listener.go
index 5b2664f1..088fe9cf 100644
--- a/client_udp_listener.go
+++ b/client_udp_listener.go
@@ -25,9 +25,8 @@ type clientUDPListener struct {
 	anyPortEnable bool
 	writeTimeout  time.Duration
 	pc            *net.UDPConn
-	cm            *clientMedia
-	isRTP         bool
 
+	readFunc  readFunc
 	readIP    net.IP
 	readPort  int
 	writeAddr *net.UDPAddr
@@ -35,14 +34,13 @@ type clientUDPListener struct {
 	running        bool
 	lastPacketTime *int64
 
-	readerDone chan struct{}
+	done chan struct{}
 }
 
 func newClientUDPListenerPair(
 	listenPacket func(network, address string) (net.PacketConn, error),
 	anyPortEnable bool,
 	writeTimeout time.Duration,
-	cm *clientMedia,
 ) (*clientUDPListener, *clientUDPListener) {
 	// choose two consecutive ports in range 65535-10000
 	// RTP port must be even and RTCP port odd
@@ -54,8 +52,7 @@ func newClientUDPListenerPair(
 			writeTimeout,
 			false,
 			net.JoinHostPort("", strconv.FormatInt(int64(rtpPort), 10)),
-			cm,
-			true)
+		)
 		if err != nil {
 			continue
 		}
@@ -67,8 +64,7 @@ func newClientUDPListenerPair(
 			writeTimeout,
 			false,
 			net.JoinHostPort("", strconv.FormatInt(int64(rtcpPort), 10)),
-			cm,
-			false)
+		)
 		if err != nil {
 			rtpListener.close()
 			continue
@@ -84,8 +80,6 @@ func newClientUDPListener(
 	writeTimeout time.Duration,
 	multicast bool,
 	address string,
-	cm *clientMedia,
-	isRTP bool,
 ) (*clientUDPListener, error) {
 	var pc *net.UDPConn
 	if multicast {
@@ -129,8 +123,6 @@ func newClientUDPListener(
 		anyPortEnable:  anyPortEnable,
 		writeTimeout:   writeTimeout,
 		pc:             pc,
-		cm:             cm,
-		isRTP:          isRTP,
 		lastPacketTime: int64Ptr(0),
 	}, nil
 }
@@ -149,24 +141,17 @@ func (u *clientUDPListener) port() int {
 func (u *clientUDPListener) start() {
 	u.running = true
 	u.pc.SetReadDeadline(time.Time{})
-	u.readerDone = make(chan struct{})
-	go u.runReader()
+	u.done = make(chan struct{})
+	go u.run()
 }
 
 func (u *clientUDPListener) stop() {
 	u.pc.SetReadDeadline(time.Now())
-	<-u.readerDone
+	<-u.done
 }
 
-func (u *clientUDPListener) runReader() {
-	defer close(u.readerDone)
-
-	var readFunc func([]byte) error
-	if u.isRTP {
-		readFunc = u.cm.readRTP
-	} else {
-		readFunc = u.cm.readRTCP
-	}
+func (u *clientUDPListener) run() {
+	defer close(u.done)
 
 	for {
 		buf := make([]byte, udpMaxPayloadSize+1)
@@ -192,7 +177,7 @@ func (u *clientUDPListener) runReader() {
 		now := time.Now()
 		atomic.StoreInt64(u.lastPacketTime, now.Unix())
 
-		readFunc(buf[:n])
+		u.readFunc(buf[:n])
 	}
 }
 
diff --git a/pkg/liberrors/client.go b/pkg/liberrors/client.go
index 7ad11851..39014ccf 100644
--- a/pkg/liberrors/client.go
+++ b/pkg/liberrors/client.go
@@ -163,12 +163,12 @@ func (e ErrClientTransportHeaderInvalidInterleavedIDs) Error() string {
 	return "invalid interleaved IDs"
 }
 
-// ErrClientTransportHeaderInterleavedIDsAlreadyUsed is an error that can be returned by a client.
-type ErrClientTransportHeaderInterleavedIDsAlreadyUsed struct{}
+// ErrClientTransportHeaderInterleavedIDsInUse is an error that can be returned by a client.
+type ErrClientTransportHeaderInterleavedIDsInUse struct{}
 
 // Error implements the error interface.
-func (e ErrClientTransportHeaderInterleavedIDsAlreadyUsed) Error() string {
-	return "interleaved IDs already used"
+func (e ErrClientTransportHeaderInterleavedIDsInUse) Error() string {
+	return "interleaved IDs are in use"
 }
 
 // ErrClientUDPTimeout is an error that can be returned by a client.
diff --git a/pkg/liberrors/server.go b/pkg/liberrors/server.go
index f1f404ed..23f3401d 100644
--- a/pkg/liberrors/server.go
+++ b/pkg/liberrors/server.go
@@ -132,12 +132,12 @@ func (e ErrServerTransportHeaderInvalidInterleavedIDs) Error() string {
 	return "invalid interleaved IDs"
 }
 
-// ErrServerTransportHeaderInterleavedIDsAlreadyUsed is an error that can be returned by a server.
-type ErrServerTransportHeaderInterleavedIDsAlreadyUsed struct{}
+// ErrServerTransportHeaderInterleavedIDsInUse is an error that can be returned by a server.
+type ErrServerTransportHeaderInterleavedIDsInUse struct{}
 
 // Error implements the error interface.
-func (e ErrServerTransportHeaderInterleavedIDsAlreadyUsed) Error() string {
-	return "interleaved IDs already used"
+func (e ErrServerTransportHeaderInterleavedIDsInUse) Error() string {
+	return "interleaved IDs are in use"
 }
 
 // ErrServerMediasDifferentPaths is an error that can be returned by a server.
diff --git a/server.go b/server.go
index f2c185e3..e370ffec 100644
--- a/server.go
+++ b/server.go
@@ -222,7 +222,6 @@ func (s *Server) Start() error {
 			s.WriteTimeout,
 			false,
 			s.UDPRTPAddress,
-			true,
 		)
 		if err != nil {
 			return err
@@ -233,7 +232,6 @@ func (s *Server) Start() error {
 			s.WriteTimeout,
 			false,
 			s.UDPRTCPAddress,
-			false,
 		)
 		if err != nil {
 			s.udpRTPListener.close()
diff --git a/server_conn_reader.go b/server_conn_reader.go
index 70b15c9d..cf86e1c5 100644
--- a/server_conn_reader.go
+++ b/server_conn_reader.go
@@ -106,21 +106,10 @@ func (cr *serverConnReader) readFuncTCP() error {
 
 		switch twhat := what.(type) {
 		case *base.InterleavedFrame:
-			channel := twhat.Channel
-			isRTP := true
-			if (channel % 2) != 0 {
-				channel--
-				isRTP = false
-			}
-
 			atomic.AddUint64(cr.sc.session.bytesReceived, uint64(len(twhat.Payload)))
 
-			if sm, ok := cr.sc.session.tcpMediasByChannel[channel]; ok {
-				if isRTP {
-					sm.readRTP(twhat.Payload)
-				} else {
-					sm.readRTCP(twhat.Payload)
-				}
+			if cb, ok := cr.sc.session.tcpCallbackByChannel[twhat.Channel]; ok {
+				cb(twhat.Payload)
 			}
 
 		case *base.Request:
diff --git a/server_session.go b/server_session.go
index 641d7179..03192f60 100644
--- a/server_session.go
+++ b/server_session.go
@@ -22,6 +22,8 @@ import (
 	"github.com/bluenviron/gortsplib/v3/pkg/url"
 )
 
+type readFunc func([]byte)
+
 func stringsReverseIndex(s, substr string) int {
 	for i := len(s) - 1 - len(substr); i >= 0; i-- {
 		if s[i:i+len(substr)] == substr {
@@ -99,42 +101,6 @@ func findFirstSupportedTransportHeader(s *Server, tsh headers.Transports) *heade
 	return nil
 }
 
-func findAndValidateTransport(inTH *headers.Transport,
-	tcpMediasByChannel map[int]*serverSessionMedia,
-) (Transport, error) {
-	if inTH.Protocol == headers.TransportProtocolUDP {
-		if inTH.Delivery != nil && *inTH.Delivery == headers.TransportDeliveryMulticast {
-			return TransportUDPMulticast, nil
-		}
-
-		if inTH.ClientPorts == nil {
-			return 0, liberrors.ErrServerTransportHeaderNoClientPorts{}
-		}
-		return TransportUDP, nil
-	}
-
-	if inTH.InterleavedIDs != nil {
-		if (inTH.InterleavedIDs[0]%2) != 0 ||
-			(inTH.InterleavedIDs[0]+1) != inTH.InterleavedIDs[1] {
-			return 0, liberrors.ErrServerTransportHeaderInvalidInterleavedIDs{}
-		}
-
-		if _, ok := tcpMediasByChannel[inTH.InterleavedIDs[0]]; ok {
-			return 0, liberrors.ErrServerTransportHeaderInterleavedIDsAlreadyUsed{}
-		}
-	}
-
-	return TransportTCP, nil
-}
-
-func findFreeChannel(tcpMediasByChannel map[int]*serverSessionMedia) int {
-	for i := 0; ; i += 2 {
-		if _, ok := tcpMediasByChannel[i]; !ok {
-			return i
-		}
-	}
-}
-
 // ServerSessionState is a state of a ServerSession.
 type ServerSessionState int
 
@@ -179,7 +145,7 @@ type ServerSession struct {
 	state                 ServerSessionState
 	setuppedMedias        map[*media.Media]*serverSessionMedia
 	setuppedMediasOrdered []*serverSessionMedia
-	tcpMediasByChannel    map[int]*serverSessionMedia
+	tcpCallbackByChannel  map[int]readFunc
 	setuppedTransport     *Transport
 	setuppedStream        *ServerStream // read
 	setuppedPath          *string
@@ -665,11 +631,36 @@ func (ss *ServerSession) handleRequestInner(sc *ServerConn, req *base.Request) (
 			query = ss.setuppedQuery
 		}
 
-		transport, err := findAndValidateTransport(inTH, ss.tcpMediasByChannel)
-		if err != nil {
-			return &base.Response{
-				StatusCode: base.StatusBadRequest,
-			}, err
+		var transport Transport
+
+		if inTH.Protocol == headers.TransportProtocolUDP {
+			if inTH.Delivery != nil && *inTH.Delivery == headers.TransportDeliveryMulticast {
+				transport = TransportUDPMulticast
+			} else {
+				transport = TransportUDP
+
+				if inTH.ClientPorts == nil {
+					return &base.Response{
+						StatusCode: base.StatusBadRequest,
+					}, liberrors.ErrServerTransportHeaderNoClientPorts{}
+				}
+			}
+		} else {
+			transport = TransportTCP
+
+			if inTH.InterleavedIDs != nil {
+				if (inTH.InterleavedIDs[0] + 1) != inTH.InterleavedIDs[1] {
+					return &base.Response{
+						StatusCode: base.StatusBadRequest,
+					}, liberrors.ErrServerTransportHeaderInvalidInterleavedIDs{}
+				}
+
+				if ss.isChannelPairInUse(inTH.InterleavedIDs[0]) {
+					return &base.Response{
+						StatusCode: base.StatusBadRequest,
+					}, liberrors.ErrServerTransportHeaderInterleavedIDsInUse{}
+				}
+			}
 		}
 
 		if ss.setuppedTransport != nil && *ss.setuppedTransport != transport {
@@ -829,15 +820,9 @@ func (ss *ServerSession) handleRequestInner(sc *ServerConn, req *base.Request) (
 			if inTH.InterleavedIDs != nil {
 				sm.tcpChannel = inTH.InterleavedIDs[0]
 			} else {
-				sm.tcpChannel = findFreeChannel(ss.tcpMediasByChannel)
-			}
-
-			if ss.tcpMediasByChannel == nil {
-				ss.tcpMediasByChannel = make(map[int]*serverSessionMedia)
+				sm.tcpChannel = ss.findFreeChannelPair()
 			}
 
-			ss.tcpMediasByChannel[sm.tcpChannel] = sm
-
 			th.Protocol = headers.TransportProtocolTCP
 			de := headers.TransportDeliveryUnicast
 			th.Delivery = &de
@@ -1128,6 +1113,23 @@ func (ss *ServerSession) handleRequestInner(sc *ServerConn, req *base.Request) (
 	}, nil
 }
 
+func (ss *ServerSession) isChannelPairInUse(channel int) bool {
+	for _, sm := range ss.setuppedMedias {
+		if sm.tcpChannel == channel || sm.tcpChannel == (channel+1) {
+			return true
+		}
+	}
+	return false
+}
+
+func (ss *ServerSession) findFreeChannelPair() int {
+	for i := 0; ; i += 2 { // prefer even channels
+		if !ss.isChannelPairInUse(i) {
+			return i
+		}
+	}
+}
+
 // OnPacketRTPAny sets the callback that is called when a RTP packet is read from any setupped media.
 func (ss *ServerSession) OnPacketRTPAny(cb func(*media.Media, formats.Format, *rtp.Packet)) {
 	for _, sm := range ss.setuppedMedias {
diff --git a/server_session_media.go b/server_session_media.go
index 8643c135..1c6303b3 100644
--- a/server_session_media.go
+++ b/server_session_media.go
@@ -27,8 +27,6 @@ type serverSessionMedia struct {
 	formats                map[uint8]*serverSessionFormat // record only
 	writePacketRTPInQueue  func([]byte)
 	writePacketRTCPInQueue func([]byte)
-	readRTP                func([]byte) error
-	readRTCP               func([]byte) error
 	onPacketRTCP           func(rtcp.Packet)
 }
 
@@ -61,51 +59,48 @@ func (sm *serverSessionMedia) start() {
 		sm.writePacketRTPInQueue = sm.writePacketRTPInQueueUDP
 		sm.writePacketRTCPInQueue = sm.writePacketRTCPInQueueUDP
 
-		if sm.ss.state == ServerSessionStatePlay {
-			sm.readRTCP = sm.readRTCPUDPPlay
-		} else {
-			sm.readRTP = sm.readRTPUDPRecord
-			sm.readRTCP = sm.readRTCPUDPRecord
+		if *sm.ss.setuppedTransport == TransportUDP {
+			if sm.ss.state == ServerSessionStatePlay {
+				// firewall opening is performed with RTCP sender reports generated by ServerStream
+
+				// readers can send RTCP packets only
+				sm.ss.s.udpRTCPListener.addClient(sm.ss.author.ip(), sm.udpRTCPReadPort, sm.readRTCPUDPPlay)
+			} else {
+				// open the firewall by sending empty packets to the counterpart.
+				sm.ss.WritePacketRTP(sm.media, &rtp.Packet{Header: rtp.Header{Version: 2}})
+				sm.ss.WritePacketRTCP(sm.media, &rtcp.ReceiverReport{})
+
+				sm.ss.s.udpRTPListener.addClient(sm.ss.author.ip(), sm.udpRTPReadPort, sm.readRTPUDPRecord)
+				sm.ss.s.udpRTCPListener.addClient(sm.ss.author.ip(), sm.udpRTCPReadPort, sm.readRTCPUDPRecord)
+			}
 		}
 
 	case TransportTCP:
 		sm.writePacketRTPInQueue = sm.writePacketRTPInQueueTCP
 		sm.writePacketRTCPInQueue = sm.writePacketRTCPInQueueTCP
 
+		if sm.ss.tcpCallbackByChannel == nil {
+			sm.ss.tcpCallbackByChannel = make(map[int]readFunc)
+		}
+
 		if sm.ss.state == ServerSessionStatePlay {
-			sm.readRTP = sm.readRTPTCPPlay
-			sm.readRTCP = sm.readRTCPTCPPlay
+			sm.ss.tcpCallbackByChannel[sm.tcpChannel] = sm.readRTPTCPPlay
+			sm.ss.tcpCallbackByChannel[sm.tcpChannel+1] = sm.readRTCPTCPPlay
 		} else {
-			sm.readRTP = sm.readRTPTCPRecord
-			sm.readRTCP = sm.readRTCPTCPRecord
+			sm.ss.tcpCallbackByChannel[sm.tcpChannel] = sm.readRTPTCPRecord
+			sm.ss.tcpCallbackByChannel[sm.tcpChannel+1] = sm.readRTCPTCPRecord
 		}
 
 		sm.tcpRTPFrame = &base.InterleavedFrame{Channel: sm.tcpChannel}
 		sm.tcpRTCPFrame = &base.InterleavedFrame{Channel: sm.tcpChannel + 1}
 		sm.tcpBuffer = make([]byte, udpMaxPayloadSize+4)
 	}
-
-	if *sm.ss.setuppedTransport == TransportUDP {
-		if sm.ss.state == ServerSessionStatePlay {
-			// firewall opening is performed with RTCP sender reports generated by ServerStream
-
-			// readers can send RTCP packets only
-			sm.ss.s.udpRTCPListener.addClient(sm.ss.author.ip(), sm.udpRTCPReadPort, sm)
-		} else {
-			// open the firewall by sending empty packets to the counterpart.
-			sm.ss.WritePacketRTP(sm.media, &rtp.Packet{Header: rtp.Header{Version: 2}})
-			sm.ss.WritePacketRTCP(sm.media, &rtcp.ReceiverReport{})
-
-			sm.ss.s.udpRTPListener.addClient(sm.ss.author.ip(), sm.udpRTPReadPort, sm)
-			sm.ss.s.udpRTCPListener.addClient(sm.ss.author.ip(), sm.udpRTCPReadPort, sm)
-		}
-	}
 }
 
 func (sm *serverSessionMedia) stop() {
 	if *sm.ss.setuppedTransport == TransportUDP {
-		sm.ss.s.udpRTPListener.removeClient(sm)
-		sm.ss.s.udpRTCPListener.removeClient(sm)
+		sm.ss.s.udpRTPListener.removeClient(sm.ss.author.ip(), sm.udpRTPReadPort)
+		sm.ss.s.udpRTCPListener.removeClient(sm.ss.author.ip(), sm.udpRTCPReadPort)
 	}
 
 	for _, sf := range sm.formats {
@@ -149,20 +144,20 @@ func (sm *serverSessionMedia) writePacketRTCP(payload []byte) {
 	})
 }
 
-func (sm *serverSessionMedia) readRTCPUDPPlay(payload []byte) error {
+func (sm *serverSessionMedia) readRTCPUDPPlay(payload []byte) {
 	plen := len(payload)
 
 	atomic.AddUint64(sm.ss.bytesReceived, uint64(plen))
 
 	if plen == (udpMaxPayloadSize + 1) {
 		sm.ss.onDecodeError(fmt.Errorf("RTCP packet is too big to be read with UDP"))
-		return nil
+		return
 	}
 
 	packets, err := rtcp.Unmarshal(payload)
 	if err != nil {
 		sm.ss.onDecodeError(err)
-		return nil
+		return
 	}
 
 	now := time.Now()
@@ -171,54 +166,51 @@ func (sm *serverSessionMedia) readRTCPUDPPlay(payload []byte) error {
 	for _, pkt := range packets {
 		sm.onPacketRTCP(pkt)
 	}
-
-	return nil
 }
 
-func (sm *serverSessionMedia) readRTPUDPRecord(payload []byte) error {
+func (sm *serverSessionMedia) readRTPUDPRecord(payload []byte) {
 	plen := len(payload)
 
 	atomic.AddUint64(sm.ss.bytesReceived, uint64(plen))
 
 	if plen == (udpMaxPayloadSize + 1) {
 		sm.ss.onDecodeError(fmt.Errorf("RTP packet is too big to be read with UDP"))
-		return nil
+		return
 	}
 
 	pkt := &rtp.Packet{}
 	err := pkt.Unmarshal(payload)
 	if err != nil {
 		sm.ss.onDecodeError(err)
-		return nil
+		return
 	}
 
 	forma, ok := sm.formats[pkt.PayloadType]
 	if !ok {
 		sm.ss.onDecodeError(fmt.Errorf("received RTP packet with unknown format: %d", pkt.PayloadType))
-		return nil
+		return
 	}
 
 	now := time.Now()
 	atomic.StoreInt64(sm.ss.udpLastPacketTime, now.Unix())
 
 	forma.readRTPUDP(pkt, now)
-	return nil
 }
 
-func (sm *serverSessionMedia) readRTCPUDPRecord(payload []byte) error {
+func (sm *serverSessionMedia) readRTCPUDPRecord(payload []byte) {
 	plen := len(payload)
 
 	atomic.AddUint64(sm.ss.bytesReceived, uint64(plen))
 
 	if plen == (udpMaxPayloadSize + 1) {
 		sm.ss.onDecodeError(fmt.Errorf("RTCP packet is too big to be read with UDP"))
-		return nil
+		return
 	}
 
 	packets, err := rtcp.Unmarshal(payload)
 	if err != nil {
 		sm.ss.onDecodeError(err)
-		return nil
+		return
 	}
 
 	now := time.Now()
@@ -236,67 +228,60 @@ func (sm *serverSessionMedia) readRTCPUDPRecord(payload []byte) error {
 	for _, pkt := range packets {
 		sm.onPacketRTCP(pkt)
 	}
-
-	return nil
 }
 
-func (sm *serverSessionMedia) readRTPTCPPlay(_ []byte) error {
-	return nil
+func (sm *serverSessionMedia) readRTPTCPPlay(_ []byte) {
 }
 
-func (sm *serverSessionMedia) readRTCPTCPPlay(payload []byte) error {
+func (sm *serverSessionMedia) readRTCPTCPPlay(payload []byte) {
 	if len(payload) > udpMaxPayloadSize {
 		sm.ss.onDecodeError(fmt.Errorf("RTCP packet size (%d) is greater than maximum allowed (%d)",
 			len(payload), udpMaxPayloadSize))
-		return nil
+		return
 	}
 
 	packets, err := rtcp.Unmarshal(payload)
 	if err != nil {
 		sm.ss.onDecodeError(err)
-		return nil
+		return
 	}
 
 	for _, pkt := range packets {
 		sm.onPacketRTCP(pkt)
 	}
-
-	return nil
 }
 
-func (sm *serverSessionMedia) readRTPTCPRecord(payload []byte) error {
+func (sm *serverSessionMedia) readRTPTCPRecord(payload []byte) {
 	pkt := &rtp.Packet{}
 	err := pkt.Unmarshal(payload)
 	if err != nil {
-		return err
+		sm.ss.onDecodeError(err)
+		return
 	}
 
 	forma, ok := sm.formats[pkt.PayloadType]
 	if !ok {
 		sm.ss.onDecodeError(fmt.Errorf("received RTP packet with unknown format: %d", pkt.PayloadType))
-		return nil
+		return
 	}
 
 	forma.readRTPTCP(pkt)
-	return nil
 }
 
-func (sm *serverSessionMedia) readRTCPTCPRecord(payload []byte) error {
+func (sm *serverSessionMedia) readRTCPTCPRecord(payload []byte) {
 	if len(payload) > udpMaxPayloadSize {
 		sm.ss.onDecodeError(fmt.Errorf("RTCP packet size (%d) is greater than maximum allowed (%d)",
 			len(payload), udpMaxPayloadSize))
-		return nil
+		return
 	}
 
 	packets, err := rtcp.Unmarshal(payload)
 	if err != nil {
 		sm.ss.onDecodeError(err)
-		return nil
+		return
 	}
 
 	for _, pkt := range packets {
 		sm.onPacketRTCP(pkt)
 	}
-
-	return nil
 }
diff --git a/server_stream.go b/server_stream.go
index 199a4e50..11e6262b 100644
--- a/server_stream.go
+++ b/server_stream.go
@@ -225,7 +225,7 @@ func (st *ServerStream) readerSetActive(ss *ServerSession) {
 		for medi, sm := range ss.setuppedMedias {
 			streamMedia := st.streamMedias[medi]
 			streamMedia.multicastWriter.rtcpl.addClient(
-				ss.author.ip(), streamMedia.multicastWriter.rtcpl.port(), sm)
+				ss.author.ip(), streamMedia.multicastWriter.rtcpl.port(), sm.readRTCPUDPPlay)
 		}
 	} else {
 		st.activeUnicastReaders[ss] = struct{}{}
@@ -241,9 +241,9 @@ func (st *ServerStream) readerSetInactive(ss *ServerSession) {
 	}
 
 	if *ss.setuppedTransport == TransportUDPMulticast {
-		for medi, sm := range ss.setuppedMedias {
+		for medi := range ss.setuppedMedias {
 			streamMedia := st.streamMedias[medi]
-			streamMedia.multicastWriter.rtcpl.removeClient(sm)
+			streamMedia.multicastWriter.rtcpl.removeClient(ss.author.ip(), streamMedia.multicastWriter.rtcpl.port())
 		}
 	} else {
 		delete(st.activeUnicastReaders, ss)
diff --git a/server_udp_listener.go b/server_udp_listener.go
index 1ded8a43..fd98fdf1 100644
--- a/server_udp_listener.go
+++ b/server_udp_listener.go
@@ -66,12 +66,11 @@ func (p *clientAddr) fill(ip net.IP, port int) {
 type serverUDPListener struct {
 	pc           *net.UDPConn
 	listenIP     net.IP
-	isRTP        bool
 	writeTimeout time.Duration
 	clientsMutex sync.RWMutex
-	clients      map[clientAddr]*serverSessionMedia
+	clients      map[clientAddr]readFunc
 
-	readerDone chan struct{}
+	done chan struct{}
 }
 
 func newServerUDPListenerMulticastPair(
@@ -86,7 +85,6 @@ func newServerUDPListenerMulticastPair(
 		writeTimeout,
 		true,
 		net.JoinHostPort(ip.String(), strconv.FormatInt(int64(multicastRTPPort), 10)),
-		true,
 	)
 	if err != nil {
 		return nil, nil, err
@@ -97,7 +95,6 @@ func newServerUDPListenerMulticastPair(
 		writeTimeout,
 		true,
 		net.JoinHostPort(ip.String(), strconv.FormatInt(int64(multicastRTCPPort), 10)),
-		false,
 	)
 	if err != nil {
 		rtpl.close()
@@ -112,7 +109,6 @@ func newServerUDPListener(
 	writeTimeout time.Duration,
 	multicast bool,
 	address string,
-	isRTP bool,
 ) (*serverUDPListener, error) {
 	var pc *net.UDPConn
 	var listenIP net.IP
@@ -160,20 +156,19 @@ func newServerUDPListener(
 	u := &serverUDPListener{
 		pc:           pc,
 		listenIP:     listenIP,
-		clients:      make(map[clientAddr]*serverSessionMedia),
-		isRTP:        isRTP,
+		clients:      make(map[clientAddr]readFunc),
 		writeTimeout: writeTimeout,
-		readerDone:   make(chan struct{}),
+		done:         make(chan struct{}),
 	}
 
-	go u.runReader()
+	go u.run()
 
 	return u, nil
 }
 
 func (u *serverUDPListener) close() {
 	u.pc.Close()
-	<-u.readerDone
+	<-u.done
 }
 
 func (u *serverUDPListener) ip() net.IP {
@@ -184,19 +179,8 @@ func (u *serverUDPListener) port() int {
 	return u.pc.LocalAddr().(*net.UDPAddr).Port
 }
 
-func (u *serverUDPListener) runReader() {
-	defer close(u.readerDone)
-
-	var readFunc func(*serverSessionMedia, []byte)
-	if u.isRTP {
-		readFunc = func(sm *serverSessionMedia, payload []byte) {
-			sm.readRTP(payload)
-		}
-	} else {
-		readFunc = func(sm *serverSessionMedia, payload []byte) {
-			sm.readRTCP(payload)
-		}
-	}
+func (u *serverUDPListener) run() {
+	defer close(u.done)
 
 	for {
 		buf := make([]byte, udpMaxPayloadSize+1)
@@ -211,12 +195,12 @@ func (u *serverUDPListener) runReader() {
 
 			var clientAddr clientAddr
 			clientAddr.fill(addr.IP, addr.Port)
-			sm, ok := u.clients[clientAddr]
+			cb, ok := u.clients[clientAddr]
 			if !ok {
 				return
 			}
 
-			readFunc(sm, buf[:n])
+			cb(buf[:n])
 		}()
 	}
 }
@@ -229,23 +213,22 @@ func (u *serverUDPListener) write(buf []byte, addr *net.UDPAddr) error {
 	return err
 }
 
-func (u *serverUDPListener) addClient(ip net.IP, port int, sm *serverSessionMedia) {
+func (u *serverUDPListener) addClient(ip net.IP, port int, cb readFunc) {
+	var addr clientAddr
+	addr.fill(ip, port)
+
 	u.clientsMutex.Lock()
 	defer u.clientsMutex.Unlock()
 
+	u.clients[addr] = cb
+}
+
+func (u *serverUDPListener) removeClient(ip net.IP, port int) {
 	var addr clientAddr
 	addr.fill(ip, port)
 
-	u.clients[addr] = sm
-}
-
-func (u *serverUDPListener) removeClient(sm *serverSessionMedia) {
 	u.clientsMutex.Lock()
 	defer u.clientsMutex.Unlock()
 
-	for addr, sm1 := range u.clients {
-		if sm1 == sm {
-			delete(u.clients, addr)
-		}
-	}
+	delete(u.clients, addr)
 }


Test patch:
diff --git a/client_play_test.go b/client_play_test.go
index 5ed00de7..3c3f7721 100644
--- a/client_play_test.go
+++ b/client_play_test.go
@@ -1551,7 +1551,7 @@ func TestClientPlayDifferentInterleavedIDs(t *testing.T) {
 				return &v
 			}(),
 			Protocol:       headers.TransportProtocolTCP,
-			InterleavedIDs: &[2]int{2, 3},
+			InterleavedIDs: &[2]int{1, 2}, // use odd value
 		}
 
 		err = conn.WriteResponse(&base.Response{
@@ -1573,7 +1573,7 @@ func TestClientPlayDifferentInterleavedIDs(t *testing.T) {
 		require.NoError(t, err)
 
 		err = conn.WriteInterleavedFrame(&base.InterleavedFrame{
-			Channel: 2,
+			Channel: 1,
 			Payload: testRTPPacketMarshaled,
 		}, make([]byte, 1024))
 		require.NoError(t, err)
diff --git a/server_play_test.go b/server_play_test.go
index a3b03fad..4489e2f7 100644
--- a/server_play_test.go
+++ b/server_play_test.go
@@ -644,7 +644,7 @@ func TestServerPlay(t *testing.T) {
 				v := headers.TransportDeliveryUnicast
 				inTH.Delivery = &v
 				inTH.Protocol = headers.TransportProtocolTCP
-				inTH.InterleavedIDs = &[2]int{4, 5}
+				inTH.InterleavedIDs = &[2]int{5, 6} // off value
 			}
 
 			res, th := doSetup(t, conn, absoluteControlAttribute(desc.MediaDescriptions[0]), inTH, "")
@@ -721,10 +721,10 @@ func TestServerPlay(t *testing.T) {
 				require.NoError(t, err)
 
 				switch f.Channel {
-				case 4:
+				case 5:
 					require.Equal(t, testRTPPacketMarshaled, f.Payload)
 
-				case 5:
+				case 6:
 					require.Equal(t, testRTCPPacketMarshaled, f.Payload)
 
 				default:
@@ -749,10 +749,10 @@ func TestServerPlay(t *testing.T) {
 					require.NoError(t, err)
 
 					switch f.Channel {
-					case 4:
+					case 5:
 						require.Equal(t, testRTPPacketMarshaled, f.Payload)
 
-					case 5:
+					case 6:
 						require.Equal(t, testRTCPPacketMarshaled, f.Payload)
 
 					default:
@@ -779,7 +779,7 @@ func TestServerPlay(t *testing.T) {
 
 			default:
 				err := conn.WriteInterleavedFrame(&base.InterleavedFrame{
-					Channel: 5,
+					Channel: 6,
 					Payload: testRTCPPacketMarshaled,
 				}, make([]byte, 1024))
 				require.NoError(t, err)