Repository: bluenviron/gortsplib
Pull request #607 (2024-08-25 18:37:06)

client: prevent switching protocol when protocol is fixed to UDP

codecov[bot]: ## [Codecov](https://app.codecov.io/gh/bluenviron/gortsplib/pull/607?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=bluenviron) Report
Attention: Patch coverage is `80.00000%` with `1 line` in your changes missing coverage. Please review.
> Project coverage is 81.26%. Comparing base [(`7df898a`)](https://app.codecov.io/gh/bluenviron/gortsplib/commit/7df898adbbfda4078f74370ca99edab8aa4a311c?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=bluenviron) to head [(`687e359`)](https://app.codecov.io/gh/bluenviron/gortsplib/commit/687e359dc0aea297d71bf82828acf3fe22c0e22b?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=bluenviron).

| [Files](https://app.codecov.io/gh/bluenviron/gortsplib/pull/607?dropdown=coverage&src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=bluenviron) | Patch % | Lines |
|---|---|---|
| [client.go](https://app.codecov.io/gh/bluenviron/gortsplib/pull/607?src=pr&el=tree&filepath=client.go&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=bluenviron#diff-Y2xpZW50Lmdv) | 80.00% | [1 Missing :warning: ](https://app.codecov.io/gh/bluenviron/gortsplib/pull/607?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=bluenviron) |

<details><summary>Additional details and impacted files</summary>


```diff
@@            Coverage Diff             @@
##             main     #607      +/-   ##
==========================================
+ Coverage   81.21%   81.26%   +0.05%     
==========================================
  Files         115      115              
  Lines       11607    11607              
==========================================
+ Hits         9427     9433       +6     
+ Misses       1771     1767       -4     
+ Partials      409      407       -2     
```

</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/bluenviron/gortsplib/pull/607?dropdown=coverage&src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=bluenviron).   
:loudspeaker: Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/?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 8c3235eb..e7d75157 100644
--- a/client.go
+++ b/client.go
@@ -971,15 +971,15 @@ func (c *Client) atLeastOneUDPPacketHasBeenReceived() bool {
 	for _, ct := range c.medias {
 		lft := atomic.LoadInt64(ct.udpRTPListener.lastPacketTime)
 		if lft != 0 {
-			return false
+			return true
 		}
 
 		lft = atomic.LoadInt64(ct.udpRTCPListener.lastPacketTime)
 		if lft != 0 {
-			return false
+			return true
 		}
 	}
-	return true
+	return false
 }
 
 func (c *Client) isInUDPTimeout() bool {
@@ -1007,10 +1007,10 @@ func (c *Client) isInTCPTimeout() bool {
 func (c *Client) doCheckTimeout() error {
 	if *c.effectiveTransport == TransportUDP ||
 		*c.effectiveTransport == TransportUDPMulticast {
-		if c.checkTimeoutInitial && !c.backChannelSetupped {
+		if c.checkTimeoutInitial && !c.backChannelSetupped && c.Transport == nil {
 			c.checkTimeoutInitial = false
 
-			if c.atLeastOneUDPPacketHasBeenReceived() {
+			if !c.atLeastOneUDPPacketHasBeenReceived() {
 				err := c.trySwitchingProtocol()
 				if err != nil {
 					return err
diff --git a/pkg/headers/transport.go b/pkg/headers/transport.go
index 9ad23f36..dc714bf5 100644
--- a/pkg/headers/transport.go
+++ b/pkg/headers/transport.go
@@ -47,6 +47,14 @@ const (
 	TransportProtocolTCP
 )
 
+// String implements fmt.Stringer.
+func (p TransportProtocol) String() string {
+	if p == TransportProtocolUDP {
+		return "RTP/AVP"
+	}
+	return "RTP/AVP/TCP"
+}
+
 // TransportDelivery is a delivery method.
 type TransportDelivery int
 
@@ -56,6 +64,14 @@ const (
 	TransportDeliveryMulticast
 )
 
+// String implements fmt.Stringer.
+func (d TransportDelivery) String() string {
+	if d == TransportDeliveryUnicast {
+		return "unicast"
+	}
+	return "multicast"
+}
+
 // TransportMode is a transport mode.
 type TransportMode int
 
@@ -67,6 +83,33 @@ const (
 	TransportModeRecord
 )
 
+func (m *TransportMode) unmarshal(v string) error {
+	str := strings.ToLower(v)
+
+	switch str {
+	case "play":
+		*m = TransportModePlay
+		return nil
+
+		// receive is an old alias for record, used by ffmpeg with the
+		// -listen flag, and by Darwin Streaming Server
+	case "record", "receive":
+		*m = TransportModeRecord
+		return nil
+
+	default:
+		return fmt.Errorf("invalid transport mode: '%s'", str)
+	}
+}
+
+// String implements fmt.Stringer.
+func (m TransportMode) String() string {
+	if m == TransportModePlay {
+		return "play"
+	}
+	return "record"
+}
+
 // Transport is a Transport header.
 type Transport struct {
 	// protocol of the stream
@@ -218,24 +261,12 @@ func (h *Transport) Unmarshal(v base.HeaderValue) error {
 			}
 
 		case "mode":
-			str := strings.ToLower(v)
-			str = strings.TrimPrefix(str, "\"")
-			str = strings.TrimSuffix(str, "\"")
-
-			switch str {
-			case "play":
-				v := TransportModePlay
-				h.Mode = &v
-
-				// receive is an old alias for record, used by ffmpeg with the
-				// -listen flag, and by Darwin Streaming Server
-			case "record", "receive":
-				v := TransportModeRecord
-				h.Mode = &v
-
-			default:
-				return fmt.Errorf("invalid transport mode: '%s'", str)
+			var m TransportMode
+			err = m.unmarshal(v)
+			if err != nil {
+				return err
 			}
+			h.Mode = &m
 
 		default:
 			// ignore non-standard keys
@@ -253,18 +284,10 @@ func (h *Transport) Unmarshal(v base.HeaderValue) error {
 func (h Transport) Marshal() base.HeaderValue {
 	var rets []string
 
-	if h.Protocol == TransportProtocolUDP {
-		rets = append(rets, "RTP/AVP")
-	} else {
-		rets = append(rets, "RTP/AVP/TCP")
-	}
+	rets = append(rets, h.Protocol.String())
 
 	if h.Delivery != nil {
-		if *h.Delivery == TransportDeliveryUnicast {
-			rets = append(rets, "unicast")
-		} else {
-			rets = append(rets, "multicast")
-		}
+		rets = append(rets, h.Delivery.String())
 	}
 
 	if h.Source != nil {
@@ -309,11 +332,7 @@ func (h Transport) Marshal() base.HeaderValue {
 	}
 
 	if h.Mode != nil {
-		if *h.Mode == TransportModePlay {
-			rets = append(rets, "mode=play")
-		} else {
-			rets = append(rets, "mode=record")
-		}
+		rets = append(rets, "mode="+h.Mode.String())
 	}
 
 	return base.HeaderValue{strings.Join(rets, ";")}
diff --git a/pkg/liberrors/server.go b/pkg/liberrors/server.go
index bcd345d4..22ab635b 100644
--- a/pkg/liberrors/server.go
+++ b/pkg/liberrors/server.go
@@ -84,12 +84,16 @@ func (e ErrServerMediaNotFound) Error() string {
 
 // ErrServerTransportHeaderInvalidMode is an error that can be returned by a server.
 type ErrServerTransportHeaderInvalidMode struct {
-	Mode headers.TransportMode
+	Mode *headers.TransportMode
 }
 
 // Error implements the error interface.
 func (e ErrServerTransportHeaderInvalidMode) Error() string {
-	return fmt.Sprintf("transport header contains a invalid mode (%v)", e.Mode)
+	m := "null"
+	if e.Mode != nil {
+		m = e.Mode.String()
+	}
+	return fmt.Sprintf("transport header contains a invalid mode (%v)", m)
 }
 
 // ErrServerTransportHeaderNoClientPorts is an error that can be returned by a server.
diff --git a/server_session.go b/server_session.go
index affc7433..c8760388 100644
--- a/server_session.go
+++ b/server_session.go
@@ -728,7 +728,7 @@ func (ss *ServerSession) handleRequestInner(sc *ServerConn, req *base.Request) (
 			if inTH.Mode != nil && *inTH.Mode != headers.TransportModePlay {
 				return &base.Response{
 					StatusCode: base.StatusBadRequest,
-				}, liberrors.ErrServerTransportHeaderInvalidMode{Mode: *inTH.Mode}
+				}, liberrors.ErrServerTransportHeaderInvalidMode{Mode: inTH.Mode}
 			}
 
 		default: // record
@@ -741,7 +741,7 @@ func (ss *ServerSession) handleRequestInner(sc *ServerConn, req *base.Request) (
 			if inTH.Mode == nil || *inTH.Mode != headers.TransportModeRecord {
 				return &base.Response{
 					StatusCode: base.StatusBadRequest,
-				}, liberrors.ErrServerTransportHeaderInvalidMode{Mode: *inTH.Mode}
+				}, liberrors.ErrServerTransportHeaderInvalidMode{Mode: inTH.Mode}
 			}
 		}
 


Test patch:
diff --git a/client_play_test.go b/client_play_test.go
index 9b5ece83..f505598f 100644
--- a/client_play_test.go
+++ b/client_play_test.go
@@ -2278,7 +2278,7 @@ func TestClientPlayErrorTimeout(t *testing.T) {
 				})
 				require.NoError(t, err2)
 
-				if transport == "udp" || transport == "auto" {
+				if transport == "auto" {
 					// write a packet to skip the protocol autodetection feature
 					_, err2 = l1.WriteTo(testRTPPacketMarshaled, &net.UDPAddr{
 						IP:   net.ParseIP("127.0.0.1"),
diff --git a/server_record_test.go b/server_record_test.go
index f82771d5..9cc5acd6 100644
--- a/server_record_test.go
+++ b/server_record_test.go
@@ -110,7 +110,7 @@ func TestServerRecordErrorAnnounce(t *testing.T) {
 			"unsupported Content-Type header '[aa]'",
 		},
 		{
-			"invalid medias",
+			"invalid sdp",
 			base.Request{
 				Method: base.Announce,
 				URL:    mustParseURL("rtsp://localhost:8554/teststream"),
@@ -122,6 +122,29 @@ func TestServerRecordErrorAnnounce(t *testing.T) {
 			},
 			"invalid SDP: invalid line: (\x01\x02\x03\x04)",
 		},
+		{
+			"invalid session",
+			base.Request{
+				Method: base.Announce,
+				URL:    mustParseURL("rtsp://localhost:8554/teststream"),
+				Header: base.Header{
+					"CSeq":         base.HeaderValue{"1"},
+					"Content-Type": base.HeaderValue{"application/sdp"},
+				},
+				Body: []byte("v=0\r\n" +
+					"o=- 0 0 IN IP4 127.0.0.1\r\n" +
+					"s=-\r\n" +
+					"c=IN IP4 0.0.0.0\r\n" +
+					"t=0 0\r\n" +
+					"m=video 0 RTP/AVP 96\r\n" +
+					"a=control\r\n" +
+					"a=rtpmap:97 H264/90000\r\n" +
+					"a=fmtp:aa packetization-mode=1; profile-level-id=4D002A; " +
+					"sprop-parameter-sets=Z00AKp2oHgCJ+WbgICAgQA==,aO48gA==\r\n",
+				),
+			},
+			"invalid SDP: media 1 is invalid: clock rate not found",
+		},
 		{
 			"invalid URL 1",
 			invalidURLAnnounceReq(t, "rtsp://  aaaaa"),
@@ -168,6 +191,87 @@ func TestServerRecordErrorAnnounce(t *testing.T) {
 	}
 }
 
+func TestServerRecordErrorSetup(t *testing.T) {
+	for _, ca := range []struct {
+		name string
+		err  string
+	}{
+		{
+			"invalid transport",
+			"transport header contains a invalid mode (null)",
+		},
+	} {
+		t.Run(ca.name, func(t *testing.T) {
+			s := &Server{
+				Handler: &testServerHandler{
+					onConnClose: func(ctx *ServerHandlerOnConnCloseCtx) {
+						require.EqualError(t, ctx.Error, ca.err)
+					},
+					onAnnounce: func(_ *ServerHandlerOnAnnounceCtx) (*base.Response, error) {
+						return &base.Response{
+							StatusCode: base.StatusOK,
+						}, nil
+					},
+					onSetup: func(_ *ServerHandlerOnSetupCtx) (*base.Response, *ServerStream, error) {
+						return &base.Response{
+							StatusCode: base.StatusOK,
+						}, nil, nil
+					},
+					onRecord: func(_ *ServerHandlerOnRecordCtx) (*base.Response, error) {
+						return &base.Response{
+							StatusCode: base.StatusOK,
+						}, nil
+					},
+					onPause: func(_ *ServerHandlerOnPauseCtx) (*base.Response, error) {
+						return &base.Response{
+							StatusCode: base.StatusOK,
+						}, nil
+					},
+				},
+				RTSPAddress:    "localhost:8554",
+				UDPRTPAddress:  "127.0.0.1:8000",
+				UDPRTCPAddress: "127.0.0.1:8001",
+			}
+
+			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)
+
+			medias := []*description.Media{testH264Media}
+
+			doAnnounce(t, conn, "rtsp://localhost:8554/teststream", medias)
+
+			var inTH *headers.Transport
+
+			switch ca.name {
+			case "invalid transport":
+				inTH = &headers.Transport{
+					Delivery:    deliveryPtr(headers.TransportDeliveryUnicast),
+					Mode:        nil,
+					Protocol:    headers.TransportProtocolUDP,
+					ClientPorts: &[2]int{35466, 35467},
+				}
+			}
+
+			res, err := writeReqReadRes(conn, base.Request{
+				Method: base.Setup,
+				URL:    mustParseURL("rtsp://localhost:8554/teststream/" + medias[0].Control),
+				Header: base.Header{
+					"CSeq":      base.HeaderValue{"1"},
+					"Transport": inTH.Marshal(),
+				},
+			})
+			require.NoError(t, err)
+			require.NotEqual(t, base.StatusOK, res.StatusCode)
+		})
+	}
+}
+
 func TestServerRecordPath(t *testing.T) {
 	for _, ca := range []struct {
 		name        string