rtsp: support reading streams tunneled with HTTP or WebSocket (#4986)

This commit is contained in:
Alessandro Ros
2025-09-17 22:31:20 +02:00
committed by GitHub
parent 558d1c3818
commit f81c50ee68
6 changed files with 50 additions and 17 deletions

2
go.mod
View File

@@ -11,7 +11,7 @@ require (
github.com/asticode/go-astits v1.13.0 github.com/asticode/go-astits v1.13.0
github.com/bluenviron/gohlslib/v2 v2.2.3 github.com/bluenviron/gohlslib/v2 v2.2.3
github.com/bluenviron/gortmplib v0.0.0-20250916095243-72b6eaffb6a4 github.com/bluenviron/gortmplib v0.0.0-20250916095243-72b6eaffb6a4
github.com/bluenviron/gortsplib/v5 v5.0.0-20250917174653-288b637b2301 github.com/bluenviron/gortsplib/v5 v5.0.0-20250917193011-6107dea9a082
github.com/bluenviron/mediacommon/v2 v2.4.2 github.com/bluenviron/mediacommon/v2 v2.4.2
github.com/datarhei/gosrt v0.9.0 github.com/datarhei/gosrt v0.9.0
github.com/fsnotify/fsnotify v1.9.0 github.com/fsnotify/fsnotify v1.9.0

4
go.sum
View File

@@ -35,8 +35,8 @@ github.com/bluenviron/gohlslib/v2 v2.2.3 h1:1R/Jnh1kNR9UB09KAX6xjS2GcdKFRLuPd9wM
github.com/bluenviron/gohlslib/v2 v2.2.3/go.mod h1:z4Viks+Mdgcl7OcOVJ1fgSmuUwCCJBxYJPLN49n7Vnw= github.com/bluenviron/gohlslib/v2 v2.2.3/go.mod h1:z4Viks+Mdgcl7OcOVJ1fgSmuUwCCJBxYJPLN49n7Vnw=
github.com/bluenviron/gortmplib v0.0.0-20250916095243-72b6eaffb6a4 h1:ZDsCiFpmOoEy2eWcJBEhV8jb0sA9MxO/BzYmyrmm4hE= github.com/bluenviron/gortmplib v0.0.0-20250916095243-72b6eaffb6a4 h1:ZDsCiFpmOoEy2eWcJBEhV8jb0sA9MxO/BzYmyrmm4hE=
github.com/bluenviron/gortmplib v0.0.0-20250916095243-72b6eaffb6a4/go.mod h1:73VbUEJLkaixSRPRI/imEkVRkBccvR1GKrd6SUOdkrQ= github.com/bluenviron/gortmplib v0.0.0-20250916095243-72b6eaffb6a4/go.mod h1:73VbUEJLkaixSRPRI/imEkVRkBccvR1GKrd6SUOdkrQ=
github.com/bluenviron/gortsplib/v5 v5.0.0-20250917174653-288b637b2301 h1:dLEqNi4/uTN6lUM3djwWXhO0uqs9/TbRSg4AfDOJjH8= github.com/bluenviron/gortsplib/v5 v5.0.0-20250917193011-6107dea9a082 h1:x/YfIKksGOKaY/PmKg7USS81gyw2tzF7HLXk5ftWlKY=
github.com/bluenviron/gortsplib/v5 v5.0.0-20250917174653-288b637b2301/go.mod h1:zp5tDI2tkjVMgyWiy+B2tRQowqYq/GwHjdAdYFQAxrU= github.com/bluenviron/gortsplib/v5 v5.0.0-20250917193011-6107dea9a082/go.mod h1:zp5tDI2tkjVMgyWiy+B2tRQowqYq/GwHjdAdYFQAxrU=
github.com/bluenviron/mediacommon/v2 v2.4.2 h1:rggs61nTaqPcR1+RhlIE8/nDqfF5PO57QxpxBzSFfrw= github.com/bluenviron/mediacommon/v2 v2.4.2 h1:rggs61nTaqPcR1+RhlIE8/nDqfF5PO57QxpxBzSFfrw=
github.com/bluenviron/mediacommon/v2 v2.4.2/go.mod h1:zy1fODPuS/kBd93ftgJS1Jhvjq7LFWfAo32KP7By9AE= github.com/bluenviron/mediacommon/v2 v2.4.2/go.mod h1:zy1fODPuS/kBd93ftgJS1Jhvjq7LFWfAo32KP7By9AE=
github.com/bytedance/sonic v1.13.2 h1:8/H1FempDZqC4VqjptGo14QQlJx8VdZJegxs6wwfqpQ= github.com/bytedance/sonic v1.13.2 h1:8/H1FempDZqC4VqjptGo14QQlJx8VdZJegxs6wwfqpQ=

View File

@@ -4,7 +4,7 @@ import (
"encoding/json" "encoding/json"
"fmt" "fmt"
"net" "net"
gourl "net/url" "net/url"
"reflect" "reflect"
"regexp" "regexp"
"sort" "sort"
@@ -372,8 +372,12 @@ func (pconf *Path) validate(
} }
case strings.HasPrefix(pconf.Source, "rtsp://") || case strings.HasPrefix(pconf.Source, "rtsp://") ||
strings.HasPrefix(pconf.Source, "rtsps://"): strings.HasPrefix(pconf.Source, "rtsps://") ||
_, err := base.ParseURL(pconf.Source) strings.HasPrefix(pconf.Source, "rtsp+http://") ||
strings.HasPrefix(pconf.Source, "rtsps+http://") ||
strings.HasPrefix(pconf.Source, "rtsp+ws://") ||
strings.HasPrefix(pconf.Source, "rtsps+ws://"):
_, err := url.Parse(pconf.Source)
if err != nil { if err != nil {
return fmt.Errorf("'%s' is not a valid URL", pconf.Source) return fmt.Errorf("'%s' is not a valid URL", pconf.Source)
} }
@@ -390,7 +394,7 @@ func (pconf *Path) validate(
case strings.HasPrefix(pconf.Source, "rtmp://") || case strings.HasPrefix(pconf.Source, "rtmp://") ||
strings.HasPrefix(pconf.Source, "rtmps://"): strings.HasPrefix(pconf.Source, "rtmps://"):
u, err := gourl.Parse(pconf.Source) u, err := url.Parse(pconf.Source)
if err != nil { if err != nil {
return fmt.Errorf("'%s' is not a valid URL", pconf.Source) return fmt.Errorf("'%s' is not a valid URL", pconf.Source)
} }
@@ -406,15 +410,11 @@ func (pconf *Path) validate(
case strings.HasPrefix(pconf.Source, "http://") || case strings.HasPrefix(pconf.Source, "http://") ||
strings.HasPrefix(pconf.Source, "https://"): strings.HasPrefix(pconf.Source, "https://"):
u, err := gourl.Parse(pconf.Source) u, err := url.Parse(pconf.Source)
if err != nil { if err != nil {
return fmt.Errorf("'%s' is not a valid URL", pconf.Source) return fmt.Errorf("'%s' is not a valid URL", pconf.Source)
} }
if u.Scheme != "http" && u.Scheme != "https" {
return fmt.Errorf("'%s' is not a valid URL", pconf.Source)
}
if u.User != nil { if u.User != nil {
pass, _ := u.User.Password() pass, _ := u.User.Password()
user := u.User.Username() user := u.User.Username()
@@ -454,14 +454,14 @@ func (pconf *Path) validate(
} }
case strings.HasPrefix(pconf.Source, "srt://"): case strings.HasPrefix(pconf.Source, "srt://"):
_, err := gourl.Parse(pconf.Source) _, err := url.Parse(pconf.Source)
if err != nil { if err != nil {
return fmt.Errorf("'%s' is not a valid URL", pconf.Source) return fmt.Errorf("'%s' is not a valid URL", pconf.Source)
} }
case strings.HasPrefix(pconf.Source, "whep://") || case strings.HasPrefix(pconf.Source, "whep://") ||
strings.HasPrefix(pconf.Source, "wheps://"): strings.HasPrefix(pconf.Source, "wheps://"):
_, err := gourl.Parse(pconf.Source) _, err := url.Parse(pconf.Source)
if err != nil { if err != nil {
return fmt.Errorf("'%s' is not a valid URL", pconf.Source) return fmt.Errorf("'%s' is not a valid URL", pconf.Source)
} }

View File

@@ -95,7 +95,11 @@ func (s *Handler) Initialize() {
switch { switch {
case strings.HasPrefix(s.Conf.Source, "rtsp://") || case strings.HasPrefix(s.Conf.Source, "rtsp://") ||
strings.HasPrefix(s.Conf.Source, "rtsps://"): strings.HasPrefix(s.Conf.Source, "rtsps://") ||
strings.HasPrefix(s.Conf.Source, "rtsp+http://") ||
strings.HasPrefix(s.Conf.Source, "rtsps+http://") ||
strings.HasPrefix(s.Conf.Source, "rtsp+ws://") ||
strings.HasPrefix(s.Conf.Source, "rtsps+ws://"):
s.instance = &ssrtsp.Source{ s.instance = &ssrtsp.Source{
ReadTimeout: s.ReadTimeout, ReadTimeout: s.ReadTimeout,
WriteTimeout: s.WriteTimeout, WriteTimeout: s.WriteTimeout,

View File

@@ -2,6 +2,8 @@
package rtsp package rtsp
import ( import (
"net/url"
"regexp"
"time" "time"
"github.com/bluenviron/gortsplib/v5" "github.com/bluenviron/gortsplib/v5"
@@ -116,14 +118,37 @@ func (s *Source) Run(params defs.StaticSourceRunParams) error {
decodeErrors.Start() decodeErrors.Start()
defer decodeErrors.Stop() defer decodeErrors.Stop()
u, err := base.ParseURL(params.ResolvedSource) u0, err := url.Parse(params.ResolvedSource)
if err != nil {
return err
}
var scheme string
if u0.Scheme == "rtsp" || u0.Scheme == "rtsp+http" || u0.Scheme == "rtsp+ws" {
scheme = "rtsp"
} else {
scheme = "rtsps"
}
var tunnel gortsplib.Tunnel
switch u0.Scheme {
case "rtsp+http", "rtsps+http":
tunnel = gortsplib.TunnelHTTP
case "rtsp+ws", "rtsps+ws":
tunnel = gortsplib.TunnelWebSocket
default:
tunnel = gortsplib.TunnelNone
}
u, err := base.ParseURL(regexp.MustCompile("^.*?://").ReplaceAllString(params.ResolvedSource, "rtsp://"))
if err != nil { if err != nil {
return err return err
} }
c := &gortsplib.Client{ c := &gortsplib.Client{
Scheme: u.Scheme, Scheme: scheme,
Host: u.Host, Host: u.Host,
Tunnel: tunnel,
Protocol: params.Conf.RTSPTransport.Protocol, Protocol: params.Conf.RTSPTransport.Protocol,
TLSConfig: tls.MakeConfig(u.Hostname(), params.Conf.SourceFingerprint), TLSConfig: tls.MakeConfig(u.Hostname(), params.Conf.SourceFingerprint),
ReadTimeout: time.Duration(s.ReadTimeout), ReadTimeout: time.Duration(s.ReadTimeout),

View File

@@ -438,6 +438,10 @@ pathDefaults:
# * publisher -> the stream is provided by a RTSP, RTMP, WebRTC or SRT client # * publisher -> the stream is provided by a RTSP, RTMP, WebRTC or SRT client
# * rtsp://existing-url -> the stream is pulled from another RTSP server / camera # * rtsp://existing-url -> the stream is pulled from another RTSP server / camera
# * rtsps://existing-url -> the stream is pulled from another RTSP server / camera with RTSPS # * rtsps://existing-url -> the stream is pulled from another RTSP server / camera with RTSPS
# * rtsp+http://existing-url -> the stream is pulled from another RTSP server / camera, with HTTP tunneling
# * rtsps+http://existing-url -> the stream is pulled from another RTSP server / camera, with HTTPS tunneling
# * rtsp+ws://existing-url -> the stream is pulled from another RTSP server / camera, with WebSocket tunneling
# * rtsps+ws://existing-url -> the stream is pulled from another RTSP server / camera, with secure WebSocket tunneling
# * rtmp://existing-url -> the stream is pulled from another RTMP server / camera # * rtmp://existing-url -> the stream is pulled from another RTMP server / camera
# * rtmps://existing-url -> the stream is pulled from another RTMP server / camera with RTMPS # * rtmps://existing-url -> the stream is pulled from another RTMP server / camera with RTMPS
# * http://existing-url/stream.m3u8 -> the stream is pulled from another HLS server / camera # * http://existing-url/stream.m3u8 -> the stream is pulled from another HLS server / camera