Repository: bluenviron/mediamtx

Issue:
Expose STUNGatherTimeout
### Describe the feature

We request that MediaMTX expose a configurable option for the STUN gather timeout to enhance flexibility and adaptability in WebRTC configurations. This feature could be implemented by allowing the setting of STUNGatherTimeout via a method such as `settingsEngine.SetSTUNGatherTimeout()` and naming the configuration parameter `webrtcSTUNGatherTimeout`.

### Problem Statement:

Currently, MediaMTX does not provide an option to configure the STUN gather timeout for WebRTC connections. The default timeout may not suit all use cases, particularly in environments with varying network conditions or latency requirements. This limitation can hinder the efficiency and reliability of WebRTC applications that rely on MediaMTX.

### Proposed Solution:

Expose Configuration Option: Introduce a new parameter `webrtcSTUNGatherTimeout` in the MediaMTX configuration file.

Implementation: Use `settingsEngine.SetSTUNGatherTimeout()` internally to apply the user-defined timeout.

Default Value: Retain the current default timeout value if the user does not explicitly set `webrtcSTUNGatherTimeout`.

### Benefits:

Flexibility: Users can optimize the STUN timeout for their specific network conditions.
Performance: Enhanced connection stability and reduced latency for WebRTC applications.
User Control: Increased configurability aligns with the diverse needs of MediaMTX users.

### Real world use
For less than 1% of our `/whep` API calls, the entire call takes exactly 5 seconds, all of this time is spent gathering ICE candidates. This makes our application feel sluggish, we would like to set the value to 1-2 seconds, improving responsiveness.

We look forward to discussing this proposal further and providing any additional input required to facilitate its implementation.

Hints from issue comments:
@github-actions: This issue is being locked automatically because it has been closed for more than 6 months.
Please open a new issue in case you encounter a similar problem.

Solution patch:
diff --git a/apidocs/openapi.yaml b/apidocs/openapi.yaml
index 5d25d6fc..f51b48f8 100644
--- a/apidocs/openapi.yaml
+++ b/apidocs/openapi.yaml
@@ -290,6 +290,8 @@ components:
           type: string
         webrtcTrackGatherTimeout:
           type: string
+        webrtcSTUNGatherTimeout:
+          type: string
 
         # SRT server
         srt:
diff --git a/go.mod b/go.mod
index 22e3bade..830e498e 100644
--- a/go.mod
+++ b/go.mod
@@ -33,8 +33,8 @@ require (
 	github.com/pion/webrtc/v4 v4.0.7
 	github.com/stretchr/testify v1.10.0
 	golang.org/x/crypto v0.32.0
-	golang.org/x/sys v0.29.0
-	golang.org/x/term v0.28.0
+	golang.org/x/sys v0.30.0
+	golang.org/x/term v0.29.0
 	gopkg.in/yaml.v2 v2.4.0
 )
 
diff --git a/go.sum b/go.sum
index 40e7d0ac..abcb1824 100644
--- a/go.sum
+++ b/go.sum
@@ -284,12 +284,12 @@ golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7w
 golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU=
-golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
+golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
+golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
 golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
 golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
-golang.org/x/term v0.28.0 h1:/Ts8HFuMR2E6IP/jlo7QVLZHggjKQbhu/7H0LJFr3Gg=
-golang.org/x/term v0.28.0/go.mod h1:Sw/lC2IAUZ92udQNf3WodGtn4k/XoLyZoh8v/8uiwek=
+golang.org/x/term v0.29.0 h1:L6pJp37ocefwRRtYPKSWOWzOtWSxVajvz2ldH/xi3iU=
+golang.org/x/term v0.29.0/go.mod h1:6bl4lRlvVuDgSf3179VpIxBF0o10JUpXWOnI7nErv7s=
 golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
 golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
 golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
