diff --git a/api/openapi.yaml b/api/openapi.yaml index ca288533..cfd58ed1 100644 --- a/api/openapi.yaml +++ b/api/openapi.yaml @@ -1269,6 +1269,8 @@ components: properties: dest: type: string + destFingerprint: + type: string whipBearerToken: type: string diff --git a/docs/2-features/11-forward.md b/docs/2-features/11-forward.md index ea0d22b5..92d41a54 100644 --- a/docs/2-features/11-forward.md +++ b/docs/2-features/11-forward.md @@ -28,8 +28,16 @@ We support forwarding streams by using the WebRTC protocol and the WHIP extensio paths: mypath: forward: + # use whip:// for HTTP and whips:// for HTTPS. - dest: whip://host:port/mystream/whip - whipBearerToken: mytoken + # If the destination is HTTPS and the destination TLS certificate is self-signed + # or invalid, you can provide the fingerprint of the certificate in order to + # validate it anyway. It can be obtained by running: + # openssl s_client -connect dest_ip:dest_port /dev/null | sed -n '/BEGIN/,/END/p' > server.crt + # openssl x509 -in server.crt -noout -fingerprint -sha256 | cut -d "=" -f2 | tr -d ':' + destFingerprint: + # Token to insert in the Authorization: Bearer header. + whipBearerToken: "" ``` If the remote server is a _MediaMTX_ instance, remember to add a `/whip` suffix after the stream name, since in _MediaMTX_ [it's part of the WHIP URL](../3-publish/05-webrtc-clients.md). @@ -42,7 +50,14 @@ Add the target URL inside `dest` of a `forward` entry: paths: mypath: forward: + # Use rtsp:// for plain RTSP and rtsps:// for encrypted RTSP. - dest: rtsp://user:pass@host:port/path + # If the destination is RTSPS and the destination TLS certificate is self-signed + # or invalid, you can provide the fingerprint of the certificate in order to + # validate it anyway. It can be obtained by running: + # openssl s_client -connect dest_ip:dest_port /dev/null | sed -n '/BEGIN/,/END/p' > server.crt + # openssl x509 -in server.crt -noout -fingerprint -sha256 | cut -d "=" -f2 | tr -d ':' + destFingerprint: ``` ## RTMP @@ -53,7 +68,14 @@ Add the target URL inside `dest` of a `forward` entry: paths: mypath: forward: + # Use rtmp:// for plain RTMP and rtmps:// for encrypted RTMP. - dest: rtmp://user:pass@host:port/path#streamKey + # If the destination is RTMPS and the destination TLS certificate is self-signed + # or invalid, you can provide the fingerprint of the certificate in order to + # validate it anyway. It can be obtained by running: + # openssl s_client -connect dest_ip:dest_port /dev/null | sed -n '/BEGIN/,/END/p' > server.crt + # openssl x509 -in server.crt -noout -fingerprint -sha256 | cut -d "=" -f2 | tr -d ':' + destFingerprint: ``` ## FFmpeg diff --git a/docs/2-features/27-rtsp-specific-features.md b/docs/2-features/27-rtsp-specific-features.md index e400e3bf..ee766f0d 100644 --- a/docs/2-features/27-rtsp-specific-features.md +++ b/docs/2-features/27-rtsp-specific-features.md @@ -103,7 +103,7 @@ paths: source: rtsp+http://standard-rtsp-url ``` -There are also the `rtsps+http`, `rtsp+ws`, `rtsps+ws` schemas to handle any variant. +There are also the `rtsps+http`, `rtsp+ws`, `rtsps+ws` schemes to handle any variant. ## MPEG-TS inside RTSP diff --git a/docs/3-publish/06-webrtc-servers.md b/docs/3-publish/06-webrtc-servers.md index b9f6a8e1..a3b8ddc8 100644 --- a/docs/3-publish/06-webrtc-servers.md +++ b/docs/3-publish/06-webrtc-servers.md @@ -10,8 +10,14 @@ In order to ingest a WebRTC stream from a remote server, add the corresponding W ```yml paths: proxied: - # url of the source stream. Use whep:// for HTTP and wheps:// for HTTPS + # Use whep:// for HTTP and wheps:// for HTTPS. source: whep://host:port/path + # If the source is HTTPS and the source TLS certificate is self-signed + # or invalid, you can provide the fingerprint of the certificate in order to + # validate it anyway. It can be obtained by running: + # openssl s_client -connect source_ip:source_port /dev/null | sed -n '/BEGIN/,/END/p' > server.crt + # openssl x509 -in server.crt -noout -fingerprint -sha256 | cut -d "=" -f2 | tr -d ':' + sourceFingerprint: # Token to insert in the Authorization: Bearer header. whepBearerToken: "" ``` diff --git a/docs/3-publish/08-rtsp-cameras-and-servers.md b/docs/3-publish/08-rtsp-cameras-and-servers.md index 78b24aab..1beb620e 100644 --- a/docs/3-publish/08-rtsp-cameras-and-servers.md +++ b/docs/3-publish/08-rtsp-cameras-and-servers.md @@ -11,7 +11,14 @@ Most IP cameras expose their video stream by using a RTSP server that is embedde ```yml paths: proxied: + # Use rtsp:// for plain RTSP and rtsps:// for encrypted RTSP. source: rtsp://user:pass@host:port/path + # If the source is RTSPS and the source TLS certificate is self-signed + # or invalid, you can provide the fingerprint of the certificate in order to + # validate it anyway. It can be obtained by running: + # openssl s_client -connect source_ip:source_port /dev/null | sed -n '/BEGIN/,/END/p' > server.crt + # openssl x509 -in server.crt -noout -fingerprint -sha256 | cut -d "=" -f2 | tr -d ':' + sourceFingerprint: ``` If username or password contain special characters (like ?, :, etc), they need to be [url-encoded](https://www.urlencoder.org/). diff --git a/docs/3-publish/10-rtmp-cameras-and-servers.md b/docs/3-publish/10-rtmp-cameras-and-servers.md index a3d05b0b..e644b9ff 100644 --- a/docs/3-publish/10-rtmp-cameras-and-servers.md +++ b/docs/3-publish/10-rtmp-cameras-and-servers.md @@ -10,7 +10,14 @@ You can use _MediaMTX_ to connect to one or several existing RTMP servers and re ```yml paths: proxied: + # Use rtmp:// for plain RTMP and rtmps:// for encrypted RTMP. source: rtmp://user:pass@host:port/path#streamKey + # If the source is RTMPS and the source TLS certificate is self-signed + # or invalid, you can provide the fingerprint of the certificate in order to + # validate it anyway. It can be obtained by running: + # openssl s_client -connect source_ip:source_port /dev/null | sed -n '/BEGIN/,/END/p' > server.crt + # openssl x509 -in server.crt -noout -fingerprint -sha256 | cut -d "=" -f2 | tr -d ':' + sourceFingerprint: ``` If username or password contain special characters (like ?, :, etc), they need to be [url-encoded](https://www.urlencoder.org/). diff --git a/docs/3-publish/11-hls-cameras-and-servers.md b/docs/3-publish/11-hls-cameras-and-servers.md index 9c078765..3faa8d59 100644 --- a/docs/3-publish/11-hls-cameras-and-servers.md +++ b/docs/3-publish/11-hls-cameras-and-servers.md @@ -11,7 +11,14 @@ HLS is a streaming protocol that works by splitting streams into segments, and b ```yml paths: proxied: + # Use http:// for plain HTTP and https:// for HTTPS. source: http://user:pass@host:port/path + # If the source is HTTPS and the source TLS certificate is self-signed + # or invalid, you can provide the fingerprint of the certificate in order to + # validate it anyway. It can be obtained by running: + # openssl s_client -connect source_ip:source_port /dev/null | sed -n '/BEGIN/,/END/p' > server.crt + # openssl x509 -in server.crt -noout -fingerprint -sha256 | cut -d "=" -f2 | tr -d ':' + sourceFingerprint: ``` If username or password contain special characters (like ?, :, etc), they need to be [url-encoded](https://www.urlencoder.org/). diff --git a/internal/api/api_forward_test.go b/internal/api/api_forward_test.go index d36e544a..dda38349 100644 --- a/internal/api/api_forward_test.go +++ b/internal/api/api_forward_test.go @@ -71,7 +71,7 @@ func TestForward(t *testing.T) { ID: whipID, Pos: 2, Created: time.Date(2026, 6, 18, 9, 1, 0, 0, time.UTC), - Conf: conf.ForwardDest{Dest: "whip://localhost/live/stream/whip", WhipBearerToken: "mytoken"}, + Conf: conf.ForwardDest{Dest: "whip://localhost/live/stream/whip", WHIPBearerToken: "mytoken"}, Protocol: defs.APIForwardDestProtocolWHIP, State: defs.APIForwardDestStateForwarding, OutboundBytes: 456, @@ -117,7 +117,7 @@ func TestForward(t *testing.T) { ID: whipID, Pos: 2, Created: time.Date(2026, 6, 18, 9, 1, 0, 0, time.UTC), - Conf: conf.ForwardDest{Dest: "whip://localhost/live/stream/whip", WhipBearerToken: "mytoken"}, + Conf: conf.ForwardDest{Dest: "whip://localhost/live/stream/whip", WHIPBearerToken: "mytoken"}, Protocol: defs.APIForwardDestProtocolWHIP, State: defs.APIForwardDestStateForwarding, OutboundBytes: 456, @@ -128,7 +128,7 @@ func TestForward(t *testing.T) { httpRequest(t, hc, http.MethodGet, "http://localhost:9997/v3/paths/forward/get?path=my%2Fnested%2Fstream&id="+whipID.String(), nil, &item) require.Equal(t, "whip://localhost/live/stream/whip", item.Conf.Dest) - require.Equal(t, "mytoken", item.Conf.WhipBearerToken) + require.Equal(t, "mytoken", item.Conf.WHIPBearerToken) require.Equal(t, defs.APIForwardDestProtocolWHIP, item.Protocol) require.Equal(t, defs.APIForwardDestStateForwarding, item.State) require.Equal(t, uint64(456), item.OutboundBytes) diff --git a/internal/conf/conf_test.go b/internal/conf/conf_test.go index bf3937aa..672f84c5 100644 --- a/internal/conf/conf_test.go +++ b/internal/conf/conf_test.go @@ -890,6 +890,7 @@ func TestConfErrors(t *testing.T) { " mypath:\n" + " forward:\n" + " - dest: whip://localhost/stream/whip\n" + + " destFingerprint: 0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef\n" + " whipBearerToken: mytoken\n", "", }, diff --git a/internal/conf/forward_dest.go b/internal/conf/forward_dest.go index 711f8ac2..4eec2034 100644 --- a/internal/conf/forward_dest.go +++ b/internal/conf/forward_dest.go @@ -9,7 +9,8 @@ import ( // ForwardDest is a destination to which a path is forwarded. type ForwardDest struct { Dest string `json:"dest"` - WhipBearerToken string `json:"whipBearerToken"` + DestFingerprint string `json:"destFingerprint"` + WHIPBearerToken string `json:"whipBearerToken"` } func validateForwardDest(dest string) (*url.URL, error) { diff --git a/internal/core/forward_test.go b/internal/core/forward_test.go index bae28784..b70a5fa4 100644 --- a/internal/core/forward_test.go +++ b/internal/core/forward_test.go @@ -538,7 +538,7 @@ func TestPathForwardWHIP(t *testing.T) { require.Len(t, list.Items, 1) added := list.Items[0] require.Equal(t, dest, added.Conf.Dest) - require.Equal(t, bearerToken, added.Conf.WhipBearerToken) + require.Equal(t, bearerToken, added.Conf.WHIPBearerToken) require.Equal(t, defs.APIForwardDestProtocolWHIP, added.Protocol) require.Equal(t, 1, added.Pos) @@ -550,7 +550,7 @@ func TestPathForwardWHIP(t *testing.T) { "http://localhost:9997/v3/paths/forward/get?path=source&id="+added.ID.String(), nil, &item) return item.State == defs.APIForwardDestStateForwarding && item.Protocol == defs.APIForwardDestProtocolWHIP && - item.Conf.WhipBearerToken == bearerToken && + item.Conf.WHIPBearerToken == bearerToken && item.OutboundBytes > 0 }, 5*time.Second, 100*time.Millisecond) } diff --git a/internal/forward/dest_handler.go b/internal/forward/dest_handler.go index e936e73c..17b288f1 100644 --- a/internal/forward/dest_handler.go +++ b/internal/forward/dest_handler.go @@ -185,19 +185,21 @@ func (h *DestHandler) runOnce(strm *stream.Stream) error { switch h.protocol { case defs.APIForwardDestProtocolRTMP, defs.APIForwardDestProtocolRTMPS: dest = &forwardrtmp.Dest{ - Stream: strm, - Dest: resolvedDest, - WriteTimeout: h.WriteTimeout, - Parent: h, + Stream: strm, + Dest: resolvedDest, + DestFingerprint: h.Conf.DestFingerprint, + WriteTimeout: h.WriteTimeout, + Parent: h, } case defs.APIForwardDestProtocolRTSP, defs.APIForwardDestProtocolRTSPS: dest = &forwardrtsp.Dest{ - Stream: strm, - Dest: resolvedDest, - ReadTimeout: h.ReadTimeout, - WriteTimeout: h.WriteTimeout, - Parent: h, + Stream: strm, + Dest: resolvedDest, + DestFingerprint: h.Conf.DestFingerprint, + ReadTimeout: h.ReadTimeout, + WriteTimeout: h.WriteTimeout, + Parent: h, } case defs.APIForwardDestProtocolSRT: @@ -213,8 +215,9 @@ func (h *DestHandler) runOnce(strm *stream.Stream) error { dest = &forwardwebrtc.Dest{ Stream: strm, Dest: resolvedDest, + DestFingerprint: h.Conf.DestFingerprint, ReadTimeout: h.ReadTimeout, - WhipBearerToken: h.Conf.WhipBearerToken, + BearerToken: h.Conf.WHIPBearerToken, Parent: h, } diff --git a/internal/forward/manager.go b/internal/forward/manager.go index 71d26010..072e1700 100644 --- a/internal/forward/manager.go +++ b/internal/forward/manager.go @@ -75,7 +75,7 @@ func (m *Manager) ReloadConf(forward conf.Forward) { toClose := make([]*DestHandler, 0) for i, dest := range forward { - if i < len(m.destHandlers) && m.destHandlers[i].Conf.Dest == dest.Dest { + if i < len(m.destHandlers) && m.destHandlers[i].Conf == dest { newHandlers[i] = m.destHandlers[i] } else { if i < len(m.destHandlers) { diff --git a/internal/forward/manager_test.go b/internal/forward/manager_test.go index 1c4e737a..47af8fb5 100644 --- a/internal/forward/manager_test.go +++ b/internal/forward/manager_test.go @@ -83,6 +83,7 @@ func TestManagerReloadConf(t *testing.T) { Forward: conf.Forward{ {Dest: "rtmp://localhost:5788/app/stream"}, {Dest: "rtsp://localhost:5789/stream"}, + {Dest: "whip://localhost:5790/teststream/whip", WHIPBearerToken: "mytoken"}, }, Parent: test.NilLogger, } @@ -126,13 +127,25 @@ func TestManagerReloadConf(t *testing.T) { Protocol: "rtsp", State: list1.Items[1].State, }, + { + ID: list1.Items[2].ID, + Pos: 3, + Created: list1.Items[2].Created, + Conf: conf.ForwardDest{ + Dest: "whip://localhost:5790/teststream/whip", + WHIPBearerToken: "mytoken", + }, + Protocol: "whip", + State: list1.Items[2].State, + LastError: list1.Items[2].LastError, + }, }, }, list1) m.ReloadConf(conf.Forward{ - {Dest: "rtmp://localhost:5788/app/stream"}, // unchanged - {Dest: "whip://localhost:5790/teststream/whip", WhipBearerToken: "mytoken"}, - {Dest: "rtsp://localhost:5789/stream"}, + {Dest: "rtmp://localhost:5788/app/stream"}, // unchanged + {Dest: "rtsp://localhost:5789/stream", DestFingerprint: "fingerprint"}, // changed params + {Dest: "whip://localhost:5790/teststream/whip", WHIPBearerToken: "othertoken"}, }) list2 := m.APIList() @@ -152,23 +165,31 @@ func TestManagerReloadConf(t *testing.T) { Pos: 2, Created: list2.Items[1].Created, Conf: conf.ForwardDest{ - Dest: "whip://localhost:5790/teststream/whip", - WhipBearerToken: "mytoken", + Dest: "rtsp://localhost:5789/stream", + DestFingerprint: "fingerprint", }, - Protocol: "whip", + Protocol: "rtsp", State: list2.Items[1].State, LastError: list2.Items[1].LastError, }, { - ID: list2.Items[2].ID, - Pos: 3, - Created: list2.Items[2].Created, - Conf: conf.ForwardDest{Dest: "rtsp://localhost:5789/stream"}, - Protocol: "rtsp", - State: list2.Items[2].State, + ID: list2.Items[2].ID, + Pos: 3, + Created: list2.Items[2].Created, + Conf: conf.ForwardDest{ + Dest: "whip://localhost:5790/teststream/whip", + WHIPBearerToken: "othertoken", + }, + Protocol: "whip", + State: list2.Items[2].State, + LastError: list2.Items[2].LastError, }, }, }, list2) + + require.Equal(t, list1.Items[0].ID, list2.Items[0].ID) + require.NotEqual(t, list1.Items[1].ID, list2.Items[1].ID) + require.NotEqual(t, list1.Items[2].ID, list2.Items[2].ID) }) } } diff --git a/internal/forward/rtmp/dest.go b/internal/forward/rtmp/dest.go index f747507a..7d50eaec 100644 --- a/internal/forward/rtmp/dest.go +++ b/internal/forward/rtmp/dest.go @@ -17,6 +17,7 @@ import ( "github.com/bluenviron/mediamtx/internal/conf" "github.com/bluenviron/mediamtx/internal/logger" rtmpprotocol "github.com/bluenviron/mediamtx/internal/protocols/rtmp" + ptls "github.com/bluenviron/mediamtx/internal/protocols/tls" "github.com/bluenviron/mediamtx/internal/stream" ) @@ -65,10 +66,11 @@ func fourCCList(desc *description.Session) amf0.StrictArray { // Dest is a RTMP forward destination. type Dest struct { - Stream *stream.Stream - Dest string - WriteTimeout conf.Duration - Parent logger.Writer + Stream *stream.Stream + Dest string + DestFingerprint string + WriteTimeout conf.Duration + Parent logger.Writer mutex sync.RWMutex outboundBytesFunc func() uint64 @@ -98,8 +100,9 @@ func (d *Dest) Run(ctx context.Context) error { } conn := &gortmplib.Client{ - URL: u, - Publish: true, + URL: u, + Publish: true, + TLSConfig: ptls.MakeConfig(d.DestFingerprint), } err = conn.Initialize(ctx) diff --git a/internal/forward/rtsp/dest.go b/internal/forward/rtsp/dest.go index 7bf32f9d..8c732499 100644 --- a/internal/forward/rtsp/dest.go +++ b/internal/forward/rtsp/dest.go @@ -13,17 +13,19 @@ import ( "github.com/bluenviron/mediamtx/internal/conf" "github.com/bluenviron/mediamtx/internal/logger" + ptls "github.com/bluenviron/mediamtx/internal/protocols/tls" "github.com/bluenviron/mediamtx/internal/stream" "github.com/bluenviron/mediamtx/internal/unit" ) // Dest is a RTSP forward destination. type Dest struct { - Stream *stream.Stream - Dest string - ReadTimeout conf.Duration - WriteTimeout conf.Duration - Parent logger.Writer + Stream *stream.Stream + Dest string + DestFingerprint string + ReadTimeout conf.Duration + WriteTimeout conf.Duration + Parent logger.Writer mutex sync.RWMutex outboundBytesFunc func() uint64 @@ -59,6 +61,7 @@ func (d *Dest) Run(ctx context.Context) error { Host: u.Host, ReadTimeout: time.Duration(d.ReadTimeout), WriteTimeout: time.Duration(d.WriteTimeout), + TLSConfig: ptls.MakeConfig(d.DestFingerprint), OnRequest: func(req *base.Request) { d.Log(logger.Debug, "[c->s] %v", req) }, diff --git a/internal/forward/webrtc/dest.go b/internal/forward/webrtc/dest.go index 79ae2d5a..4862516b 100644 --- a/internal/forward/webrtc/dest.go +++ b/internal/forward/webrtc/dest.go @@ -12,6 +12,7 @@ import ( "github.com/bluenviron/mediamtx/internal/conf" "github.com/bluenviron/mediamtx/internal/logger" + ptls "github.com/bluenviron/mediamtx/internal/protocols/tls" pwebrtc "github.com/bluenviron/mediamtx/internal/protocols/webrtc" "github.com/bluenviron/mediamtx/internal/protocols/whip" "github.com/bluenviron/mediamtx/internal/stream" @@ -21,8 +22,9 @@ import ( type Dest struct { Stream *stream.Stream Dest string + DestFingerprint string ReadTimeout conf.Duration - WhipBearerToken string + BearerToken string Parent logger.Writer mutex sync.RWMutex @@ -56,8 +58,6 @@ func (d *Dest) Run(ctx context.Context) error { u.Scheme = strings.Replace(u.Scheme, "whip", "http", 1) - hc := &http.Client{Timeout: time.Duration(d.ReadTimeout)} - r := &stream.Reader{Parent: d} pc := &pwebrtc.PeerConnection{} @@ -66,14 +66,22 @@ func (d *Dest) Run(ctx context.Context) error { return err } + tr := &http.Transport{ + TLSClientConfig: ptls.MakeConfig(d.DestFingerprint), + } + defer tr.CloseIdleConnections() + client := &whip.Client{ URL: u, Publish: true, OutboundTracks: pc.OutboundTracks, OutboundDataChannels: pc.OutboundDataChannels, - HTTPClient: hc, - BearerToken: d.WhipBearerToken, - Log: d, + HTTPClient: &http.Client{ + Timeout: time.Duration(d.ReadTimeout), + Transport: tr, + }, + BearerToken: d.BearerToken, + Log: d, } if err = client.Initialize(ctx); err != nil { return err diff --git a/internal/forward/webrtc/dest_test.go b/internal/forward/webrtc/dest_test.go index a68d4f02..c174cb7b 100644 --- a/internal/forward/webrtc/dest_test.go +++ b/internal/forward/webrtc/dest_test.go @@ -150,11 +150,11 @@ func TestDest(t *testing.T) { defer cancel() dest := &forwardwebrtc.Dest{ - Stream: strm, - Dest: destURL, - ReadTimeout: conf.Duration(10 * time.Second), - WhipBearerToken: bearerToken, - Parent: test.NilLogger, + Stream: strm, + Dest: destURL, + ReadTimeout: conf.Duration(10 * time.Second), + BearerToken: bearerToken, + Parent: test.NilLogger, } done := make(chan error, 1) diff --git a/mediamtx.yml b/mediamtx.yml index f4e141db..05603992 100644 --- a/mediamtx.yml +++ b/mediamtx.yml @@ -550,6 +550,12 @@ pathDefaults: # * $MTX_PATH: path name # * $G1, $G2, ...: regular expression groups, if path name is a regular expression. # - dest: + # # If the destination TLS certificate is self-signed + # # or invalid, you can provide the fingerprint of the certificate in order to + # # validate it anyway. It can be obtained by running: + # # openssl s_client -connect dest_ip:dest_port /dev/null | sed -n '/BEGIN/,/END/p' > server.crt + # # openssl x509 -in server.crt -noout -fingerprint -sha256 | cut -d "=" -f2 | tr -d ':' + # destFingerprint: # # Token to insert in the Authorization: Bearer header when using WHIP. # whipBearerToken: ""