mirror of
https://github.com/aler9/rtsp-simple-server
synced 2025-10-05 23:56:54 +08:00
This allows to proxy requests to other servers by using regular expressions.
This commit is contained in:
22
README.md
22
README.md
@@ -115,7 +115,8 @@ _rtsp-simple-server_ has been rebranded as _MediaMTX_. The reason is pretty obvi
|
|||||||
* [Encrypt the configuration](#encrypt-the-configuration)
|
* [Encrypt the configuration](#encrypt-the-configuration)
|
||||||
* [Remuxing, re-encoding, compression](#remuxing-re-encoding-compression)
|
* [Remuxing, re-encoding, compression](#remuxing-re-encoding-compression)
|
||||||
* [Record streams to disk](#record-streams-to-disk)
|
* [Record streams to disk](#record-streams-to-disk)
|
||||||
* [Forward streams to another server](#forward-streams-to-another-server)
|
* [Forward streams to other servers](#forward-streams-to-other-servers)
|
||||||
|
* [Proxy requests to other servers](#proxy-requests-to-other-servers)
|
||||||
* [On-demand publishing](#on-demand-publishing)
|
* [On-demand publishing](#on-demand-publishing)
|
||||||
* [Start on boot](#start-on-boot)
|
* [Start on boot](#start-on-boot)
|
||||||
* [Linux](#linux)
|
* [Linux](#linux)
|
||||||
@@ -1164,7 +1165,7 @@ To upload recordings to a remote location, you can use _MediaMTX_ together with
|
|||||||
|
|
||||||
If you want to delete local segments after they are uploaded, replace `rclone sync` with `rclone move`.
|
If you want to delete local segments after they are uploaded, replace `rclone sync` with `rclone move`.
|
||||||
|
|
||||||
### Forward streams to another server
|
### Forward streams to other servers
|
||||||
|
|
||||||
To forward incoming streams to another server, use _FFmpeg_ inside the `runOnReady` parameter:
|
To forward incoming streams to another server, use _FFmpeg_ inside the `runOnReady` parameter:
|
||||||
|
|
||||||
@@ -1173,10 +1174,25 @@ pathDefaults:
|
|||||||
runOnReady: >
|
runOnReady: >
|
||||||
ffmpeg -i rtsp://localhost:$RTSP_PORT/$MTX_PATH
|
ffmpeg -i rtsp://localhost:$RTSP_PORT/$MTX_PATH
|
||||||
-c copy
|
-c copy
|
||||||
-f rtsp rtsp://another-server/another-path
|
-f rtsp rtsp://other-server:8554/another-path
|
||||||
runOnReadyRestart: yes
|
runOnReadyRestart: yes
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Proxy requests to other servers
|
||||||
|
|
||||||
|
The server allows to proxy incoming requests to other servers or cameras. This is useful to expose servers or cameras behind a NAT. Edit `mediamtx.yml` and replace everything inside section `paths` with the following content:
|
||||||
|
|
||||||
|
```yml
|
||||||
|
paths:
|
||||||
|
"~^proxy_(.+)$":
|
||||||
|
# If path name is a regular expression, $G1, G2, etc will be replaced
|
||||||
|
# with regular expression groups.
|
||||||
|
source: rtsp://other-server:8554/$G1
|
||||||
|
sourceOnDemand: yes
|
||||||
|
```
|
||||||
|
|
||||||
|
All requests addressed to `rtsp://server:8854/proxy_a` will be forwarded to `rtsp://other-server:8854/a` and so on.
|
||||||
|
|
||||||
### On-demand publishing
|
### On-demand publishing
|
||||||
|
|
||||||
Edit `mediamtx.yml` and replace everything inside section `paths` with the following content:
|
Edit `mediamtx.yml` and replace everything inside section `paths` with the following content:
|
||||||
|
@@ -235,15 +235,16 @@ func (pconf *Path) check(conf *Conf, name string) error {
|
|||||||
|
|
||||||
// General
|
// General
|
||||||
|
|
||||||
|
if pconf.Source != "publisher" && pconf.Source != "redirect" &&
|
||||||
|
pconf.Regexp != nil && !pconf.SourceOnDemand {
|
||||||
|
return fmt.Errorf("a path with a regular expression (or path 'all') and a static source" +
|
||||||
|
" must have 'sourceOnDemand' set to true")
|
||||||
|
}
|
||||||
switch {
|
switch {
|
||||||
case pconf.Source == "publisher":
|
case pconf.Source == "publisher":
|
||||||
|
|
||||||
case strings.HasPrefix(pconf.Source, "rtsp://") ||
|
case strings.HasPrefix(pconf.Source, "rtsp://") ||
|
||||||
strings.HasPrefix(pconf.Source, "rtsps://"):
|
strings.HasPrefix(pconf.Source, "rtsps://"):
|
||||||
if pconf.Regexp != nil {
|
|
||||||
return fmt.Errorf("a path with a regular expression (or path 'all') cannot have a RTSP source. use another path")
|
|
||||||
}
|
|
||||||
|
|
||||||
_, err := base.ParseURL(pconf.Source)
|
_, err := base.ParseURL(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)
|
||||||
@@ -251,10 +252,6 @@ func (pconf *Path) check(conf *Conf, name string) error {
|
|||||||
|
|
||||||
case strings.HasPrefix(pconf.Source, "rtmp://") ||
|
case strings.HasPrefix(pconf.Source, "rtmp://") ||
|
||||||
strings.HasPrefix(pconf.Source, "rtmps://"):
|
strings.HasPrefix(pconf.Source, "rtmps://"):
|
||||||
if pconf.Regexp != nil {
|
|
||||||
return fmt.Errorf("a path with a regular expression (or path 'all') cannot have a RTMP source. use another path")
|
|
||||||
}
|
|
||||||
|
|
||||||
u, err := gourl.Parse(pconf.Source)
|
u, err := gourl.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)
|
||||||
@@ -271,10 +268,6 @@ func (pconf *Path) check(conf *Conf, name string) error {
|
|||||||
|
|
||||||
case strings.HasPrefix(pconf.Source, "http://") ||
|
case strings.HasPrefix(pconf.Source, "http://") ||
|
||||||
strings.HasPrefix(pconf.Source, "https://"):
|
strings.HasPrefix(pconf.Source, "https://"):
|
||||||
if pconf.Regexp != nil {
|
|
||||||
return fmt.Errorf("a path with a regular expression (or path 'all') cannot have a HLS source. use another path")
|
|
||||||
}
|
|
||||||
|
|
||||||
u, err := gourl.Parse(pconf.Source)
|
u, err := gourl.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)
|
||||||
@@ -293,19 +286,12 @@ func (pconf *Path) check(conf *Conf, name string) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
case strings.HasPrefix(pconf.Source, "udp://"):
|
case strings.HasPrefix(pconf.Source, "udp://"):
|
||||||
if pconf.Regexp != nil {
|
|
||||||
return fmt.Errorf("a path with a regular expression (or path 'all') cannot have a HLS source. use another path")
|
|
||||||
}
|
|
||||||
|
|
||||||
_, _, err := net.SplitHostPort(pconf.Source[len("udp://"):])
|
_, _, err := net.SplitHostPort(pconf.Source[len("udp://"):])
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("'%s' is not a valid UDP URL", pconf.Source)
|
return fmt.Errorf("'%s' is not a valid UDP URL", pconf.Source)
|
||||||
}
|
}
|
||||||
|
|
||||||
case strings.HasPrefix(pconf.Source, "srt://"):
|
case strings.HasPrefix(pconf.Source, "srt://"):
|
||||||
if pconf.Regexp != nil {
|
|
||||||
return fmt.Errorf("a path with a regular expression (or path 'all') cannot have a SRT source. use another path")
|
|
||||||
}
|
|
||||||
|
|
||||||
_, err := gourl.Parse(pconf.Source)
|
_, err := gourl.Parse(pconf.Source)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -314,11 +300,6 @@ func (pconf *Path) check(conf *Conf, name string) error {
|
|||||||
|
|
||||||
case strings.HasPrefix(pconf.Source, "whep://") ||
|
case strings.HasPrefix(pconf.Source, "whep://") ||
|
||||||
strings.HasPrefix(pconf.Source, "wheps://"):
|
strings.HasPrefix(pconf.Source, "wheps://"):
|
||||||
if pconf.Regexp != nil {
|
|
||||||
return fmt.Errorf("a path with a regular expression (or path 'all') " +
|
|
||||||
"cannot have a WebRTC/WHEP source. use another path")
|
|
||||||
}
|
|
||||||
|
|
||||||
_, err := gourl.Parse(pconf.Source)
|
_, err := gourl.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)
|
||||||
@@ -327,10 +308,6 @@ func (pconf *Path) check(conf *Conf, name string) error {
|
|||||||
case pconf.Source == "redirect":
|
case pconf.Source == "redirect":
|
||||||
|
|
||||||
case pconf.Source == "rpiCamera":
|
case pconf.Source == "rpiCamera":
|
||||||
if pconf.Regexp != nil {
|
|
||||||
return fmt.Errorf(
|
|
||||||
"a path with a regular expression (or path 'all') cannot have 'rpiCamera' as source. use another path")
|
|
||||||
}
|
|
||||||
|
|
||||||
default:
|
default:
|
||||||
return fmt.Errorf("invalid source: '%s'", pconf.Source)
|
return fmt.Errorf("invalid source: '%s'", pconf.Source)
|
||||||
|
@@ -197,12 +197,22 @@ func (pa *path) run() {
|
|||||||
if pa.conf.Source == "redirect" {
|
if pa.conf.Source == "redirect" {
|
||||||
pa.source = &sourceRedirect{}
|
pa.source = &sourceRedirect{}
|
||||||
} else if pa.conf.HasStaticSource() {
|
} else if pa.conf.HasStaticSource() {
|
||||||
pa.source = newStaticSourceHandler(
|
resolvedSource := pa.conf.Source
|
||||||
pa.conf,
|
if len(pa.matches) > 1 {
|
||||||
pa.readTimeout,
|
for i, ma := range pa.matches[1:] {
|
||||||
pa.writeTimeout,
|
resolvedSource = strings.ReplaceAll(resolvedSource, "$G"+strconv.FormatInt(int64(i+1), 10), ma)
|
||||||
pa.writeQueueSize,
|
}
|
||||||
pa)
|
}
|
||||||
|
|
||||||
|
pa.source = &staticSourceHandler{
|
||||||
|
conf: pa.conf,
|
||||||
|
readTimeout: pa.readTimeout,
|
||||||
|
writeTimeout: pa.writeTimeout,
|
||||||
|
writeQueueSize: pa.writeQueueSize,
|
||||||
|
resolvedSource: resolvedSource,
|
||||||
|
parent: pa,
|
||||||
|
}
|
||||||
|
pa.source.(*staticSourceHandler).initialize()
|
||||||
|
|
||||||
if !pa.conf.SourceOnDemand {
|
if !pa.conf.SourceOnDemand {
|
||||||
pa.source.(*staticSourceHandler).start(false)
|
pa.source.(*staticSourceHandler).start(false)
|
||||||
|
@@ -82,6 +82,25 @@ func main() {
|
|||||||
}
|
}
|
||||||
`
|
`
|
||||||
|
|
||||||
|
type testServer struct {
|
||||||
|
onDescribe func(*gortsplib.ServerHandlerOnDescribeCtx) (*base.Response, *gortsplib.ServerStream, error)
|
||||||
|
onSetup func(*gortsplib.ServerHandlerOnSetupCtx) (*base.Response, *gortsplib.ServerStream, error)
|
||||||
|
onPlay func(*gortsplib.ServerHandlerOnPlayCtx) (*base.Response, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (sh *testServer) OnDescribe(ctx *gortsplib.ServerHandlerOnDescribeCtx,
|
||||||
|
) (*base.Response, *gortsplib.ServerStream, error) {
|
||||||
|
return sh.onDescribe(ctx)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (sh *testServer) OnSetup(ctx *gortsplib.ServerHandlerOnSetupCtx) (*base.Response, *gortsplib.ServerStream, error) {
|
||||||
|
return sh.onSetup(ctx)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (sh *testServer) OnPlay(ctx *gortsplib.ServerHandlerOnPlayCtx) (*base.Response, error) {
|
||||||
|
return sh.onPlay(ctx)
|
||||||
|
}
|
||||||
|
|
||||||
var _ defs.Path = &path{}
|
var _ defs.Path = &path{}
|
||||||
|
|
||||||
func TestPathRunOnDemand(t *testing.T) {
|
func TestPathRunOnDemand(t *testing.T) {
|
||||||
@@ -573,3 +592,58 @@ func TestPathFallback(t *testing.T) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestPathSourceRegexp(t *testing.T) {
|
||||||
|
var stream *gortsplib.ServerStream
|
||||||
|
|
||||||
|
s := gortsplib.Server{
|
||||||
|
Handler: &testServer{
|
||||||
|
onDescribe: func(ctx *gortsplib.ServerHandlerOnDescribeCtx,
|
||||||
|
) (*base.Response, *gortsplib.ServerStream, error) {
|
||||||
|
require.Equal(t, "/a", ctx.Path)
|
||||||
|
return &base.Response{
|
||||||
|
StatusCode: base.StatusOK,
|
||||||
|
}, stream, nil
|
||||||
|
},
|
||||||
|
onSetup: func(ctx *gortsplib.ServerHandlerOnSetupCtx) (*base.Response, *gortsplib.ServerStream, error) {
|
||||||
|
return &base.Response{
|
||||||
|
StatusCode: base.StatusOK,
|
||||||
|
}, stream, nil
|
||||||
|
},
|
||||||
|
onPlay: func(ctx *gortsplib.ServerHandlerOnPlayCtx) (*base.Response, error) {
|
||||||
|
return &base.Response{
|
||||||
|
StatusCode: base.StatusOK,
|
||||||
|
}, nil
|
||||||
|
},
|
||||||
|
},
|
||||||
|
RTSPAddress: "127.0.0.1:8555",
|
||||||
|
}
|
||||||
|
|
||||||
|
err := s.Start()
|
||||||
|
require.NoError(t, err)
|
||||||
|
defer s.Close()
|
||||||
|
|
||||||
|
stream = gortsplib.NewServerStream(&s, &description.Session{Medias: []*description.Media{testMediaH264}})
|
||||||
|
defer stream.Close()
|
||||||
|
|
||||||
|
p, ok := newInstance(
|
||||||
|
"paths:\n" +
|
||||||
|
" '~^test_(.+)$':\n" +
|
||||||
|
" source: rtsp://127.0.0.1:8555/$G1\n" +
|
||||||
|
" sourceOnDemand: yes\n" +
|
||||||
|
" 'all':\n")
|
||||||
|
require.Equal(t, true, ok)
|
||||||
|
defer p.Close()
|
||||||
|
|
||||||
|
reader := gortsplib.Client{}
|
||||||
|
|
||||||
|
u, err := base.ParseURL("rtsp://127.0.0.1:8554/test_a")
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
err = reader.Start(u.Scheme, u.Host)
|
||||||
|
require.NoError(t, err)
|
||||||
|
defer reader.Close()
|
||||||
|
|
||||||
|
_, _, err = reader.Describe(u)
|
||||||
|
require.NoError(t, err)
|
||||||
|
}
|
||||||
|
@@ -31,6 +31,10 @@ type staticSourceHandlerParent interface {
|
|||||||
// staticSourceHandler is a static source handler.
|
// staticSourceHandler is a static source handler.
|
||||||
type staticSourceHandler struct {
|
type staticSourceHandler struct {
|
||||||
conf *conf.Path
|
conf *conf.Path
|
||||||
|
readTimeout conf.StringDuration
|
||||||
|
writeTimeout conf.StringDuration
|
||||||
|
writeQueueSize int
|
||||||
|
resolvedSource string
|
||||||
parent staticSourceHandlerParent
|
parent staticSourceHandlerParent
|
||||||
|
|
||||||
ctx context.Context
|
ctx context.Context
|
||||||
@@ -47,72 +51,66 @@ type staticSourceHandler struct {
|
|||||||
done chan struct{}
|
done chan struct{}
|
||||||
}
|
}
|
||||||
|
|
||||||
func newStaticSourceHandler(
|
func (s *staticSourceHandler) initialize() {
|
||||||
cnf *conf.Path,
|
s.chReloadConf = make(chan *conf.Path)
|
||||||
readTimeout conf.StringDuration,
|
s.chInstanceSetReady = make(chan defs.PathSourceStaticSetReadyReq)
|
||||||
writeTimeout conf.StringDuration,
|
s.chInstanceSetNotReady = make(chan defs.PathSourceStaticSetNotReadyReq)
|
||||||
writeQueueSize int,
|
|
||||||
parent staticSourceHandlerParent,
|
|
||||||
) *staticSourceHandler {
|
|
||||||
s := &staticSourceHandler{
|
|
||||||
conf: cnf,
|
|
||||||
parent: parent,
|
|
||||||
chReloadConf: make(chan *conf.Path),
|
|
||||||
chInstanceSetReady: make(chan defs.PathSourceStaticSetReadyReq),
|
|
||||||
chInstanceSetNotReady: make(chan defs.PathSourceStaticSetNotReadyReq),
|
|
||||||
}
|
|
||||||
|
|
||||||
switch {
|
switch {
|
||||||
case strings.HasPrefix(cnf.Source, "rtsp://") ||
|
case strings.HasPrefix(s.resolvedSource, "rtsp://") ||
|
||||||
strings.HasPrefix(cnf.Source, "rtsps://"):
|
strings.HasPrefix(s.resolvedSource, "rtsps://"):
|
||||||
s.instance = &rtspsource.Source{
|
s.instance = &rtspsource.Source{
|
||||||
ReadTimeout: readTimeout,
|
ResolvedSource: s.resolvedSource,
|
||||||
WriteTimeout: writeTimeout,
|
ReadTimeout: s.readTimeout,
|
||||||
WriteQueueSize: writeQueueSize,
|
WriteTimeout: s.writeTimeout,
|
||||||
|
WriteQueueSize: s.writeQueueSize,
|
||||||
Parent: s,
|
Parent: s,
|
||||||
}
|
}
|
||||||
|
|
||||||
case strings.HasPrefix(cnf.Source, "rtmp://") ||
|
case strings.HasPrefix(s.resolvedSource, "rtmp://") ||
|
||||||
strings.HasPrefix(cnf.Source, "rtmps://"):
|
strings.HasPrefix(s.resolvedSource, "rtmps://"):
|
||||||
s.instance = &rtmpsource.Source{
|
s.instance = &rtmpsource.Source{
|
||||||
ReadTimeout: readTimeout,
|
ResolvedSource: s.resolvedSource,
|
||||||
WriteTimeout: writeTimeout,
|
ReadTimeout: s.readTimeout,
|
||||||
|
WriteTimeout: s.writeTimeout,
|
||||||
Parent: s,
|
Parent: s,
|
||||||
}
|
}
|
||||||
|
|
||||||
case strings.HasPrefix(cnf.Source, "http://") ||
|
case strings.HasPrefix(s.resolvedSource, "http://") ||
|
||||||
strings.HasPrefix(cnf.Source, "https://"):
|
strings.HasPrefix(s.resolvedSource, "https://"):
|
||||||
s.instance = &hlssource.Source{
|
s.instance = &hlssource.Source{
|
||||||
ReadTimeout: readTimeout,
|
ResolvedSource: s.resolvedSource,
|
||||||
|
ReadTimeout: s.readTimeout,
|
||||||
Parent: s,
|
Parent: s,
|
||||||
}
|
}
|
||||||
|
|
||||||
case strings.HasPrefix(cnf.Source, "udp://"):
|
case strings.HasPrefix(s.resolvedSource, "udp://"):
|
||||||
s.instance = &udpsource.Source{
|
s.instance = &udpsource.Source{
|
||||||
ReadTimeout: readTimeout,
|
ResolvedSource: s.resolvedSource,
|
||||||
|
ReadTimeout: s.readTimeout,
|
||||||
Parent: s,
|
Parent: s,
|
||||||
}
|
}
|
||||||
|
|
||||||
case strings.HasPrefix(cnf.Source, "srt://"):
|
case strings.HasPrefix(s.resolvedSource, "srt://"):
|
||||||
s.instance = &srtsource.Source{
|
s.instance = &srtsource.Source{
|
||||||
ReadTimeout: readTimeout,
|
ResolvedSource: s.resolvedSource,
|
||||||
|
ReadTimeout: s.readTimeout,
|
||||||
Parent: s,
|
Parent: s,
|
||||||
}
|
}
|
||||||
|
|
||||||
case strings.HasPrefix(cnf.Source, "whep://") ||
|
case strings.HasPrefix(s.resolvedSource, "whep://") ||
|
||||||
strings.HasPrefix(cnf.Source, "wheps://"):
|
strings.HasPrefix(s.resolvedSource, "wheps://"):
|
||||||
s.instance = &webrtcsource.Source{
|
s.instance = &webrtcsource.Source{
|
||||||
ReadTimeout: readTimeout,
|
ResolvedSource: s.resolvedSource,
|
||||||
|
ReadTimeout: s.readTimeout,
|
||||||
Parent: s,
|
Parent: s,
|
||||||
}
|
}
|
||||||
|
|
||||||
case cnf.Source == "rpiCamera":
|
case s.resolvedSource == "rpiCamera":
|
||||||
s.instance = &rpicamerasource.Source{
|
s.instance = &rpicamerasource.Source{
|
||||||
Parent: s,
|
Parent: s,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return s
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *staticSourceHandler) close(reason string) {
|
func (s *staticSourceHandler) close(reason string) {
|
||||||
|
@@ -20,6 +20,7 @@ import (
|
|||||||
|
|
||||||
// Source is a HLS static source.
|
// Source is a HLS static source.
|
||||||
type Source struct {
|
type Source struct {
|
||||||
|
ResolvedSource string
|
||||||
ReadTimeout conf.StringDuration
|
ReadTimeout conf.StringDuration
|
||||||
Parent defs.StaticSourceParent
|
Parent defs.StaticSourceParent
|
||||||
}
|
}
|
||||||
@@ -43,7 +44,7 @@ func (s *Source) Run(params defs.StaticSourceRunParams) error {
|
|||||||
|
|
||||||
var c *gohlslib.Client
|
var c *gohlslib.Client
|
||||||
c = &gohlslib.Client{
|
c = &gohlslib.Client{
|
||||||
URI: params.Conf.Source,
|
URI: s.ResolvedSource,
|
||||||
HTTPClient: &http.Client{
|
HTTPClient: &http.Client{
|
||||||
Timeout: time.Duration(s.ReadTimeout),
|
Timeout: time.Duration(s.ReadTimeout),
|
||||||
Transport: &http.Transport{
|
Transport: &http.Transport{
|
||||||
|
@@ -104,12 +104,11 @@ func TestSource(t *testing.T) {
|
|||||||
te := tester.New(
|
te := tester.New(
|
||||||
func(p defs.StaticSourceParent) defs.StaticSource {
|
func(p defs.StaticSourceParent) defs.StaticSource {
|
||||||
return &Source{
|
return &Source{
|
||||||
|
ResolvedSource: "http://localhost:5780/stream.m3u8",
|
||||||
Parent: p,
|
Parent: p,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
&conf.Path{
|
&conf.Path{},
|
||||||
Source: "http://localhost:5780/stream.m3u8",
|
|
||||||
},
|
|
||||||
)
|
)
|
||||||
defer te.Close()
|
defer te.Close()
|
||||||
|
|
||||||
|
@@ -23,6 +23,7 @@ import (
|
|||||||
|
|
||||||
// Source is a RTMP static source.
|
// Source is a RTMP static source.
|
||||||
type Source struct {
|
type Source struct {
|
||||||
|
ResolvedSource string
|
||||||
ReadTimeout conf.StringDuration
|
ReadTimeout conf.StringDuration
|
||||||
WriteTimeout conf.StringDuration
|
WriteTimeout conf.StringDuration
|
||||||
Parent defs.StaticSourceParent
|
Parent defs.StaticSourceParent
|
||||||
@@ -37,7 +38,7 @@ func (s *Source) Log(level logger.Level, format string, args ...interface{}) {
|
|||||||
func (s *Source) Run(params defs.StaticSourceRunParams) error {
|
func (s *Source) Run(params defs.StaticSourceRunParams) error {
|
||||||
s.Log(logger.Debug, "connecting")
|
s.Log(logger.Debug, "connecting")
|
||||||
|
|
||||||
u, err := url.Parse(params.Conf.Source)
|
u, err := url.Parse(s.ResolvedSource)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@@ -156,26 +156,25 @@ func TestSource(t *testing.T) {
|
|||||||
te = tester.New(
|
te = tester.New(
|
||||||
func(p defs.StaticSourceParent) defs.StaticSource {
|
func(p defs.StaticSourceParent) defs.StaticSource {
|
||||||
return &Source{
|
return &Source{
|
||||||
|
ResolvedSource: "rtmp://localhost:1937/teststream",
|
||||||
ReadTimeout: conf.StringDuration(10 * time.Second),
|
ReadTimeout: conf.StringDuration(10 * time.Second),
|
||||||
WriteTimeout: conf.StringDuration(10 * time.Second),
|
WriteTimeout: conf.StringDuration(10 * time.Second),
|
||||||
Parent: p,
|
Parent: p,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
&conf.Path{
|
&conf.Path{},
|
||||||
Source: "rtmp://localhost:1937/teststream",
|
|
||||||
},
|
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
te = tester.New(
|
te = tester.New(
|
||||||
func(p defs.StaticSourceParent) defs.StaticSource {
|
func(p defs.StaticSourceParent) defs.StaticSource {
|
||||||
return &Source{
|
return &Source{
|
||||||
|
ResolvedSource: "rtmps://localhost:1937/teststream",
|
||||||
ReadTimeout: conf.StringDuration(10 * time.Second),
|
ReadTimeout: conf.StringDuration(10 * time.Second),
|
||||||
WriteTimeout: conf.StringDuration(10 * time.Second),
|
WriteTimeout: conf.StringDuration(10 * time.Second),
|
||||||
Parent: p,
|
Parent: p,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
&conf.Path{
|
&conf.Path{
|
||||||
Source: "rtmps://localhost:1937/teststream",
|
|
||||||
SourceFingerprint: "33949E05FFFB5FF3E8AA16F8213A6251B4D9363804BA53233C4DA9A46D6F2739",
|
SourceFingerprint: "33949E05FFFB5FF3E8AA16F8213A6251B4D9363804BA53233C4DA9A46D6F2739",
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
@@ -62,6 +62,7 @@ func createRangeHeader(cnf *conf.Path) (*headers.Range, error) {
|
|||||||
|
|
||||||
// Source is a RTSP static source.
|
// Source is a RTSP static source.
|
||||||
type Source struct {
|
type Source struct {
|
||||||
|
ResolvedSource string
|
||||||
ReadTimeout conf.StringDuration
|
ReadTimeout conf.StringDuration
|
||||||
WriteTimeout conf.StringDuration
|
WriteTimeout conf.StringDuration
|
||||||
WriteQueueSize int
|
WriteQueueSize int
|
||||||
@@ -103,7 +104,7 @@ func (s *Source) Run(params defs.StaticSourceRunParams) error {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
u, err := base.ParseURL(params.Conf.Source)
|
u, err := base.ParseURL(s.ResolvedSource)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@@ -201,7 +201,6 @@ func TestSource(t *testing.T) {
|
|||||||
|
|
||||||
err = s.Start()
|
err = s.Start()
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
defer s.Wait() //nolint:errcheck
|
|
||||||
defer s.Close()
|
defer s.Close()
|
||||||
|
|
||||||
stream = gortsplib.NewServerStream(&s, &description.Session{Medias: []*description.Media{testMediaH264}})
|
stream = gortsplib.NewServerStream(&s, &description.Session{Medias: []*description.Media{testMediaH264}})
|
||||||
@@ -216,6 +215,7 @@ func TestSource(t *testing.T) {
|
|||||||
te = tester.New(
|
te = tester.New(
|
||||||
func(p defs.StaticSourceParent) defs.StaticSource {
|
func(p defs.StaticSourceParent) defs.StaticSource {
|
||||||
return &Source{
|
return &Source{
|
||||||
|
ResolvedSource: "rtsp://testuser:testpass@localhost:8555/teststream",
|
||||||
ReadTimeout: conf.StringDuration(10 * time.Second),
|
ReadTimeout: conf.StringDuration(10 * time.Second),
|
||||||
WriteTimeout: conf.StringDuration(10 * time.Second),
|
WriteTimeout: conf.StringDuration(10 * time.Second),
|
||||||
WriteQueueSize: 2048,
|
WriteQueueSize: 2048,
|
||||||
@@ -223,7 +223,6 @@ func TestSource(t *testing.T) {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
&conf.Path{
|
&conf.Path{
|
||||||
Source: "rtsp://testuser:testpass@localhost:8555/teststream",
|
|
||||||
RTSPTransport: sp,
|
RTSPTransport: sp,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
@@ -231,6 +230,7 @@ func TestSource(t *testing.T) {
|
|||||||
te = tester.New(
|
te = tester.New(
|
||||||
func(p defs.StaticSourceParent) defs.StaticSource {
|
func(p defs.StaticSourceParent) defs.StaticSource {
|
||||||
return &Source{
|
return &Source{
|
||||||
|
ResolvedSource: "rtsps://testuser:testpass@localhost:8555/teststream",
|
||||||
ReadTimeout: conf.StringDuration(10 * time.Second),
|
ReadTimeout: conf.StringDuration(10 * time.Second),
|
||||||
WriteTimeout: conf.StringDuration(10 * time.Second),
|
WriteTimeout: conf.StringDuration(10 * time.Second),
|
||||||
WriteQueueSize: 2048,
|
WriteQueueSize: 2048,
|
||||||
@@ -238,7 +238,6 @@ func TestSource(t *testing.T) {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
&conf.Path{
|
&conf.Path{
|
||||||
Source: "rtsps://testuser:testpass@localhost:8555/teststream",
|
|
||||||
SourceFingerprint: "33949E05FFFB5FF3E8AA16F8213A6251B4D9363804BA53233C4DA9A46D6F2739",
|
SourceFingerprint: "33949E05FFFB5FF3E8AA16F8213A6251B4D9363804BA53233C4DA9A46D6F2739",
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
@@ -306,7 +305,6 @@ func TestRTSPSourceNoPassword(t *testing.T) {
|
|||||||
|
|
||||||
err = s.Start()
|
err = s.Start()
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
defer s.Wait() //nolint:errcheck
|
|
||||||
defer s.Close()
|
defer s.Close()
|
||||||
|
|
||||||
stream = gortsplib.NewServerStream(&s, &description.Session{Medias: []*description.Media{testMediaH264}})
|
stream = gortsplib.NewServerStream(&s, &description.Session{Medias: []*description.Media{testMediaH264}})
|
||||||
@@ -318,6 +316,7 @@ func TestRTSPSourceNoPassword(t *testing.T) {
|
|||||||
te := tester.New(
|
te := tester.New(
|
||||||
func(p defs.StaticSourceParent) defs.StaticSource {
|
func(p defs.StaticSourceParent) defs.StaticSource {
|
||||||
return &Source{
|
return &Source{
|
||||||
|
ResolvedSource: "rtsp://testuser:@127.0.0.1:8555/teststream",
|
||||||
ReadTimeout: conf.StringDuration(10 * time.Second),
|
ReadTimeout: conf.StringDuration(10 * time.Second),
|
||||||
WriteTimeout: conf.StringDuration(10 * time.Second),
|
WriteTimeout: conf.StringDuration(10 * time.Second),
|
||||||
WriteQueueSize: 2048,
|
WriteQueueSize: 2048,
|
||||||
@@ -325,7 +324,6 @@ func TestRTSPSourceNoPassword(t *testing.T) {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
&conf.Path{
|
&conf.Path{
|
||||||
Source: "rtsp://testuser:@127.0.0.1:8555/teststream",
|
|
||||||
RTSPTransport: sp,
|
RTSPTransport: sp,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
@@ -389,15 +387,12 @@ func TestRTSPSourceRange(t *testing.T) {
|
|||||||
|
|
||||||
err := s.Start()
|
err := s.Start()
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
defer s.Wait() //nolint:errcheck
|
|
||||||
defer s.Close()
|
defer s.Close()
|
||||||
|
|
||||||
stream = gortsplib.NewServerStream(&s, &description.Session{Medias: []*description.Media{testMediaH264}})
|
stream = gortsplib.NewServerStream(&s, &description.Session{Medias: []*description.Media{testMediaH264}})
|
||||||
defer stream.Close()
|
defer stream.Close()
|
||||||
|
|
||||||
cnf := &conf.Path{
|
cnf := &conf.Path{}
|
||||||
Source: "rtsp://127.0.0.1:8555/teststream",
|
|
||||||
}
|
|
||||||
|
|
||||||
switch ca {
|
switch ca {
|
||||||
case "clock":
|
case "clock":
|
||||||
@@ -416,6 +411,7 @@ func TestRTSPSourceRange(t *testing.T) {
|
|||||||
te := tester.New(
|
te := tester.New(
|
||||||
func(p defs.StaticSourceParent) defs.StaticSource {
|
func(p defs.StaticSourceParent) defs.StaticSource {
|
||||||
return &Source{
|
return &Source{
|
||||||
|
ResolvedSource: "rtsp://127.0.0.1:8555/teststream",
|
||||||
ReadTimeout: conf.StringDuration(10 * time.Second),
|
ReadTimeout: conf.StringDuration(10 * time.Second),
|
||||||
WriteTimeout: conf.StringDuration(10 * time.Second),
|
WriteTimeout: conf.StringDuration(10 * time.Second),
|
||||||
WriteQueueSize: 2048,
|
WriteQueueSize: 2048,
|
||||||
|
@@ -17,6 +17,7 @@ import (
|
|||||||
|
|
||||||
// Source is a SRT static source.
|
// Source is a SRT static source.
|
||||||
type Source struct {
|
type Source struct {
|
||||||
|
ResolvedSource string
|
||||||
ReadTimeout conf.StringDuration
|
ReadTimeout conf.StringDuration
|
||||||
Parent defs.StaticSourceParent
|
Parent defs.StaticSourceParent
|
||||||
}
|
}
|
||||||
@@ -31,7 +32,7 @@ func (s *Source) Run(params defs.StaticSourceRunParams) error {
|
|||||||
s.Log(logger.Debug, "connecting")
|
s.Log(logger.Debug, "connecting")
|
||||||
|
|
||||||
conf := srt.DefaultConfig()
|
conf := srt.DefaultConfig()
|
||||||
address, err := conf.UnmarshalURL(params.Conf.Source)
|
address, err := conf.UnmarshalURL(s.ResolvedSource)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@@ -6,7 +6,7 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/bluenviron/mediacommon/pkg/formats/mpegts"
|
"github.com/bluenviron/mediacommon/pkg/formats/mpegts"
|
||||||
"github.com/datarhei/gosrt"
|
srt "github.com/datarhei/gosrt"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
|
|
||||||
"github.com/bluenviron/mediamtx/internal/conf"
|
"github.com/bluenviron/mediamtx/internal/conf"
|
||||||
@@ -59,13 +59,12 @@ func TestSource(t *testing.T) {
|
|||||||
te := tester.New(
|
te := tester.New(
|
||||||
func(p defs.StaticSourceParent) defs.StaticSource {
|
func(p defs.StaticSourceParent) defs.StaticSource {
|
||||||
return &Source{
|
return &Source{
|
||||||
|
ResolvedSource: "srt://localhost:9002?streamid=sidname&passphrase=ttest1234567",
|
||||||
ReadTimeout: conf.StringDuration(10 * time.Second),
|
ReadTimeout: conf.StringDuration(10 * time.Second),
|
||||||
Parent: p,
|
Parent: p,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
&conf.Path{
|
&conf.Path{},
|
||||||
Source: "srt://localhost:9002?streamid=sidname&passphrase=ttest1234567",
|
|
||||||
},
|
|
||||||
)
|
)
|
||||||
defer te.Close()
|
defer te.Close()
|
||||||
|
|
||||||
|
@@ -45,6 +45,7 @@ type packetConn interface {
|
|||||||
|
|
||||||
// Source is a UDP static source.
|
// Source is a UDP static source.
|
||||||
type Source struct {
|
type Source struct {
|
||||||
|
ResolvedSource string
|
||||||
ReadTimeout conf.StringDuration
|
ReadTimeout conf.StringDuration
|
||||||
Parent defs.StaticSourceParent
|
Parent defs.StaticSourceParent
|
||||||
}
|
}
|
||||||
@@ -58,7 +59,7 @@ func (s *Source) Log(level logger.Level, format string, args ...interface{}) {
|
|||||||
func (s *Source) Run(params defs.StaticSourceRunParams) error {
|
func (s *Source) Run(params defs.StaticSourceRunParams) error {
|
||||||
s.Log(logger.Debug, "connecting")
|
s.Log(logger.Debug, "connecting")
|
||||||
|
|
||||||
hostPort := params.Conf.Source[len("udp://"):]
|
hostPort := s.ResolvedSource[len("udp://"):]
|
||||||
|
|
||||||
addr, err := net.ResolveUDPAddr("udp", hostPort)
|
addr, err := net.ResolveUDPAddr("udp", hostPort)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@@ -18,13 +18,12 @@ func TestSource(t *testing.T) {
|
|||||||
te := tester.New(
|
te := tester.New(
|
||||||
func(p defs.StaticSourceParent) defs.StaticSource {
|
func(p defs.StaticSourceParent) defs.StaticSource {
|
||||||
return &Source{
|
return &Source{
|
||||||
|
ResolvedSource: "udp://localhost:9001",
|
||||||
ReadTimeout: conf.StringDuration(10 * time.Second),
|
ReadTimeout: conf.StringDuration(10 * time.Second),
|
||||||
Parent: p,
|
Parent: p,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
&conf.Path{
|
&conf.Path{},
|
||||||
Source: "udp://localhost:9001",
|
|
||||||
},
|
|
||||||
)
|
)
|
||||||
defer te.Close()
|
defer te.Close()
|
||||||
|
|
||||||
|
@@ -19,6 +19,7 @@ import (
|
|||||||
|
|
||||||
// Source is a WebRTC static source.
|
// Source is a WebRTC static source.
|
||||||
type Source struct {
|
type Source struct {
|
||||||
|
ResolvedSource string
|
||||||
ReadTimeout conf.StringDuration
|
ReadTimeout conf.StringDuration
|
||||||
Parent defs.StaticSourceParent
|
Parent defs.StaticSourceParent
|
||||||
}
|
}
|
||||||
@@ -32,7 +33,7 @@ func (s *Source) Log(level logger.Level, format string, args ...interface{}) {
|
|||||||
func (s *Source) Run(params defs.StaticSourceRunParams) error {
|
func (s *Source) Run(params defs.StaticSourceRunParams) error {
|
||||||
s.Log(logger.Debug, "connecting")
|
s.Log(logger.Debug, "connecting")
|
||||||
|
|
||||||
u, err := url.Parse(params.Conf.Source)
|
u, err := url.Parse(s.ResolvedSource)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@@ -127,13 +127,12 @@ func TestSource(t *testing.T) {
|
|||||||
te := tester.New(
|
te := tester.New(
|
||||||
func(p defs.StaticSourceParent) defs.StaticSource {
|
func(p defs.StaticSourceParent) defs.StaticSource {
|
||||||
return &Source{
|
return &Source{
|
||||||
|
ResolvedSource: "whep://localhost:9003/my/resource",
|
||||||
ReadTimeout: conf.StringDuration(10 * time.Second),
|
ReadTimeout: conf.StringDuration(10 * time.Second),
|
||||||
Parent: p,
|
Parent: p,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
&conf.Path{
|
&conf.Path{},
|
||||||
Source: "whep://localhost:9003/my/resource",
|
|
||||||
},
|
|
||||||
)
|
)
|
||||||
defer te.Close()
|
defer te.Close()
|
||||||
|
|
||||||
|
@@ -265,6 +265,8 @@ pathDefaults:
|
|||||||
# * wheps://existing-url -> the stream is pulled from another WebRTC server / camera with HTTPS
|
# * wheps://existing-url -> the stream is pulled from another WebRTC server / camera with HTTPS
|
||||||
# * redirect -> the stream is provided by another path or server
|
# * redirect -> the stream is provided by another path or server
|
||||||
# * rpiCamera -> the stream is provided by a Raspberry Pi Camera
|
# * rpiCamera -> the stream is provided by a Raspberry Pi Camera
|
||||||
|
# If path name is a regular expression, $G1, G2, etc will be replaced
|
||||||
|
# with regular expression groups.
|
||||||
source: publisher
|
source: publisher
|
||||||
# If the source is a URL, and the source certificate is self-signed
|
# If the source is a URL, and the source certificate is self-signed
|
||||||
# or invalid, you can provide the fingerprint of the certificate in order to
|
# or invalid, you can provide the fingerprint of the certificate in order to
|
||||||
|
Reference in New Issue
Block a user