Skip to content

Commit

Permalink
implement redirect sources (#92)
Browse files Browse the repository at this point in the history
  • Loading branch information
aler9 committed Oct 27, 2020
1 parent 67efdb6 commit 53ba724
Show file tree
Hide file tree
Showing 6 changed files with 168 additions and 81 deletions.
7 changes: 4 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,15 @@
[![Go Report Card](https://goreportcard.com/badge/github.com/aler9/rtsp-simple-server)](https://goreportcard.com/report/github.com/aler9/rtsp-simple-server)
[![Docker Hub](https://img.shields.io/badge/docker-aler9%2Frtsp--simple--server-blue)](https://hub.docker.com/r/aler9/rtsp-simple-server)

_rtsp-simple-server_ is a simple, ready-to-use and zero-dependency RTSP server and RTSP proxy, a software that allows multiple users to publish and read live video and audio streams over time. RTSP is a standard protocol that describes how to perform these operations with the help of a server, that is contacted by both readers and publishers in order to negotiate a streaming method, then relays the publisher streams to the readers.
_rtsp-simple-server_ is a simple, ready-to-use and zero-dependency RTSP server and RTSP proxy, a software that allows multiple users to publish and read live video and audio streams over time. RTSP is a standard protocol that describes how to perform these operations with the help of a server, that is contacted by both readers and publishers and relays the publisher streams to the readers.

Features:
* Read and publish live streams with UDP and TCP
* Each stream can have multiple video and audio tracks, encoded with any codec (including H264, H265, VP8, VP9, MP3, AAC, Opus, PCM)
* Publish multiple streams at once, each in a separate path, that can be read by multiple users
* Serve multiple streams at once, each in a separate path, that can be read by multiple users
* Pull and serve streams from other RTSP or RTMP servers, always or on-demand (RTSP proxy)
* Provide separate authentication for reading and publishing
* Redirect to other RTSP servers (load balancing)
* Authenticate readers and publishers separately
* Run custom commands when clients connect, disconnect, read or publish streams
* Compatible with Linux, Windows and Mac, does not require any dependency or interpreter, it's a single executable

Expand Down
20 changes: 16 additions & 4 deletions client/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,8 +39,9 @@ type streamTrack struct {
}

type describeData struct {
sdp []byte
err error
sdp []byte
redirect string
err error
}

type state int
Expand Down Expand Up @@ -889,6 +890,17 @@ func (c *Client) runWaitingDescribe() bool {
return true
}

if res.redirect != "" {
c.conn.WriteResponse(&base.Response{
StatusCode: base.StatusMovedPermanently,
Header: base.Header{
"CSeq": c.describeCSeq,
"Location": base.HeaderValue{res.redirect},
},
})
return true
}

c.conn.WriteResponse(&base.Response{
StatusCode: base.StatusOK,
Header: base.Header{
Expand Down Expand Up @@ -1309,6 +1321,6 @@ func (c *Client) OnReaderFrame(trackId int, streamType base.StreamType, buf []by
}
}

func (c *Client) OnPathDescribeData(sdp []byte, err error) {
c.describeData <- describeData{sdp, err}
func (c *Client) OnPathDescribeData(sdp []byte, redirect string, err error) {
c.describeData <- describeData{sdp, redirect, err}
}
14 changes: 14 additions & 0 deletions conf/pathconf.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ type PathConf struct {
SourceProtocol string `yaml:"sourceProtocol"`
SourceProtocolParsed gortsplib.StreamProtocol `yaml:"-" json:"-"`
SourceOnDemand bool `yaml:"sourceOnDemand"`
SourceRedirect string `yaml:"sourceRedirect"`
RunOnInit string `yaml:"runOnInit"`
RunOnDemand string `yaml:"runOnDemand"`
RunOnPublish string `yaml:"runOnPublish"`
Expand Down Expand Up @@ -64,6 +65,7 @@ func (pconf *PathConf) fillAndCheck(name string) error {
if err != nil {
return fmt.Errorf("'%s' is not a valid url", pconf.Source)
}

if u.User != nil {
pass, _ := u.User.Password()
user := u.User.Username()
Expand All @@ -76,6 +78,7 @@ func (pconf *PathConf) fillAndCheck(name string) error {
if pconf.SourceProtocol == "" {
pconf.SourceProtocol = "udp"
}

switch pconf.SourceProtocol {
case "udp":
pconf.SourceProtocolParsed = gortsplib.StreamProtocolUDP
Expand All @@ -96,6 +99,7 @@ func (pconf *PathConf) fillAndCheck(name string) error {
if err != nil {
return fmt.Errorf("'%s' is not a valid url", pconf.Source)
}

if u.User != nil {
pass, _ := u.User.Password()
user := u.User.Username()
Expand All @@ -107,15 +111,25 @@ func (pconf *PathConf) fillAndCheck(name string) error {

} else if pconf.Source == "record" {

} else if pconf.Source == "redirect" {

} else {
return fmt.Errorf("unsupported source: '%s'", pconf.Source)
}

if pconf.SourceRedirect != "" {
_, err := url.Parse(pconf.SourceRedirect)
if err != nil {
return fmt.Errorf("'%s' is not a valid url", pconf.SourceRedirect)
}
}

if pconf.PublishUser != "" {
if !regexp.MustCompile("^[a-zA-Z0-9]+$").MatchString(pconf.PublishUser) {
return fmt.Errorf("publish username must be alphanumeric")
}
}

if pconf.PublishPass != "" {
if !regexp.MustCompile("^[a-zA-Z0-9]+$").MatchString(pconf.PublishPass) {
return fmt.Errorf("publish password must be alphanumeric")
Expand Down
92 changes: 62 additions & 30 deletions main_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -233,7 +233,7 @@ func TestPublish(t *testing.T) {

switch conf.publishSoft {
case "ffmpeg":
cnt1, err := newContainer("ffmpeg", "publish", []string{
cnt1, err := newContainer("ffmpeg", "source", []string{
"-re",
"-stream_loop", "-1",
"-i", "/emptyvideo.ts",
Expand All @@ -256,7 +256,7 @@ func TestPublish(t *testing.T) {

time.Sleep(1 * time.Second)

cnt2, err := newContainer("ffmpeg", "read", []string{
cnt2, err := newContainer("ffmpeg", "dest", []string{
"-rtsp_transport", "udp",
"-i", "rtsp://" + ownDockerIp + ":8554/teststream",
"-vframes", "1",
Expand Down Expand Up @@ -289,7 +289,7 @@ func TestRead(t *testing.T) {

time.Sleep(1 * time.Second)

cnt1, err := newContainer("ffmpeg", "publish", []string{
cnt1, err := newContainer("ffmpeg", "source", []string{
"-re",
"-stream_loop", "-1",
"-i", "/emptyvideo.ts",
Expand All @@ -305,7 +305,7 @@ func TestRead(t *testing.T) {

switch conf.readSoft {
case "ffmpeg":
cnt2, err := newContainer("ffmpeg", "read", []string{
cnt2, err := newContainer("ffmpeg", "dest", []string{
"-rtsp_transport", conf.readProto,
"-i", "rtsp://" + ownDockerIp + ":8554/teststream",
"-vframes", "1",
Expand Down Expand Up @@ -337,14 +337,13 @@ func TestRead(t *testing.T) {
}

func TestTCPOnly(t *testing.T) {
conf := "protocols: [tcp]\n"
p, err := testProgram(conf)
p, err := testProgram("protocols: [tcp]\n")
require.NoError(t, err)
defer p.close()

time.Sleep(1 * time.Second)

cnt1, err := newContainer("ffmpeg", "publish", []string{
cnt1, err := newContainer("ffmpeg", "source", []string{
"-re",
"-stream_loop", "-1",
"-i", "/emptyvideo.ts",
Expand All @@ -356,7 +355,7 @@ func TestTCPOnly(t *testing.T) {
require.NoError(t, err)
defer cnt1.close()

cnt2, err := newContainer("ffmpeg", "read", []string{
cnt2, err := newContainer("ffmpeg", "dest", []string{
"-rtsp_transport", "tcp",
"-i", "rtsp://" + ownDockerIp + ":8554/teststream",
"-vframes", "1",
Expand All @@ -377,7 +376,7 @@ func TestPathWithSlash(t *testing.T) {

time.Sleep(1 * time.Second)

cnt1, err := newContainer("ffmpeg", "publish", []string{
cnt1, err := newContainer("ffmpeg", "source", []string{
"-re",
"-stream_loop", "-1",
"-i", "/emptyvideo.ts",
Expand All @@ -389,7 +388,7 @@ func TestPathWithSlash(t *testing.T) {
require.NoError(t, err)
defer cnt1.close()

cnt2, err := newContainer("ffmpeg", "read", []string{
cnt2, err := newContainer("ffmpeg", "dest", []string{
"-rtsp_transport", "udp",
"-i", "rtsp://" + ownDockerIp + ":8554/test/stream",
"-vframes", "1",
Expand All @@ -410,7 +409,7 @@ func TestPathWithQuery(t *testing.T) {

time.Sleep(1 * time.Second)

cnt1, err := newContainer("ffmpeg", "publish", []string{
cnt1, err := newContainer("ffmpeg", "source", []string{
"-re",
"-stream_loop", "-1",
"-i", "/emptyvideo.ts",
Expand All @@ -422,7 +421,7 @@ func TestPathWithQuery(t *testing.T) {
require.NoError(t, err)
defer cnt1.close()

cnt2, err := newContainer("ffmpeg", "read", []string{
cnt2, err := newContainer("ffmpeg", "dest", []string{
"-rtsp_transport", "udp",
"-i", "rtsp://" + ownDockerIp + ":8554/test?param3=otherval",
"-vframes", "1",
Expand All @@ -438,12 +437,11 @@ func TestPathWithQuery(t *testing.T) {

func TestAuth(t *testing.T) {
t.Run("publish", func(t *testing.T) {
conf := "paths:\n" +
p, err := testProgram("paths:\n" +
" all:\n" +
" publishUser: testuser\n" +
" publishPass: testpass\n" +
" publishIps: [172.17.0.0/16]\n"
p, err := testProgram(conf)
" publishIps: [172.17.0.0/16]\n")
require.NoError(t, err)
defer p.close()

Expand Down Expand Up @@ -482,12 +480,11 @@ func TestAuth(t *testing.T) {
"vlc",
} {
t.Run("read_"+soft, func(t *testing.T) {
conf := "paths:\n" +
p, err := testProgram("paths:\n" +
" all:\n" +
" readUser: testuser\n" +
" readPass: testpass\n" +
" readIps: [172.17.0.0/16]\n"
p, err := testProgram(conf)
" readIps: [172.17.0.0/16]\n")
require.NoError(t, err)
defer p.close()

Expand Down Expand Up @@ -540,11 +537,10 @@ func TestSourceRtsp(t *testing.T) {
"tcp",
} {
t.Run(proto, func(t *testing.T) {
conf := "paths:\n" +
p1, err := testProgram("paths:\n" +
" all:\n" +
" readUser: testuser\n" +
" readPass: testpass\n"
p1, err := testProgram(conf)
" readPass: testpass\n")
require.NoError(t, err)
defer p1.close()

Expand All @@ -564,16 +560,15 @@ func TestSourceRtsp(t *testing.T) {

time.Sleep(1 * time.Second)

conf = "rtspPort: 8555\n" +
p2, err := testProgram("rtspPort: 8555\n" +
"rtpPort: 8100\n" +
"rtcpPort: 8101\n" +
"\n" +
"paths:\n" +
" proxied:\n" +
" source: rtsp://testuser:testpass@localhost:8554/teststream\n" +
" sourceProtocol: " + proto + "\n" +
" sourceOnDemand: yes\n"
p2, err := testProgram(conf)
" sourceOnDemand: yes\n")
require.NoError(t, err)
defer p2.close()

Expand Down Expand Up @@ -615,11 +610,10 @@ func TestSourceRtmp(t *testing.T) {

time.Sleep(1 * time.Second)

conf := "paths:\n" +
p, err := testProgram("paths:\n" +
" proxied:\n" +
" source: rtmp://" + cnt1.ip() + "/stream/test\n" +
" sourceOnDemand: yes\n"
p, err := testProgram(conf)
" sourceOnDemand: yes\n")
require.NoError(t, err)
defer p.close()

Expand All @@ -639,11 +633,49 @@ func TestSourceRtmp(t *testing.T) {
require.Equal(t, 0, code)
}

func TestRedirect(t *testing.T) {
p1, err := testProgram("paths:\n" +
" path1:\n" +
" source: redirect\n" +
" sourceRedirect: rtsp://" + ownDockerIp + ":8554/path2\n" +
" path2:\n")
require.NoError(t, err)
defer p1.close()

time.Sleep(1 * time.Second)

cnt1, err := newContainer("ffmpeg", "source", []string{
"-re",
"-stream_loop", "-1",
"-i", "/emptyvideo.ts",
"-c", "copy",
"-f", "rtsp",
"-rtsp_transport", "udp",
"rtsp://" + ownDockerIp + ":8554/path2",
})
require.NoError(t, err)
defer cnt1.close()

time.Sleep(1 * time.Second)

cnt2, err := newContainer("ffmpeg", "dest", []string{
"-rtsp_transport", "udp",
"-i", "rtsp://" + ownDockerIp + ":8554/path1",
"-vframes", "1",
"-f", "image2",
"-y", "/dev/null",
})
require.NoError(t, err)
defer cnt2.close()

code := cnt2.wait()
require.Equal(t, 0, code)
}

func TestRunOnDemand(t *testing.T) {
conf := "paths:\n" +
p1, err := testProgram("paths:\n" +
" all:\n" +
" runOnDemand: ffmpeg -hide_banner -loglevel error -re -i testimages/ffmpeg/emptyvideo.ts -c copy -f rtsp rtsp://localhost:8554/$RTSP_SERVER_PATH\n"
p1, err := testProgram(conf)
" runOnDemand: ffmpeg -hide_banner -loglevel error -re -i testimages/ffmpeg/emptyvideo.ts -c copy -f rtsp rtsp://localhost:8554/$RTSP_SERVER_PATH\n")
require.NoError(t, err)
defer p1.close()

Expand Down
Loading

0 comments on commit 53ba724

Please sign in to comment.