diff --git a/internal/conf/conf.go b/internal/conf/conf.go
index bc1e13c2..8946f6dc 100644
--- a/internal/conf/conf.go
+++ b/internal/conf/conf.go
@@ -280,6 +280,7 @@ type Conf struct {
 	WebRTCICEServers2           WebRTCICEServers `json:"webrtcICEServers2"`
 	WebRTCHandshakeTimeout      Duration         `json:"webrtcHandshakeTimeout"`
 	WebRTCTrackGatherTimeout    Duration         `json:"webrtcTrackGatherTimeout"`
+	WebRTCSTUNGatherTimeout     Duration         `json:"webrtcSTUNGatherTimeout"`
 	WebRTCICEUDPMuxAddress      *string          `json:"webrtcICEUDPMuxAddress,omitempty"`  // deprecated
 	WebRTCICETCPMuxAddress      *string          `json:"webrtcICETCPMuxAddress,omitempty"`  // deprecated
 	WebRTCICEHostNAT1To1IPs     *[]string        `json:"webrtcICEHostNAT1To1IPs,omitempty"` // deprecated
@@ -405,6 +406,7 @@ func (conf *Conf) setDefaults() {
 	conf.WebRTCICEServers2 = []WebRTCICEServer{}
 	conf.WebRTCHandshakeTimeout = 10 * Duration(time.Second)
 	conf.WebRTCTrackGatherTimeout = 2 * Duration(time.Second)
+	conf.WebRTCSTUNGatherTimeout = 5 * Duration(time.Second)
 
 	// SRT server
 	conf.SRT = true
diff --git a/internal/core/core.go b/internal/core/core.go
index 976173e4..ae207f64 100644
--- a/internal/core/core.go
+++ b/internal/core/core.go
@@ -549,6 +549,7 @@ func (p *Core) createResources(initial bool) error {
 			AdditionalHosts:       p.conf.WebRTCAdditionalHosts,
 			ICEServers:            p.conf.WebRTCICEServers2,
 			HandshakeTimeout:      p.conf.WebRTCHandshakeTimeout,
+			STUNGatherTimeout:     p.conf.WebRTCSTUNGatherTimeout,
 			TrackGatherTimeout:    p.conf.WebRTCTrackGatherTimeout,
 			ExternalCmdPool:       p.externalCmdPool,
 			PathManager:           p.pathManager,
@@ -816,6 +817,7 @@ func (p *Core) closeResources(newConf *conf.Conf, calledByAPI bool) {
 		!reflect.DeepEqual(newConf.WebRTCAdditionalHosts, p.conf.WebRTCAdditionalHosts) ||
 		!reflect.DeepEqual(newConf.WebRTCICEServers2, p.conf.WebRTCICEServers2) ||
 		newConf.WebRTCHandshakeTimeout != p.conf.WebRTCHandshakeTimeout ||
+		newConf.WebRTCSTUNGatherTimeout != p.conf.WebRTCSTUNGatherTimeout ||
 		newConf.WebRTCTrackGatherTimeout != p.conf.WebRTCTrackGatherTimeout ||
 		closeMetrics ||
 		closePathManager ||
diff --git a/internal/protocols/webrtc/peer_connection.go b/internal/protocols/webrtc/peer_connection.go
index 5d0f44b6..b58972a1 100644
--- a/internal/protocols/webrtc/peer_connection.go
+++ b/internal/protocols/webrtc/peer_connection.go
@@ -74,6 +74,7 @@ type PeerConnection struct {
 	ICETCPMux             ice.TCPMux
 	HandshakeTimeout      conf.Duration
 	TrackGatherTimeout    conf.Duration
+	STUNGatherTimeout     conf.Duration
 	LocalRandomUDP        bool
 	IPsFromInterfaces     bool
 	IPsFromInterfacesList []string
@@ -136,6 +137,7 @@ func (co *PeerConnection) Start() error {
 	}
 
 	settingsEngine.SetNetworkTypes(networkTypes)
+	settingsEngine.SetSTUNGatherTimeout(time.Duration(co.STUNGatherTimeout))
 
 	mediaEngine := &webrtc.MediaEngine{}
 
diff --git a/internal/servers/webrtc/server.go b/internal/servers/webrtc/server.go
index 54a073b9..8b52275b 100644
--- a/internal/servers/webrtc/server.go
+++ b/internal/servers/webrtc/server.go
@@ -190,6 +190,7 @@ type Server struct {
 	ICEServers            []conf.WebRTCICEServer
 	HandshakeTimeout      conf.Duration
 	TrackGatherTimeout    conf.Duration
+	STUNGatherTimeout     conf.Duration
 	ExternalCmdPool       *externalcmd.Pool
 	PathManager           serverPathManager
 	Parent                serverParent
diff --git a/internal/servers/webrtc/session.go b/internal/servers/webrtc/session.go
index 98eae7d1..ab1b4d51 100644
--- a/internal/servers/webrtc/session.go
+++ b/internal/servers/webrtc/session.go
@@ -154,6 +154,7 @@ func (s *session) runPublish() (int, error) {
 		ICEServers:            iceServers,
 		HandshakeTimeout:      s.parent.HandshakeTimeout,
 		TrackGatherTimeout:    s.parent.TrackGatherTimeout,
+		STUNGatherTimeout:     s.parent.STUNGatherTimeout,
 		IPsFromInterfaces:     s.ipsFromInterfaces,
 		IPsFromInterfacesList: s.ipsFromInterfacesList,
 		AdditionalHosts:       s.additionalHosts,
@@ -271,6 +272,7 @@ func (s *session) runRead() (int, error) {
 		ICEServers:            iceServers,
 		HandshakeTimeout:      s.parent.HandshakeTimeout,
 		TrackGatherTimeout:    s.parent.TrackGatherTimeout,
+		STUNGatherTimeout:     s.parent.STUNGatherTimeout,
 		IPsFromInterfaces:     s.ipsFromInterfaces,
 		IPsFromInterfacesList: s.ipsFromInterfacesList,
 		AdditionalHosts:       s.additionalHosts,
diff --git a/mediamtx.yml b/mediamtx.yml
index 22e35ddc..71cba5cf 100644
--- a/mediamtx.yml
+++ b/mediamtx.yml
@@ -387,6 +387,8 @@ webrtcICEServers2: []
 webrtcHandshakeTimeout: 10s
 # Maximum time to gather video tracks.
 webrtcTrackGatherTimeout: 2s
+# The maximum time to gather STUN candidates.
+webrtcSTUNGatherTimeout: 5s
 
 ###############################################
 # Global settings -> SRT server



Test patch:
diff --git a/internal/protocols/webrtc/peer_connection_test.go b/internal/protocols/webrtc/peer_connection_test.go
index 69998189..8848bbd9 100644
--- a/internal/protocols/webrtc/peer_connection_test.go
+++ b/internal/protocols/webrtc/peer_connection_test.go
@@ -16,6 +16,7 @@ func TestPeerConnectionCloseImmediately(t *testing.T) {
 	pc := &PeerConnection{
 		HandshakeTimeout:   conf.Duration(10 * time.Second),
 		TrackGatherTimeout: conf.Duration(2 * time.Second),
+		STUNGatherTimeout:  conf.Duration(5 * time.Second),
 		LocalRandomUDP:     true,
 		IPsFromInterfaces:  true,
 		Publish:            false,
diff --git a/internal/servers/webrtc/server_test.go b/internal/servers/webrtc/server_test.go
index 07171533..22fee958 100644
--- a/internal/servers/webrtc/server_test.go
+++ b/internal/servers/webrtc/server_test.go
@@ -99,6 +99,7 @@ func initializeTestServer(t *testing.T) *Server {
 		ICEServers:            []conf.WebRTCICEServer{},
 		HandshakeTimeout:      conf.Duration(10 * time.Second),
 		TrackGatherTimeout:    conf.Duration(2 * time.Second),
+		STUNGatherTimeout:     conf.Duration(5 * time.Second),
 		ExternalCmdPool:       nil,
 		PathManager:           pm,
 		Parent:                test.NilLogger,
@@ -187,6 +188,7 @@ func TestServerOptionsICEServer(t *testing.T) {
 		}},
 		HandshakeTimeout:   conf.Duration(10 * time.Second),
 		TrackGatherTimeout: conf.Duration(2 * time.Second),
+		STUNGatherTimeout:  conf.Duration(5 * time.Second),
 		ExternalCmdPool:    nil,
 		PathManager:        pathManager,
 		Parent:             test.NilLogger,
@@ -257,6 +259,7 @@ func TestServerPublish(t *testing.T) {
 		ICEServers:            []conf.WebRTCICEServer{},
 		HandshakeTimeout:      conf.Duration(10 * time.Second),
 		TrackGatherTimeout:    conf.Duration(2 * time.Second),
+		STUNGatherTimeout:     conf.Duration(5 * time.Second),
 		ExternalCmdPool:       nil,
 		PathManager:           pathManager,
 		Parent:                test.NilLogger,
@@ -544,6 +547,7 @@ func TestServerRead(t *testing.T) {
 				ICEServers:            []conf.WebRTCICEServer{},
 				HandshakeTimeout:      conf.Duration(10 * time.Second),
 				TrackGatherTimeout:    conf.Duration(2 * time.Second),
+				STUNGatherTimeout:     conf.Duration(5 * time.Second),
 				ExternalCmdPool:       nil,
 				PathManager:           pathManager,
 				Parent:                test.NilLogger,
@@ -632,6 +636,7 @@ func TestServerReadNotFound(t *testing.T) {
 		ICEServers:            []conf.WebRTCICEServer{},
 		HandshakeTimeout:      conf.Duration(10 * time.Second),
 		TrackGatherTimeout:    conf.Duration(2 * time.Second),
+		STUNGatherTimeout:     conf.Duration(5 * time.Second),
 		ExternalCmdPool:       nil,
 		PathManager:           pm,
 		Parent:                test.NilLogger,
diff --git a/internal/staticsources/webrtc/source_test.go b/internal/staticsources/webrtc/source_test.go
index 1d5d2bdd..8d787f85 100644
--- a/internal/staticsources/webrtc/source_test.go
+++ b/internal/staticsources/webrtc/source_test.go
@@ -41,6 +41,7 @@ func TestSource(t *testing.T) {
 		Publish:            true,
 		HandshakeTimeout:   conf.Duration(10 * time.Second),
 		TrackGatherTimeout: conf.Duration(2 * time.Second),
+		STUNGatherTimeout:  conf.Duration(5 * time.Second),
 		OutgoingTracks:     outgoingTracks,
 		Log:                test.NilLogger,
 	}