├── .dockerignore
├── .gitignore
├── .travis.yml
├── LICENSE
├── Makefile
├── README.md
├── go.mod
├── go.sum
├── main.go
├── main_test.go
├── server-client.go
├── server-tcpl.go
├── server-udpl.go
├── stream-udpl.go
├── stream.go
└── test-images
├── ffmpeg
├── Dockerfile
├── emptyvideo.ts
└── start.sh
└── rtsp-simple-server
├── Dockerfile
└── start.sh
/.dockerignore:
--------------------------------------------------------------------------------
1 | # do not add .git, since it is needed to extract the tag
2 |
3 | /release
4 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | /release
2 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: minimal
2 |
3 | services:
4 | - docker
5 |
6 | script:
7 | - make test
8 | - make release
9 |
10 | deploy:
11 | provider: releases
12 | api_key:
13 | secure: T1C0VUhf/p2Ms6reI9pNDnam5W9itCQ+IkEFlpNFrqJU5D4IXd7xty6E82ZX9pYSj9UIN7l0LwCgvyUd3NFMMyACzagoWOS0EAGruaveqCXAg5g0loFjJYCQE8DLKCaKvvWcLL8hJT+hf1OpwAO+LHmVBLbOKwotPaZOa47vCErpxnTQEGaA+VfZSVo76Ok/85rKIVA3Vq/pLQlvwJDVpDvWpiEJirMw6b1+eI6RNTLiLq/xuQpPyUyY9BoxOTJEkhJ4QXsv6+3QDUgN1vQkUMK/4SNUUDxKdFwWcSkdTW6sZ34YTeXgDbywmj9Tqkm/wJUkYg3QY9WJEpLhw5vZ2ycPbjZao9ovgL/D8iY+0IjWc87XGhOHzVGrtVSix12mpolHwLSqn9U3HS7w2jGVv+elzbHxAiksDe00O+cXDaBiZ0YQ48Ok0//VxUqoTVn1lQOUaVUrijX3OAf+Z15F953CzBUQZgdR6tApeXJq/qrSEV+rxF1FOzZRqGijE48ntxamVRkd+RdNwKLMRTtJwO8+2fZfqbBaWs+SstEmtzupyPvUJxo0YMf3MFq8QMX2O/nzgvTwW/YnSoMckMtern9YuF5ztxe9ZP17nPk9er4E7qgkvvLeFy+b5bnzmLPIzJMuftPgKuQeqNx3sCnE98Wavzhy3rQwNTLf40vLzBA=
14 | skip_cleanup: true
15 | file_glob: true
16 | file: release/*
17 | on:
18 | repo: aler9/rtsp-simple-proxy
19 | tags: true
20 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2019 aler9
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/Makefile:
--------------------------------------------------------------------------------
1 |
2 | .PHONY: $(shell ls)
3 |
4 | BASE_IMAGE = amd64/golang:1.14-alpine3.11
5 |
6 | help:
7 | @echo "usage: make [action]"
8 | @echo ""
9 | @echo "available actions:"
10 | @echo ""
11 | @echo " mod-tidy run go mod tidy"
12 | @echo " format format source files"
13 | @echo " test run available tests"
14 | @echo " run ARGS=args run app"
15 | @echo " release build release assets"
16 | @echo " travis-setup setup travis CI"
17 | @echo ""
18 |
19 | blank :=
20 | define NL
21 |
22 | $(blank)
23 | endef
24 |
25 | mod-tidy:
26 | docker run --rm -it -v $(PWD):/s $(BASE_IMAGE) \
27 | sh -c "apk add git && cd /s && GOPROXY=direct go get && GOPROXY=direct go mod tidy"
28 |
29 | format:
30 | docker run --rm -it -v $(PWD):/s $(BASE_IMAGE) \
31 | sh -c "cd /s && find . -type f -name '*.go' | xargs gofmt -l -w -s"
32 |
33 | define DOCKERFILE_TEST
34 | FROM $(BASE_IMAGE)
35 | RUN apk add --no-cache make docker-cli git
36 | WORKDIR /s
37 | COPY go.mod go.sum ./
38 | RUN go mod download
39 | COPY . ./
40 | endef
41 | export DOCKERFILE_TEST
42 |
43 | test:
44 | echo "$$DOCKERFILE_TEST" | docker build -q . -f - -t temp
45 | docker run --rm -it \
46 | -v /var/run/docker.sock:/var/run/docker.sock:ro \
47 | temp \
48 | make test-nodocker
49 |
50 | test-nodocker:
51 | $(foreach IMG,$(shell echo test-images/*/ | xargs -n1 basename), \
52 | docker build -q test-images/$(IMG) -t rtsp-simple-proxy-test-$(IMG)$(NL))
53 | $(eval export CGO_ENABLED = 0)
54 | go test -v .
55 |
56 | define DOCKERFILE_RUN
57 | FROM $(BASE_IMAGE)
58 | RUN apk add --no-cache git
59 | WORKDIR /s
60 | COPY go.mod go.sum ./
61 | RUN go mod download
62 | COPY . ./
63 | RUN GOPROXY=direct go build -o /out .
64 | endef
65 | export DOCKERFILE_RUN
66 |
67 | define CONFFILE
68 | server:
69 | protocols: [ tcp, udp ]
70 | rtspPort: 8555
71 | rtpPort: 8050
72 | rtcpPort: 8051
73 |
74 | streams:
75 | test1:
76 | url: rtsp://localhost:8554/mystream
77 |
78 | endef
79 | export CONFFILE
80 |
81 | ARGS ?= stdin
82 |
83 | run:
84 | echo "$$DOCKERFILE_RUN" | docker build -q . -f - -t temp
85 | echo "$$CONFFILE" | docker run --rm -i \
86 | --network=host \
87 | temp \
88 | /out $(ARGS)
89 |
90 | define DOCKERFILE_RELEASE
91 | FROM $(BASE_IMAGE)
92 | RUN apk add --no-cache zip make git tar
93 | WORKDIR /s
94 | COPY go.mod go.sum ./
95 | RUN go mod download
96 | COPY . ./
97 | RUN make release-nodocker
98 | endef
99 | export DOCKERFILE_RELEASE
100 |
101 | release:
102 | echo "$$DOCKERFILE_RELEASE" | docker build . -f - -t temp \
103 | && docker run --rm -it -v $(PWD):/out \
104 | temp sh -c "rm -rf /out/release && cp -r /s/release /out/"
105 |
106 | release-nodocker:
107 | $(eval VERSION := $(shell git describe --tags))
108 | $(eval GOBUILD := go build -ldflags '-X "main.Version=$(VERSION)"')
109 | rm -rf release && mkdir release
110 |
111 | CGO_ENABLED=0 GOOS=windows GOARCH=amd64 $(GOBUILD) -o /tmp/rtsp-simple-proxy.exe
112 | cd /tmp && zip -q $(PWD)/release/rtsp-simple-proxy_$(VERSION)_windows_amd64.zip rtsp-simple-proxy.exe
113 |
114 | CGO_ENABLED=0 GOOS=linux GOARCH=amd64 $(GOBUILD) -o /tmp/rtsp-simple-proxy
115 | tar -C /tmp -czf $(PWD)/release/rtsp-simple-proxy_$(VERSION)_linux_amd64.tar.gz --owner=0 --group=0 rtsp-simple-proxy
116 |
117 | CGO_ENABLED=0 GOOS=linux GOARCH=arm GOARM=6 $(GOBUILD) -o /tmp/rtsp-simple-proxy
118 | tar -C /tmp -czf $(PWD)/release/rtsp-simple-proxy_$(VERSION)_linux_arm6.tar.gz --owner=0 --group=0 rtsp-simple-proxy
119 |
120 | CGO_ENABLED=0 GOOS=linux GOARCH=arm GOARM=7 $(GOBUILD) -o /tmp/rtsp-simple-proxy
121 | tar -C /tmp -czf $(PWD)/release/rtsp-simple-proxy_$(VERSION)_linux_arm7.tar.gz --owner=0 --group=0 rtsp-simple-proxy
122 |
123 | CGO_ENABLED=0 GOOS=linux GOARCH=arm64 $(GOBUILD) -o /tmp/rtsp-simple-proxy
124 | tar -C /tmp -czf $(PWD)/release/rtsp-simple-proxy_$(VERSION)_linux_arm64.tar.gz --owner=0 --group=0 rtsp-simple-proxy
125 |
126 | CGO_ENABLED=0 GOOS=darwin GOARCH=amd64 $(GOBUILD) -o /tmp/rtsp-simple-proxy
127 | tar -C /tmp -czf $(PWD)/release/rtsp-simple-proxy_$(VERSION)_darwin_amd64.tar.gz --owner=0 --group=0 rtsp-simple-proxy
128 |
129 | define DOCKERFILE_TRAVIS
130 | FROM ruby:alpine
131 | RUN apk add --no-cache build-base git
132 | RUN gem install travis
133 | endef
134 | export DOCKERFILE_TRAVIS
135 |
136 | travis-setup:
137 | echo "$$DOCKERFILE_TRAVIS" | docker build - -t temp
138 | docker run --rm -it \
139 | -v $(PWD):/s \
140 | temp \
141 | sh -c "cd /s \
142 | && travis setup releases --force"
143 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 | # DEPRECATED - Merged into rtsp-simple-server
3 |
4 | This project has been merged into rtsp-simple-server; please look at this page for instructions on how to setup a RTSP proxy:
5 |
6 | https://github.com/aler9/rtsp-simple-server#usage-as-rtsp-proxy
7 |
8 |
9 |
10 | # rtsp-simple-proxy
11 |
12 | [](https://goreportcard.com/report/github.com/aler9/rtsp-simple-proxy)
13 | [](https://travis-ci.org/aler9/rtsp-simple-proxy)
14 |
15 | _rtsp-simple-proxy_ is a simple, ready-to-use and zero-dependency RTSP proxy, a software that receives one or more existing RTSP streams and makes them available to other users. A proxy is usually deployed in one of these scenarios:
16 | * when there are multiple users that are receiving a stream and the bandwidth is limited, so the proxy is used to receive the stream once. Users can then connect to the proxy instead of the original source.
17 | * when there's a NAT / firewall between a stream and the users, in this case the proxy is installed in the NAT and makes the stream available to the outside world.
18 |
19 | Features:
20 | * Receive multiple streams in TCP or UDP
21 | * Distribute streams in TCP or UDP
22 | * Supports the RTP/RTCP streaming protocol
23 | * Supports authentication (i.e. username and password)
24 | * Compatible with Linux, Windows and Mac, does not require any dependency or interpreter, it's a single executable
25 |
26 | ## Installation
27 |
28 | Precompiled binaries are available in the [release](https://github.com/aler9/rtsp-simple-proxy/releases) page. Just download and extract the executable.
29 |
30 | ## Usage
31 |
32 | #### Basic usage
33 |
34 | 1. Create a configuration file named `conf.yml`, placed in the same folder of the executable, with the following content:
35 | ```yaml
36 | streams:
37 | # name of the stream
38 | mypath:
39 | # url of the source stream, in the format rtsp://user:pass@host:port/path
40 | url: rtsp://myhost:8554/mystream
41 | ```
42 |
43 | 2. Launch the proxy:
44 | ```
45 | ./rtsp-simple-proxy
46 | ```
47 |
48 | 3. Open any stream you have defined in the configuration file, by using the stream name as path, for instance with VLC:
49 | ```
50 | vlc rtsp://localhost:8554/mypath
51 | ```
52 |
53 | #### Full configuration file
54 |
55 | ```yaml
56 | # timeout of read operations
57 | readTimeout: 5s
58 | # timeout of write operations
59 | writeTimeout: 5s
60 |
61 | server:
62 | # supported protocols
63 | protocols: [ tcp, udp ]
64 | # port of the RTSP TCP listener
65 | rtspPort: 8554
66 | # port of the RTP UDP listener
67 | rtpPort: 8050
68 | # port of the RTCP UDP listener
69 | rtcpPort: 8051
70 | # optional username required to read
71 | readUser:
72 | # optional password required to read
73 | readPass:
74 |
75 | streams:
76 | # name of the stream
77 | test1:
78 | # url of the source stream, in the format rtsp://user:pass@host:port/path
79 | url: rtsp://myhost:8554/mystream
80 | # whether to use tcp or udp
81 | protocol: udp
82 | ```
83 |
84 | #### Full command-line usage
85 |
86 | ```
87 | usage: rtsp-simple-proxy []
88 |
89 | rtsp-simple-proxy v0.0.0
90 |
91 | RTSP proxy.
92 |
93 | Flags:
94 | --help Show context-sensitive help (also try --help-long and --help-man).
95 | --version print version
96 |
97 | Args:
98 | [] path of a config file. The default is conf.yml. Use 'stdin' to
99 | read config from stdin
100 | ```
101 |
102 | ## Links
103 |
104 | Related projects
105 | * https://github.com/aler9/rtsp-simple-server
106 | * https://github.com/aler9/gortsplib
107 |
108 | IETF Standards
109 | * RTSP 1.0 https://tools.ietf.org/html/rfc2326
110 | * RTSP 2.0 https://tools.ietf.org/html/rfc7826
111 | * HTTP 1.1 https://tools.ietf.org/html/rfc2616
112 |
--------------------------------------------------------------------------------
/go.mod:
--------------------------------------------------------------------------------
1 | module rtsp-simple-proxy
2 |
3 | go 1.13
4 |
5 | require (
6 | github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751 // indirect
7 | github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d // indirect
8 | github.com/aler9/gortsplib v0.0.0-20200627203653-496c25a09c9f
9 | github.com/stretchr/testify v1.4.0
10 | gopkg.in/alecthomas/kingpin.v2 v2.2.6
11 | gopkg.in/yaml.v2 v2.2.2
12 | gortc.io/sdp v0.18.2
13 | )
14 |
--------------------------------------------------------------------------------
/go.sum:
--------------------------------------------------------------------------------
1 | github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751 h1:JYp7IbQjafoB+tBA3gMyHYHrpOtNuDiK/uB5uXxq5wM=
2 | github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
3 | github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d h1:UQZhZ2O0vMHr2cI+DC1Mbh0TJxzA3RcLoMsFw+aXw7E=
4 | github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho=
5 | github.com/aler9/gortsplib v0.0.0-20200627203653-496c25a09c9f h1:Gcp2e8Voy1e1XX84kkf+FLu5chrzS1Xqmg7yShY5HKQ=
6 | github.com/aler9/gortsplib v0.0.0-20200627203653-496c25a09c9f/go.mod h1:sL64nUkmrTVhlT/GCaxRXyI2Xk7m8XSdw5Uv8xKGPdc=
7 | github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
8 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
9 | github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
10 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
11 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
12 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
13 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
14 | github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk=
15 | github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
16 | gopkg.in/alecthomas/kingpin.v2 v2.2.6 h1:jMFz6MfLP0/4fUyZle81rXUoxOBFi19VUFKVDOQfozc=
17 | gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
18 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
19 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
20 | gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw=
21 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
22 | gortc.io/sdp v0.18.2 h1:w2L1h9rgMZwGQz/bYMxTQ36POQ114ETpYUX1pFfSsLg=
23 | gortc.io/sdp v0.18.2/go.mod h1:Oj8tpRIx+Zx6lyrQR9+HHegByfbaz92A9wPSWrQhTI4=
24 |
--------------------------------------------------------------------------------
/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "fmt"
5 | "log"
6 | "os"
7 | "time"
8 |
9 | "gopkg.in/alecthomas/kingpin.v2"
10 | "gopkg.in/yaml.v2"
11 | )
12 |
13 | var Version = "v0.0.0"
14 |
15 | type trackFlow int
16 |
17 | const (
18 | _TRACK_FLOW_RTP trackFlow = iota
19 | _TRACK_FLOW_RTCP
20 | )
21 |
22 | type track struct {
23 | rtpPort int
24 | rtcpPort int
25 | }
26 |
27 | type streamProtocol int
28 |
29 | const (
30 | _STREAM_PROTOCOL_UDP streamProtocol = iota
31 | _STREAM_PROTOCOL_TCP
32 | )
33 |
34 | func (s streamProtocol) String() string {
35 | if s == _STREAM_PROTOCOL_UDP {
36 | return "udp"
37 | }
38 | return "tcp"
39 | }
40 |
41 | type streamConf struct {
42 | Url string `yaml:"url"`
43 | Protocol string `yaml:"protocol"`
44 | }
45 |
46 | type conf struct {
47 | ReadTimeout string `yaml:"readTimeout"`
48 | WriteTimeout string `yaml:"writeTimeout"`
49 | Server struct {
50 | Protocols []string `yaml:"protocols"`
51 | RtspPort int `yaml:"rtspPort"`
52 | RtpPort int `yaml:"rtpPort"`
53 | RtcpPort int `yaml:"rtcpPort"`
54 | ReadUser string `yaml:"readUser"`
55 | ReadPass string `yaml:"readPass"`
56 | } `yaml:"server"`
57 | Streams map[string]streamConf `yaml:"streams"`
58 | }
59 |
60 | func loadConf(confPath string) (*conf, error) {
61 | if confPath == "stdin" {
62 | var ret conf
63 | err := yaml.NewDecoder(os.Stdin).Decode(&ret)
64 | if err != nil {
65 | return nil, err
66 | }
67 |
68 | return &ret, nil
69 |
70 | } else {
71 | f, err := os.Open(confPath)
72 | if err != nil {
73 | return nil, err
74 | }
75 | defer f.Close()
76 |
77 | var ret conf
78 | err = yaml.NewDecoder(f).Decode(&ret)
79 | if err != nil {
80 | return nil, err
81 | }
82 |
83 | return &ret, nil
84 | }
85 | }
86 |
87 | type args struct {
88 | version bool
89 | confPath string
90 | }
91 |
92 | type program struct {
93 | conf conf
94 | readTimeout time.Duration
95 | writeTimeout time.Duration
96 | protocols map[streamProtocol]struct{}
97 | streams map[string]*stream
98 | tcpl *serverTcpListener
99 | udplRtp *serverUdpListener
100 | udplRtcp *serverUdpListener
101 | }
102 |
103 | func newProgram(sargs []string) (*program, error) {
104 | kingpin.CommandLine.Help = "rtsp-simple-proxy " + Version + "\n\n" +
105 | "RTSP proxy."
106 |
107 | argVersion := kingpin.Flag("version", "print version").Bool()
108 | argConfPath := kingpin.Arg("confpath", "path of a config file. The default is conf.yml. Use 'stdin' to read config from stdin").Default("conf.yml").String()
109 |
110 | kingpin.MustParse(kingpin.CommandLine.Parse(sargs))
111 |
112 | args := args{
113 | version: *argVersion,
114 | confPath: *argConfPath,
115 | }
116 |
117 | if args.version == true {
118 | fmt.Println(Version)
119 | os.Exit(0)
120 | }
121 |
122 | conf, err := loadConf(args.confPath)
123 | if err != nil {
124 | return nil, err
125 | }
126 | if err != nil {
127 | return nil, err
128 | }
129 |
130 | if conf.ReadTimeout == "" {
131 | conf.ReadTimeout = "5s"
132 | }
133 | if conf.WriteTimeout == "" {
134 | conf.WriteTimeout = "5s"
135 | }
136 | if len(conf.Server.Protocols) == 0 {
137 | conf.Server.Protocols = []string{"tcp", "udp"}
138 | }
139 | if conf.Server.RtspPort == 0 {
140 | conf.Server.RtspPort = 8554
141 | }
142 | if conf.Server.RtpPort == 0 {
143 | conf.Server.RtpPort = 8050
144 | }
145 | if conf.Server.RtcpPort == 0 {
146 | conf.Server.RtcpPort = 8051
147 | }
148 |
149 | readTimeout, err := time.ParseDuration(conf.ReadTimeout)
150 | if err != nil {
151 | return nil, fmt.Errorf("unable to parse read timeout: %s", err)
152 | }
153 | writeTimeout, err := time.ParseDuration(conf.WriteTimeout)
154 | if err != nil {
155 | return nil, fmt.Errorf("unable to parse write timeout: %s", err)
156 | }
157 | protocols := make(map[streamProtocol]struct{})
158 | for _, proto := range conf.Server.Protocols {
159 | switch proto {
160 | case "udp":
161 | protocols[_STREAM_PROTOCOL_UDP] = struct{}{}
162 |
163 | case "tcp":
164 | protocols[_STREAM_PROTOCOL_TCP] = struct{}{}
165 |
166 | default:
167 | return nil, fmt.Errorf("unsupported protocol: '%v'", proto)
168 | }
169 | }
170 | if len(protocols) == 0 {
171 | return nil, fmt.Errorf("no protocols provided")
172 | }
173 | if (conf.Server.RtpPort % 2) != 0 {
174 | return nil, fmt.Errorf("rtp port must be even")
175 | }
176 | if conf.Server.RtcpPort != (conf.Server.RtpPort + 1) {
177 | return nil, fmt.Errorf("rtcp port must be rtp port plus 1")
178 | }
179 | if len(conf.Streams) == 0 {
180 | return nil, fmt.Errorf("no streams provided")
181 | }
182 |
183 | log.Printf("rtsp-simple-proxy %s", Version)
184 |
185 | p := &program{
186 | conf: *conf,
187 | readTimeout: readTimeout,
188 | writeTimeout: writeTimeout,
189 | protocols: protocols,
190 | streams: make(map[string]*stream),
191 | }
192 |
193 | for path, val := range p.conf.Streams {
194 | var err error
195 | p.streams[path], err = newStream(p, path, val)
196 | if err != nil {
197 | return nil, fmt.Errorf("error in stream '%s': %s", path, err)
198 | }
199 | }
200 |
201 | p.udplRtp, err = newServerUdpListener(p, p.conf.Server.RtpPort, _TRACK_FLOW_RTP)
202 | if err != nil {
203 | return nil, err
204 | }
205 |
206 | p.udplRtcp, err = newServerUdpListener(p, p.conf.Server.RtcpPort, _TRACK_FLOW_RTCP)
207 | if err != nil {
208 | return nil, err
209 | }
210 |
211 | p.tcpl, err = newServerTcpListener(p)
212 | if err != nil {
213 | return nil, err
214 | }
215 |
216 | for _, s := range p.streams {
217 | go s.run()
218 | }
219 |
220 | go p.udplRtp.run()
221 | go p.udplRtcp.run()
222 | go p.tcpl.run()
223 |
224 | return p, nil
225 | }
226 |
227 | func (p *program) close() {
228 | for _, s := range p.streams {
229 | s.close()
230 | }
231 |
232 | p.tcpl.close()
233 | p.udplRtcp.close()
234 | p.udplRtp.close()
235 | }
236 |
237 | func main() {
238 | _, err := newProgram(os.Args[1:])
239 | if err != nil {
240 | log.Fatal("ERR: ", err)
241 | }
242 |
243 | select {}
244 | }
245 |
--------------------------------------------------------------------------------
/main_test.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "bytes"
5 | "io/ioutil"
6 | "net"
7 | "os"
8 | "os/exec"
9 | "testing"
10 | "time"
11 |
12 | "github.com/stretchr/testify/require"
13 | )
14 |
15 | var ownDockerIp = func() string {
16 | out, err := exec.Command("docker", "network", "inspect", "bridge",
17 | "-f", "{{range .IPAM.Config}}{{.Subnet}}{{end}}").Output()
18 | if err != nil {
19 | panic(err)
20 | }
21 |
22 | _, ipnet, err := net.ParseCIDR(string(out[:len(out)-1]))
23 | if err != nil {
24 | panic(err)
25 | }
26 |
27 | ifaces, err := net.Interfaces()
28 | if err != nil {
29 | panic(err)
30 | }
31 |
32 | for _, i := range ifaces {
33 | addrs, err := i.Addrs()
34 | if err != nil {
35 | continue
36 | }
37 |
38 | for _, addr := range addrs {
39 | if v, ok := addr.(*net.IPNet); ok {
40 | if ipnet.Contains(v.IP) {
41 | return v.IP.String()
42 | }
43 | }
44 | }
45 | }
46 |
47 | panic("IP not found")
48 | }()
49 |
50 | type container struct {
51 | name string
52 | stdout *bytes.Buffer
53 | }
54 |
55 | func newContainer(image string, name string, args []string) (*container, error) {
56 | c := &container{
57 | name: name,
58 | stdout: bytes.NewBuffer(nil),
59 | }
60 |
61 | exec.Command("docker", "kill", "rtsp-simple-proxy-test-"+name).Run()
62 | exec.Command("docker", "wait", "rtsp-simple-proxy-test-"+name).Run()
63 |
64 | cmd := []string{"docker", "run", "--name=rtsp-simple-proxy-test-" + name,
65 | "rtsp-simple-proxy-test-" + image}
66 | cmd = append(cmd, args...)
67 | ecmd := exec.Command(cmd[0], cmd[1:]...)
68 |
69 | ecmd.Stdout = c.stdout
70 | ecmd.Stderr = os.Stderr
71 |
72 | err := ecmd.Start()
73 | if err != nil {
74 | return nil, err
75 | }
76 |
77 | time.Sleep(1 * time.Second)
78 |
79 | return c, nil
80 | }
81 |
82 | func (c *container) close() {
83 | exec.Command("docker", "kill", "rtsp-simple-proxy-test-"+c.name).Run()
84 | exec.Command("docker", "wait", "rtsp-simple-proxy-test-"+c.name).Run()
85 | exec.Command("docker", "rm", "rtsp-simple-proxy-test-"+c.name).Run()
86 | }
87 |
88 | func (c *container) wait() {
89 | exec.Command("docker", "wait", "rtsp-simple-proxy-test-"+c.name).Run()
90 | }
91 |
92 | func (c *container) ip() string {
93 | byts, _ := exec.Command("docker", "inspect", "-f",
94 | "{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}",
95 | "rtsp-simple-proxy-test-"+c.name).Output()
96 | return string(byts[:len(byts)-1])
97 | }
98 |
99 | func TestProtocols(t *testing.T) {
100 | for _, pair := range [][2]string{
101 | {"udp", "udp"},
102 | {"udp", "tcp"},
103 | {"tcp", "udp"},
104 | {"tcp", "tcp"},
105 | } {
106 | t.Run(pair[0]+"_"+pair[1], func(t *testing.T) {
107 | cnt1, err := newContainer("rtsp-simple-server", "server", []string{})
108 | require.NoError(t, err)
109 | defer cnt1.close()
110 |
111 | time.Sleep(1 * time.Second)
112 |
113 | cnt2, err := newContainer("ffmpeg", "source", []string{
114 | "-hide_banner",
115 | "-loglevel", "panic",
116 | "-re",
117 | "-stream_loop", "-1",
118 | "-i", "/emptyvideo.ts",
119 | "-c", "copy",
120 | "-f", "rtsp",
121 | "-rtsp_transport", "udp",
122 | "rtsp://" + cnt1.ip() + ":8554/teststream",
123 | })
124 | require.NoError(t, err)
125 | defer cnt2.close()
126 |
127 | time.Sleep(1 * time.Second)
128 |
129 | ioutil.WriteFile("conf.yml", []byte("\n"+
130 | "server:\n"+
131 | " protocols: [ "+pair[1]+" ]\n"+
132 | " rtspPort: 8555\n"+
133 | "\n"+
134 | "streams:\n"+
135 | " testproxy:\n"+
136 | " url: rtsp://"+cnt1.ip()+":8554/teststream\n"+
137 | " protocol: "+pair[0]+"\n"),
138 | 0644)
139 |
140 | p, err := newProgram([]string{})
141 | require.NoError(t, err)
142 | defer p.close()
143 |
144 | time.Sleep(1 * time.Second)
145 |
146 | cnt3, err := newContainer("ffmpeg", "dest", []string{
147 | "-hide_banner",
148 | "-loglevel", "panic",
149 | "-rtsp_transport", pair[1],
150 | "-i", "rtsp://" + ownDockerIp + ":8555/testproxy",
151 | "-vframes", "1",
152 | "-f", "image2",
153 | "-y", "/dev/null",
154 | })
155 | require.NoError(t, err)
156 | defer cnt3.close()
157 |
158 | cnt3.wait()
159 |
160 | require.Equal(t, "all right\n", string(cnt3.stdout.Bytes()))
161 | })
162 | }
163 | }
164 |
165 | func TestStreamAuth(t *testing.T) {
166 | cnt1, err := newContainer("rtsp-simple-server", "server", []string{
167 | "--read-user=testuser",
168 | "--read-pass=testpass",
169 | })
170 | require.NoError(t, err)
171 | defer cnt1.close()
172 |
173 | time.Sleep(1 * time.Second)
174 |
175 | cnt2, err := newContainer("ffmpeg", "source", []string{
176 | "-hide_banner",
177 | "-loglevel", "panic",
178 | "-re",
179 | "-stream_loop", "-1",
180 | "-i", "/emptyvideo.ts",
181 | "-c", "copy",
182 | "-f", "rtsp",
183 | "-rtsp_transport", "udp",
184 | "rtsp://" + cnt1.ip() + ":8554/teststream",
185 | })
186 | require.NoError(t, err)
187 | defer cnt2.close()
188 |
189 | time.Sleep(1 * time.Second)
190 |
191 | ioutil.WriteFile("conf.yml", []byte("\n"+
192 | "server:\n"+
193 | " protocols: [ udp ]\n"+
194 | " rtspPort: 8555\n"+
195 | "\n"+
196 | "streams:\n"+
197 | " testproxy:\n"+
198 | " url: rtsp://testuser:testpass@"+cnt1.ip()+":8554/teststream\n"+
199 | " protocol: udp\n"),
200 | 0644)
201 |
202 | p, err := newProgram([]string{})
203 | require.NoError(t, err)
204 | defer p.close()
205 |
206 | time.Sleep(1 * time.Second)
207 |
208 | cnt3, err := newContainer("ffmpeg", "dest", []string{
209 | "-hide_banner",
210 | "-loglevel", "panic",
211 | "-rtsp_transport", "udp",
212 | "-i", "rtsp://" + ownDockerIp + ":8555/testproxy",
213 | "-vframes", "1",
214 | "-f", "image2",
215 | "-y", "/dev/null",
216 | })
217 | require.NoError(t, err)
218 | defer cnt3.close()
219 |
220 | cnt3.wait()
221 |
222 | require.Equal(t, "all right\n", string(cnt3.stdout.Bytes()))
223 | }
224 |
225 | func TestServerAuth(t *testing.T) {
226 | cnt1, err := newContainer("rtsp-simple-server", "server", []string{})
227 | require.NoError(t, err)
228 | defer cnt1.close()
229 |
230 | time.Sleep(1 * time.Second)
231 |
232 | cnt2, err := newContainer("ffmpeg", "source", []string{
233 | "-hide_banner",
234 | "-loglevel", "panic",
235 | "-re",
236 | "-stream_loop", "-1",
237 | "-i", "/emptyvideo.ts",
238 | "-c", "copy",
239 | "-f", "rtsp",
240 | "-rtsp_transport", "udp",
241 | "rtsp://" + cnt1.ip() + ":8554/teststream",
242 | })
243 | require.NoError(t, err)
244 | defer cnt2.close()
245 |
246 | time.Sleep(1 * time.Second)
247 |
248 | ioutil.WriteFile("conf.yml", []byte("\n"+
249 | "server:\n"+
250 | " protocols: [ udp ]\n"+
251 | " rtspPort: 8555\n"+
252 | " readUser: testuser\n"+
253 | " readPass: testpass\n"+
254 | "\n"+
255 | "streams:\n"+
256 | " testproxy:\n"+
257 | " url: rtsp://"+cnt1.ip()+":8554/teststream\n"+
258 | " protocol: udp\n"),
259 | 0644)
260 |
261 | p, err := newProgram([]string{})
262 | require.NoError(t, err)
263 | defer p.close()
264 |
265 | time.Sleep(1 * time.Second)
266 |
267 | cnt3, err := newContainer("ffmpeg", "dest", []string{
268 | "-hide_banner",
269 | "-loglevel", "panic",
270 | "-rtsp_transport", "udp",
271 | "-i", "rtsp://testuser:testpass@" + ownDockerIp + ":8555/testproxy",
272 | "-vframes", "1",
273 | "-f", "image2",
274 | "-y", "/dev/null",
275 | })
276 | require.NoError(t, err)
277 | defer cnt3.close()
278 |
279 | cnt3.wait()
280 |
281 | require.Equal(t, "all right\n", string(cnt3.stdout.Bytes()))
282 | }
283 |
--------------------------------------------------------------------------------
/server-client.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "errors"
5 | "fmt"
6 | "io"
7 | "log"
8 | "net"
9 | "strings"
10 |
11 | "github.com/aler9/gortsplib"
12 | )
13 |
14 | func interleavedChannelToTrack(channel uint8) (int, trackFlow) {
15 | if (channel % 2) == 0 {
16 | return int(channel / 2), _TRACK_FLOW_RTP
17 | }
18 | return int((channel - 1) / 2), _TRACK_FLOW_RTCP
19 | }
20 |
21 | func trackToInterleavedChannel(id int, flow trackFlow) uint8 {
22 | if flow == _TRACK_FLOW_RTP {
23 | return uint8(id * 2)
24 | }
25 | return uint8((id * 2) + 1)
26 | }
27 |
28 | type clientState int
29 |
30 | const (
31 | _CLIENT_STATE_STARTING clientState = iota
32 | _CLIENT_STATE_PRE_PLAY
33 | _CLIENT_STATE_PLAY
34 | )
35 |
36 | func (cs clientState) String() string {
37 | switch cs {
38 | case _CLIENT_STATE_STARTING:
39 | return "STARTING"
40 |
41 | case _CLIENT_STATE_PRE_PLAY:
42 | return "PRE_PLAY"
43 |
44 | case _CLIENT_STATE_PLAY:
45 | return "PLAY"
46 | }
47 | return "UNKNOWN"
48 | }
49 |
50 | type serverClient struct {
51 | p *program
52 | conn *gortsplib.ConnServer
53 | state clientState
54 | path string
55 | readAuth *gortsplib.AuthServer
56 | streamProtocol streamProtocol
57 | streamTracks []*track
58 | write chan *gortsplib.InterleavedFrame
59 | done chan struct{}
60 | }
61 |
62 | func newServerClient(p *program, nconn net.Conn) *serverClient {
63 | c := &serverClient{
64 | p: p,
65 | conn: gortsplib.NewConnServer(gortsplib.ConnServerConf{
66 | NConn: nconn,
67 | ReadTimeout: p.readTimeout,
68 | WriteTimeout: p.writeTimeout,
69 | }),
70 | state: _CLIENT_STATE_STARTING,
71 | write: make(chan *gortsplib.InterleavedFrame),
72 | done: make(chan struct{}),
73 | }
74 |
75 | c.p.tcpl.mutex.Lock()
76 | c.p.tcpl.clients[c] = struct{}{}
77 | c.p.tcpl.mutex.Unlock()
78 |
79 | go c.run()
80 |
81 | return c
82 | }
83 |
84 | func (c *serverClient) close() error {
85 | // already deleted
86 | if _, ok := c.p.tcpl.clients[c]; !ok {
87 | return nil
88 | }
89 |
90 | delete(c.p.tcpl.clients, c)
91 | c.conn.NetConn().Close()
92 | close(c.write)
93 |
94 | return nil
95 | }
96 |
97 | func (c *serverClient) log(format string, args ...interface{}) {
98 | // keep remote address outside format, since it can contain %
99 | log.Println("[RTSP client " + c.conn.NetConn().RemoteAddr().String() + "] " +
100 | fmt.Sprintf(format, args...))
101 | }
102 |
103 | func (c *serverClient) ip() net.IP {
104 | return c.conn.NetConn().RemoteAddr().(*net.TCPAddr).IP
105 | }
106 |
107 | func (c *serverClient) zone() string {
108 | return c.conn.NetConn().RemoteAddr().(*net.TCPAddr).Zone
109 | }
110 |
111 | func (c *serverClient) run() {
112 | c.log("connected")
113 |
114 | for {
115 | req, err := c.conn.ReadRequest()
116 | if err != nil {
117 | if err != io.EOF {
118 | c.log("ERR: %s", err)
119 | }
120 | break
121 | }
122 |
123 | ok := c.handleRequest(req)
124 | if !ok {
125 | break
126 | }
127 | }
128 |
129 | func() {
130 | c.p.tcpl.mutex.Lock()
131 | defer c.p.tcpl.mutex.Unlock()
132 | c.close()
133 | }()
134 |
135 | c.log("disconnected")
136 |
137 | close(c.done)
138 | }
139 |
140 | func (c *serverClient) writeResError(req *gortsplib.Request, code gortsplib.StatusCode, err error) {
141 | c.log("ERR: %s", err)
142 |
143 | header := gortsplib.Header{}
144 | if cseq, ok := req.Header["CSeq"]; ok && len(cseq) == 1 {
145 | header["CSeq"] = []string{cseq[0]}
146 | }
147 |
148 | c.conn.WriteResponse(&gortsplib.Response{
149 | StatusCode: code,
150 | Header: header,
151 | })
152 | }
153 |
154 | var errAuthCritical = errors.New("auth critical")
155 | var errAuthNotCritical = errors.New("auth not critical")
156 |
157 | func (c *serverClient) validateAuth(req *gortsplib.Request, user string, pass string, auth **gortsplib.AuthServer) error {
158 | if user == "" {
159 | return nil
160 | }
161 |
162 | initialRequest := false
163 | if *auth == nil {
164 | initialRequest = true
165 | *auth = gortsplib.NewAuthServer(user, pass, nil)
166 | }
167 |
168 | err := (*auth).ValidateHeader(req.Header["Authorization"], req.Method, req.Url)
169 | if err != nil {
170 | if !initialRequest {
171 | c.log("ERR: Unauthorized: %s", err)
172 | }
173 |
174 | c.conn.WriteResponse(&gortsplib.Response{
175 | StatusCode: gortsplib.StatusUnauthorized,
176 | Header: gortsplib.Header{
177 | "CSeq": []string{req.Header["CSeq"][0]},
178 | "WWW-Authenticate": (*auth).GenerateHeader(),
179 | },
180 | })
181 |
182 | if !initialRequest {
183 | return errAuthCritical
184 | }
185 |
186 | return errAuthNotCritical
187 | }
188 |
189 | return nil
190 | }
191 |
192 | func (c *serverClient) handleRequest(req *gortsplib.Request) bool {
193 | c.log(string(req.Method))
194 |
195 | cseq, ok := req.Header["CSeq"]
196 | if !ok || len(cseq) != 1 {
197 | c.writeResError(req, gortsplib.StatusBadRequest, fmt.Errorf("cseq missing"))
198 | return false
199 | }
200 |
201 | path := func() string {
202 | ret := req.Url.Path
203 |
204 | // remove leading slash
205 | if len(ret) > 1 {
206 | ret = ret[1:]
207 | }
208 |
209 | // strip any subpath
210 | if n := strings.Index(ret, "/"); n >= 0 {
211 | ret = ret[:n]
212 | }
213 |
214 | return ret
215 | }()
216 |
217 | switch req.Method {
218 | case gortsplib.OPTIONS:
219 | // do not check state, since OPTIONS can be requested
220 | // in any state
221 |
222 | c.conn.WriteResponse(&gortsplib.Response{
223 | StatusCode: gortsplib.StatusOK,
224 | Header: gortsplib.Header{
225 | "CSeq": []string{cseq[0]},
226 | "Public": []string{strings.Join([]string{
227 | string(gortsplib.DESCRIBE),
228 | string(gortsplib.SETUP),
229 | string(gortsplib.PLAY),
230 | string(gortsplib.PAUSE),
231 | string(gortsplib.TEARDOWN),
232 | }, ", ")},
233 | },
234 | })
235 | return true
236 |
237 | case gortsplib.DESCRIBE:
238 | if c.state != _CLIENT_STATE_STARTING {
239 | c.writeResError(req, gortsplib.StatusBadRequest,
240 | fmt.Errorf("client is in state '%s' instead of '%s'", c.state, _CLIENT_STATE_STARTING))
241 | return false
242 | }
243 |
244 | err := c.validateAuth(req, c.p.conf.Server.ReadUser, c.p.conf.Server.ReadPass, &c.readAuth)
245 | if err != nil {
246 | if err == errAuthCritical {
247 | return false
248 | }
249 | return true
250 | }
251 |
252 | sdp, err := func() ([]byte, error) {
253 | c.p.tcpl.mutex.RLock()
254 | defer c.p.tcpl.mutex.RUnlock()
255 |
256 | str, ok := c.p.streams[path]
257 | if !ok {
258 | return nil, fmt.Errorf("there is no stream on path '%s'", path)
259 | }
260 |
261 | if str.state != _STREAM_STATE_READY {
262 | return nil, fmt.Errorf("stream '%s' is not ready yet", path)
263 | }
264 |
265 | return str.serverSdpText, nil
266 | }()
267 | if err != nil {
268 | c.writeResError(req, gortsplib.StatusBadRequest, err)
269 | return false
270 | }
271 |
272 | c.conn.WriteResponse(&gortsplib.Response{
273 | StatusCode: gortsplib.StatusOK,
274 | Header: gortsplib.Header{
275 | "CSeq": []string{cseq[0]},
276 | "Content-Base": []string{req.Url.String() + "/"},
277 | "Content-Type": []string{"application/sdp"},
278 | },
279 | Content: sdp,
280 | })
281 | return true
282 |
283 | case gortsplib.SETUP:
284 | tsRaw, ok := req.Header["Transport"]
285 | if !ok || len(tsRaw) != 1 {
286 | c.writeResError(req, gortsplib.StatusBadRequest, fmt.Errorf("transport header missing"))
287 | return false
288 | }
289 |
290 | th := gortsplib.ReadHeaderTransport(tsRaw[0])
291 | if _, ok := th["multicast"]; ok {
292 | c.writeResError(req, gortsplib.StatusBadRequest, fmt.Errorf("multicast is not supported"))
293 | return false
294 | }
295 |
296 | switch c.state {
297 | // play
298 | case _CLIENT_STATE_STARTING, _CLIENT_STATE_PRE_PLAY:
299 | err := c.validateAuth(req, c.p.conf.Server.ReadUser, c.p.conf.Server.ReadPass, &c.readAuth)
300 | if err != nil {
301 | if err == errAuthCritical {
302 | return false
303 | }
304 | return true
305 | }
306 |
307 | // play via UDP
308 | if func() bool {
309 | _, ok := th["RTP/AVP"]
310 | if ok {
311 | return true
312 | }
313 | _, ok = th["RTP/AVP/UDP"]
314 | if ok {
315 | return true
316 | }
317 | return false
318 | }() {
319 | if _, ok := c.p.protocols[_STREAM_PROTOCOL_UDP]; !ok {
320 | c.writeResError(req, gortsplib.StatusUnsupportedTransport, fmt.Errorf("UDP streaming is disabled"))
321 | return false
322 | }
323 |
324 | rtpPort, rtcpPort := th.GetPorts("client_port")
325 | if rtpPort == 0 || rtcpPort == 0 {
326 | c.writeResError(req, gortsplib.StatusBadRequest, fmt.Errorf("transport header does not have valid client ports (%s)", tsRaw[0]))
327 | return false
328 | }
329 |
330 | if c.path != "" && path != c.path {
331 | c.writeResError(req, gortsplib.StatusBadRequest, fmt.Errorf("path has changed"))
332 | return false
333 | }
334 |
335 | err := func() error {
336 | c.p.tcpl.mutex.Lock()
337 | defer c.p.tcpl.mutex.Unlock()
338 |
339 | str, ok := c.p.streams[path]
340 | if !ok {
341 | return fmt.Errorf("there is no stream on path '%s'", path)
342 | }
343 |
344 | if str.state != _STREAM_STATE_READY {
345 | return fmt.Errorf("stream '%s' is not ready yet", path)
346 | }
347 |
348 | if len(c.streamTracks) > 0 && c.streamProtocol != _STREAM_PROTOCOL_UDP {
349 | return fmt.Errorf("client want to send tracks with different protocols")
350 | }
351 |
352 | if len(c.streamTracks) >= len(str.serverSdpParsed.Medias) {
353 | return fmt.Errorf("all the tracks have already been setup")
354 | }
355 |
356 | c.path = path
357 | c.streamProtocol = _STREAM_PROTOCOL_UDP
358 | c.streamTracks = append(c.streamTracks, &track{
359 | rtpPort: rtpPort,
360 | rtcpPort: rtcpPort,
361 | })
362 |
363 | c.state = _CLIENT_STATE_PRE_PLAY
364 | return nil
365 | }()
366 | if err != nil {
367 | c.writeResError(req, gortsplib.StatusBadRequest, err)
368 | return false
369 | }
370 |
371 | c.conn.WriteResponse(&gortsplib.Response{
372 | StatusCode: gortsplib.StatusOK,
373 | Header: gortsplib.Header{
374 | "CSeq": []string{cseq[0]},
375 | "Transport": []string{strings.Join([]string{
376 | "RTP/AVP/UDP",
377 | "unicast",
378 | fmt.Sprintf("client_port=%d-%d", rtpPort, rtcpPort),
379 | fmt.Sprintf("server_port=%d-%d", c.p.conf.Server.RtpPort, c.p.conf.Server.RtcpPort),
380 | }, ";")},
381 | "Session": []string{"12345678"},
382 | },
383 | })
384 | return true
385 |
386 | // play via TCP
387 | } else if _, ok := th["RTP/AVP/TCP"]; ok {
388 | if _, ok := c.p.protocols[_STREAM_PROTOCOL_TCP]; !ok {
389 | c.writeResError(req, gortsplib.StatusUnsupportedTransport, fmt.Errorf("TCP streaming is disabled"))
390 | return false
391 | }
392 |
393 | if c.path != "" && path != c.path {
394 | c.writeResError(req, gortsplib.StatusBadRequest, fmt.Errorf("path has changed"))
395 | return false
396 | }
397 |
398 | err := func() error {
399 | c.p.tcpl.mutex.Lock()
400 | defer c.p.tcpl.mutex.Unlock()
401 |
402 | str, ok := c.p.streams[path]
403 | if !ok {
404 | return fmt.Errorf("there is no stream on path '%s'", path)
405 | }
406 |
407 | if len(c.streamTracks) > 0 && c.streamProtocol != _STREAM_PROTOCOL_TCP {
408 | return fmt.Errorf("client want to send tracks with different protocols")
409 | }
410 |
411 | if len(c.streamTracks) >= len(str.serverSdpParsed.Medias) {
412 | return fmt.Errorf("all the tracks have already been setup")
413 | }
414 |
415 | c.path = path
416 | c.streamProtocol = _STREAM_PROTOCOL_TCP
417 | c.streamTracks = append(c.streamTracks, &track{
418 | rtpPort: 0,
419 | rtcpPort: 0,
420 | })
421 |
422 | c.state = _CLIENT_STATE_PRE_PLAY
423 | return nil
424 | }()
425 | if err != nil {
426 | c.writeResError(req, gortsplib.StatusBadRequest, err)
427 | return false
428 | }
429 |
430 | interleaved := fmt.Sprintf("%d-%d", ((len(c.streamTracks) - 1) * 2), ((len(c.streamTracks)-1)*2)+1)
431 |
432 | c.conn.WriteResponse(&gortsplib.Response{
433 | StatusCode: gortsplib.StatusOK,
434 | Header: gortsplib.Header{
435 | "CSeq": []string{cseq[0]},
436 | "Transport": []string{strings.Join([]string{
437 | "RTP/AVP/TCP",
438 | "unicast",
439 | fmt.Sprintf("interleaved=%s", interleaved),
440 | }, ";")},
441 | "Session": []string{"12345678"},
442 | },
443 | })
444 | return true
445 |
446 | } else {
447 | c.writeResError(req, gortsplib.StatusBadRequest, fmt.Errorf("transport header does not contain a valid protocol (RTP/AVP, RTP/AVP/UDP or RTP/AVP/TCP) (%s)", tsRaw[0]))
448 | return false
449 | }
450 |
451 | default:
452 | c.writeResError(req, gortsplib.StatusBadRequest, fmt.Errorf("client is in state '%s'", c.state))
453 | return false
454 | }
455 |
456 | case gortsplib.PLAY:
457 | if c.state != _CLIENT_STATE_PRE_PLAY {
458 | c.writeResError(req, gortsplib.StatusBadRequest,
459 | fmt.Errorf("client is in state '%s' instead of '%s'", c.state, _CLIENT_STATE_PRE_PLAY))
460 | return false
461 | }
462 |
463 | if path != c.path {
464 | c.writeResError(req, gortsplib.StatusBadRequest, fmt.Errorf("path has changed"))
465 | return false
466 | }
467 |
468 | err := func() error {
469 | c.p.tcpl.mutex.Lock()
470 | defer c.p.tcpl.mutex.Unlock()
471 |
472 | str, ok := c.p.streams[c.path]
473 | if !ok {
474 | return fmt.Errorf("no one is streaming on path '%s'", c.path)
475 | }
476 |
477 | if len(c.streamTracks) != len(str.serverSdpParsed.Medias) {
478 | return fmt.Errorf("not all tracks have been setup")
479 | }
480 |
481 | return nil
482 | }()
483 | if err != nil {
484 | c.writeResError(req, gortsplib.StatusBadRequest, err)
485 | return false
486 | }
487 |
488 | // first write response, then set state
489 | // otherwise, in case of TCP connections, RTP packets could be written
490 | // before the response
491 | c.conn.WriteResponse(&gortsplib.Response{
492 | StatusCode: gortsplib.StatusOK,
493 | Header: gortsplib.Header{
494 | "CSeq": []string{cseq[0]},
495 | "Session": []string{"12345678"},
496 | },
497 | })
498 |
499 | c.log("is receiving on path '%s', %d %s via %s", c.path, len(c.streamTracks), func() string {
500 | if len(c.streamTracks) == 1 {
501 | return "track"
502 | }
503 | return "tracks"
504 | }(), c.streamProtocol)
505 |
506 | c.p.tcpl.mutex.Lock()
507 | c.state = _CLIENT_STATE_PLAY
508 | c.p.tcpl.mutex.Unlock()
509 |
510 | // when protocol is TCP, the RTSP connection becomes a RTP connection
511 | if c.streamProtocol == _STREAM_PROTOCOL_TCP {
512 | // write RTP frames sequentially
513 | go func() {
514 | for frame := range c.write {
515 | c.conn.WriteInterleavedFrame(frame)
516 | }
517 | }()
518 |
519 | // receive RTP feedback, do not parse it, wait until connection closes
520 | buf := make([]byte, 2048)
521 | for {
522 | _, err := c.conn.NetConn().Read(buf)
523 | if err != nil {
524 | if err != io.EOF {
525 | c.log("ERR: %s", err)
526 | }
527 | return false
528 | }
529 | }
530 | }
531 |
532 | return true
533 |
534 | case gortsplib.PAUSE:
535 | if c.state != _CLIENT_STATE_PLAY {
536 | c.writeResError(req, gortsplib.StatusBadRequest,
537 | fmt.Errorf("client is in state '%s' instead of '%s'", c.state, _CLIENT_STATE_PLAY))
538 | return false
539 | }
540 |
541 | if path != c.path {
542 | c.writeResError(req, gortsplib.StatusBadRequest, fmt.Errorf("path has changed"))
543 | return false
544 | }
545 |
546 | c.log("paused")
547 |
548 | c.p.tcpl.mutex.Lock()
549 | c.state = _CLIENT_STATE_PRE_PLAY
550 | c.p.tcpl.mutex.Unlock()
551 |
552 | c.conn.WriteResponse(&gortsplib.Response{
553 | StatusCode: gortsplib.StatusOK,
554 | Header: gortsplib.Header{
555 | "CSeq": []string{cseq[0]},
556 | "Session": []string{"12345678"},
557 | },
558 | })
559 | return true
560 |
561 | case gortsplib.TEARDOWN:
562 | // close connection silently
563 | return false
564 |
565 | default:
566 | c.writeResError(req, gortsplib.StatusBadRequest, fmt.Errorf("unhandled method '%s'", req.Method))
567 | return false
568 | }
569 | }
570 |
--------------------------------------------------------------------------------
/server-tcpl.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "log"
5 | "net"
6 | "sync"
7 |
8 | "github.com/aler9/gortsplib"
9 | )
10 |
11 | type serverTcpListener struct {
12 | p *program
13 | nconn *net.TCPListener
14 | mutex sync.RWMutex
15 | clients map[*serverClient]struct{}
16 | done chan struct{}
17 | }
18 |
19 | func newServerTcpListener(p *program) (*serverTcpListener, error) {
20 | nconn, err := net.ListenTCP("tcp", &net.TCPAddr{
21 | Port: p.conf.Server.RtspPort,
22 | })
23 | if err != nil {
24 | return nil, err
25 | }
26 |
27 | l := &serverTcpListener{
28 | p: p,
29 | nconn: nconn,
30 | clients: make(map[*serverClient]struct{}),
31 | done: make(chan struct{}),
32 | }
33 |
34 | l.log("opened on :%d", p.conf.Server.RtspPort)
35 | return l, nil
36 | }
37 |
38 | func (l *serverTcpListener) log(format string, args ...interface{}) {
39 | log.Printf("[TCP listener] "+format, args...)
40 | }
41 |
42 | func (l *serverTcpListener) run() {
43 | for {
44 | nconn, err := l.nconn.AcceptTCP()
45 | if err != nil {
46 | break
47 | }
48 |
49 | newServerClient(l.p, nconn)
50 | }
51 |
52 | // close clients
53 | var doneChans []chan struct{}
54 | func() {
55 | l.mutex.Lock()
56 | defer l.mutex.Unlock()
57 | for c := range l.clients {
58 | c.close()
59 | doneChans = append(doneChans, c.done)
60 | }
61 | }()
62 | for _, c := range doneChans {
63 | <-c
64 | }
65 |
66 | close(l.done)
67 | }
68 |
69 | func (l *serverTcpListener) close() {
70 | l.nconn.Close()
71 | <-l.done
72 | }
73 |
74 | func (l *serverTcpListener) forwardTrack(path string, id int, flow trackFlow, frame []byte) {
75 | for c := range l.clients {
76 | if c.path == path && c.state == _CLIENT_STATE_PLAY {
77 | if c.streamProtocol == _STREAM_PROTOCOL_UDP {
78 | if flow == _TRACK_FLOW_RTP {
79 | l.p.udplRtp.write <- &udpWrite{
80 | addr: &net.UDPAddr{
81 | IP: c.ip(),
82 | Zone: c.zone(),
83 | Port: c.streamTracks[id].rtpPort,
84 | },
85 | buf: frame,
86 | }
87 | } else {
88 | l.p.udplRtcp.write <- &udpWrite{
89 | addr: &net.UDPAddr{
90 | IP: c.ip(),
91 | Zone: c.zone(),
92 | Port: c.streamTracks[id].rtcpPort,
93 | },
94 | buf: frame,
95 | }
96 | }
97 |
98 | } else {
99 | c.write <- &gortsplib.InterleavedFrame{
100 | Channel: trackToInterleavedChannel(id, flow),
101 | Content: frame,
102 | }
103 | }
104 | }
105 | }
106 | }
107 |
--------------------------------------------------------------------------------
/server-udpl.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "log"
5 | "net"
6 | "time"
7 | )
8 |
9 | type udpWrite struct {
10 | addr *net.UDPAddr
11 | buf []byte
12 | }
13 |
14 | type serverUdpListener struct {
15 | p *program
16 | nconn *net.UDPConn
17 | flow trackFlow
18 | write chan *udpWrite
19 | done chan struct{}
20 | }
21 |
22 | func newServerUdpListener(p *program, port int, flow trackFlow) (*serverUdpListener, error) {
23 | nconn, err := net.ListenUDP("udp", &net.UDPAddr{
24 | Port: port,
25 | })
26 | if err != nil {
27 | return nil, err
28 | }
29 |
30 | l := &serverUdpListener{
31 | p: p,
32 | nconn: nconn,
33 | flow: flow,
34 | write: make(chan *udpWrite),
35 | done: make(chan struct{}),
36 | }
37 |
38 | l.log("opened on :%d", port)
39 | return l, nil
40 | }
41 |
42 | func (l *serverUdpListener) log(format string, args ...interface{}) {
43 | var label string
44 | if l.flow == _TRACK_FLOW_RTP {
45 | label = "RTP"
46 | } else {
47 | label = "RTCP"
48 | }
49 | log.Printf("[UDP/"+label+" listener] "+format, args...)
50 | }
51 |
52 | func (l *serverUdpListener) run() {
53 | go func() {
54 | for w := range l.write {
55 | l.nconn.SetWriteDeadline(time.Now().Add(l.p.writeTimeout))
56 | l.nconn.WriteTo(w.buf, w.addr)
57 | }
58 | }()
59 |
60 | buf := make([]byte, 2048) // UDP MTU is 1400
61 | for {
62 | _, _, err := l.nconn.ReadFromUDP(buf)
63 | if err != nil {
64 | break
65 | }
66 | }
67 |
68 | close(l.write)
69 |
70 | close(l.done)
71 | }
72 |
73 | func (l *serverUdpListener) close() {
74 | l.nconn.Close()
75 | <-l.done
76 | }
77 |
--------------------------------------------------------------------------------
/stream-udpl.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "net"
5 | "sync"
6 | "time"
7 | )
8 |
9 | type streamUdpListenerState int
10 |
11 | const (
12 | _UDPL_STATE_STARTING streamUdpListenerState = iota
13 | _UDPL_STATE_RUNNING
14 | )
15 |
16 | type streamUdpListener struct {
17 | p *program
18 | nconn *net.UDPConn
19 | state streamUdpListenerState
20 | done chan struct{}
21 | publisherIp net.IP
22 | publisherPort int
23 | trackId int
24 | flow trackFlow
25 | path string
26 | mutex sync.Mutex
27 | lastFrameTime time.Time
28 | }
29 |
30 | func newStreamUdpListener(p *program, port int) (*streamUdpListener, error) {
31 | nconn, err := net.ListenUDP("udp", &net.UDPAddr{
32 | Port: port,
33 | })
34 | if err != nil {
35 | return nil, err
36 | }
37 |
38 | l := &streamUdpListener{
39 | p: p,
40 | nconn: nconn,
41 | state: _UDPL_STATE_STARTING,
42 | done: make(chan struct{}),
43 | }
44 |
45 | return l, nil
46 | }
47 |
48 | func (l *streamUdpListener) close() {
49 | l.nconn.Close()
50 |
51 | if l.state == _UDPL_STATE_RUNNING {
52 | <-l.done
53 | }
54 | }
55 |
56 | func (l *streamUdpListener) start() {
57 | l.state = _UDPL_STATE_RUNNING
58 | go l.run()
59 | }
60 |
61 | func (l *streamUdpListener) run() {
62 | for {
63 | // create a buffer for each read.
64 | // this is necessary since the buffer is propagated with channels
65 | // so it must be unique.
66 | buf := make([]byte, 2048) // UDP MTU is 1400
67 | n, addr, err := l.nconn.ReadFromUDP(buf)
68 | if err != nil {
69 | break
70 | }
71 |
72 | if !l.publisherIp.Equal(addr.IP) || addr.Port != l.publisherPort {
73 | continue
74 | }
75 |
76 | func() {
77 | l.p.tcpl.mutex.RLock()
78 | defer l.p.tcpl.mutex.RUnlock()
79 |
80 | l.p.tcpl.forwardTrack(l.path, l.trackId, l.flow, buf[:n])
81 | }()
82 |
83 | func() {
84 | l.mutex.Lock()
85 | defer l.mutex.Unlock()
86 | l.lastFrameTime = time.Now()
87 | }()
88 | }
89 |
90 | close(l.done)
91 | }
92 |
--------------------------------------------------------------------------------
/stream.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "fmt"
5 | "log"
6 | "math/rand"
7 | "net"
8 | "net/url"
9 | "strconv"
10 | "strings"
11 | "time"
12 |
13 | "github.com/aler9/gortsplib"
14 | "gortc.io/sdp"
15 | )
16 |
17 | const (
18 | _DIAL_TIMEOUT = 10 * time.Second
19 | _RETRY_INTERVAL = 5 * time.Second
20 | _CHECK_STREAM_INTERVAL = 6 * time.Second
21 | _STREAM_DEAD_AFTER = 5 * time.Second
22 | _KEEPALIVE_INTERVAL = 60 * time.Second
23 | )
24 |
25 | type streamUdpListenerPair struct {
26 | udplRtp *streamUdpListener
27 | udplRtcp *streamUdpListener
28 | }
29 |
30 | type streamState int
31 |
32 | const (
33 | _STREAM_STATE_STARTING streamState = iota
34 | _STREAM_STATE_READY
35 | )
36 |
37 | type stream struct {
38 | p *program
39 | state streamState
40 | path string
41 | conf streamConf
42 | ur *url.URL
43 | proto streamProtocol
44 | clientSdpParsed *sdp.Message
45 | serverSdpText []byte
46 | serverSdpParsed *sdp.Message
47 | firstTime bool
48 | terminate chan struct{}
49 | done chan struct{}
50 | }
51 |
52 | func newStream(p *program, path string, conf streamConf) (*stream, error) {
53 | ur, err := url.Parse(conf.Url)
54 | if err != nil {
55 | return nil, err
56 | }
57 |
58 | if ur.Port() == "" {
59 | ur.Host = ur.Hostname() + ":554"
60 | }
61 | if conf.Protocol == "" {
62 | conf.Protocol = "udp"
63 | }
64 |
65 | if ur.Scheme != "rtsp" {
66 | return nil, fmt.Errorf("unsupported scheme: %s", ur.Scheme)
67 | }
68 | if ur.User != nil {
69 | pass, _ := ur.User.Password()
70 | user := ur.User.Username()
71 | if user != "" && pass == "" ||
72 | user == "" && pass != "" {
73 | fmt.Errorf("username and password must be both provided")
74 | }
75 | }
76 |
77 | proto, err := func() (streamProtocol, error) {
78 | switch conf.Protocol {
79 | case "udp":
80 | return _STREAM_PROTOCOL_UDP, nil
81 |
82 | case "tcp":
83 | return _STREAM_PROTOCOL_TCP, nil
84 | }
85 | return streamProtocol(0), fmt.Errorf("unsupported protocol: '%v'", conf.Protocol)
86 | }()
87 | if err != nil {
88 | return nil, err
89 | }
90 |
91 | s := &stream{
92 | p: p,
93 | state: _STREAM_STATE_STARTING,
94 | path: path,
95 | conf: conf,
96 | ur: ur,
97 | proto: proto,
98 | firstTime: true,
99 | terminate: make(chan struct{}),
100 | done: make(chan struct{}),
101 | }
102 |
103 | return s, nil
104 | }
105 |
106 | func (s *stream) log(format string, args ...interface{}) {
107 | format = "[STREAM " + s.path + "] " + format
108 | log.Printf(format, args...)
109 | }
110 |
111 | func (s *stream) run() {
112 | for {
113 | ok := s.do()
114 | if !ok {
115 | break
116 | }
117 | }
118 |
119 | close(s.done)
120 | }
121 |
122 | func (s *stream) do() bool {
123 | if s.firstTime {
124 | s.firstTime = false
125 | } else {
126 | t := time.NewTimer(_RETRY_INTERVAL)
127 | select {
128 | case <-s.terminate:
129 | return false
130 | case <-t.C:
131 | }
132 | }
133 |
134 | s.log("initializing with protocol %s", s.proto)
135 |
136 | var nconn net.Conn
137 | var err error
138 | dialDone := make(chan struct{})
139 | go func() {
140 | nconn, err = net.DialTimeout("tcp", s.ur.Host, _DIAL_TIMEOUT)
141 | close(dialDone)
142 | }()
143 |
144 | select {
145 | case <-s.terminate:
146 | return false
147 | case <-dialDone:
148 | }
149 |
150 | if err != nil {
151 | s.log("ERR: %s", err)
152 | return true
153 | }
154 | defer nconn.Close()
155 |
156 | conn, err := gortsplib.NewConnClient(gortsplib.ConnClientConf{
157 | NConn: nconn,
158 | Username: func() string {
159 | if s.ur.User != nil {
160 | return s.ur.User.Username()
161 | }
162 | return ""
163 | }(),
164 | Password: func() string {
165 | if s.ur.User != nil {
166 | pass, _ := s.ur.User.Password()
167 | return pass
168 | }
169 | return ""
170 | }(),
171 | ReadTimeout: s.p.readTimeout,
172 | WriteTimeout: s.p.writeTimeout,
173 | })
174 | if err != nil {
175 | s.log("ERR: %s", err)
176 | return true
177 | }
178 |
179 | res, err := conn.WriteRequest(&gortsplib.Request{
180 | Method: gortsplib.OPTIONS,
181 | Url: &url.URL{
182 | Scheme: "rtsp",
183 | Host: s.ur.Host,
184 | Path: "/",
185 | },
186 | })
187 | if err != nil {
188 | s.log("ERR: %s", err)
189 | return true
190 | }
191 |
192 | // OPTIONS is not available in some cameras
193 | if res.StatusCode != gortsplib.StatusOK && res.StatusCode != gortsplib.StatusNotFound {
194 | s.log("ERR: OPTIONS returned code %d (%s)", res.StatusCode, res.StatusMessage)
195 | return true
196 | }
197 |
198 | res, err = conn.WriteRequest(&gortsplib.Request{
199 | Method: gortsplib.DESCRIBE,
200 | Url: &url.URL{
201 | Scheme: "rtsp",
202 | Host: s.ur.Host,
203 | Path: s.ur.Path,
204 | RawQuery: s.ur.RawQuery,
205 | },
206 | })
207 | if err != nil {
208 | s.log("ERR: %s", err)
209 | return true
210 | }
211 |
212 | if res.StatusCode != gortsplib.StatusOK {
213 | s.log("ERR: DESCRIBE returned code %d (%s)", res.StatusCode, res.StatusMessage)
214 | return true
215 | }
216 |
217 | contentType, ok := res.Header["Content-Type"]
218 | if !ok || len(contentType) != 1 {
219 | s.log("ERR: Content-Type not provided")
220 | return true
221 | }
222 |
223 | if contentType[0] != "application/sdp" {
224 | s.log("ERR: wrong Content-Type, expected application/sdp")
225 | return true
226 | }
227 |
228 | clientSdpParsed, err := gortsplib.SDPParse(res.Content)
229 | if err != nil {
230 | s.log("ERR: invalid SDP: %s", err)
231 | return true
232 | }
233 |
234 | // create a filtered SDP that is used by the server (not by the client)
235 | serverSdpParsed, serverSdpText := gortsplib.SDPFilter(clientSdpParsed, res.Content)
236 |
237 | func() {
238 | s.p.tcpl.mutex.Lock()
239 | defer s.p.tcpl.mutex.Unlock()
240 |
241 | s.clientSdpParsed = clientSdpParsed
242 | s.serverSdpText = serverSdpText
243 | s.serverSdpParsed = serverSdpParsed
244 | }()
245 |
246 | if s.proto == _STREAM_PROTOCOL_UDP {
247 | return s.runUdp(conn)
248 | } else {
249 | return s.runTcp(conn)
250 | }
251 | }
252 |
253 | func (s *stream) runUdp(conn *gortsplib.ConnClient) bool {
254 | publisherIp := conn.NetConn().RemoteAddr().(*net.TCPAddr).IP
255 |
256 | var streamUdpListenerPairs []streamUdpListenerPair
257 |
258 | defer func() {
259 | for _, pair := range streamUdpListenerPairs {
260 | pair.udplRtp.close()
261 | pair.udplRtcp.close()
262 | }
263 | }()
264 |
265 | for i, media := range s.clientSdpParsed.Medias {
266 | var rtpPort int
267 | var rtcpPort int
268 | var udplRtp *streamUdpListener
269 | var udplRtcp *streamUdpListener
270 | func() {
271 | for {
272 | // choose two consecutive ports in range 65536-10000
273 | // rtp must be pair and rtcp odd
274 | rtpPort = (rand.Intn((65535-10000)/2) * 2) + 10000
275 | rtcpPort = rtpPort + 1
276 |
277 | var err error
278 | udplRtp, err = newStreamUdpListener(s.p, rtpPort)
279 | if err != nil {
280 | continue
281 | }
282 |
283 | udplRtcp, err = newStreamUdpListener(s.p, rtcpPort)
284 | if err != nil {
285 | udplRtp.close()
286 | continue
287 | }
288 |
289 | return
290 | }
291 | }()
292 |
293 | res, err := conn.WriteRequest(&gortsplib.Request{
294 | Method: gortsplib.SETUP,
295 | Url: func() *url.URL {
296 | control := media.Attributes.Value("control")
297 |
298 | // no control attribute
299 | if control == "" {
300 | return s.ur
301 | }
302 |
303 | // absolute path
304 | if strings.HasPrefix(control, "rtsp://") {
305 | ur, err := url.Parse(control)
306 | if err != nil {
307 | return s.ur
308 | }
309 | return ur
310 | }
311 |
312 | // relative path
313 | return &url.URL{
314 | Scheme: "rtsp",
315 | Host: s.ur.Host,
316 | Path: func() string {
317 | ret := s.ur.Path
318 |
319 | if len(ret) == 0 || ret[len(ret)-1] != '/' {
320 | ret += "/"
321 | }
322 |
323 | control := media.Attributes.Value("control")
324 | if control != "" {
325 | ret += control
326 | } else {
327 | ret += "trackID=" + strconv.FormatInt(int64(i+1), 10)
328 | }
329 |
330 | return ret
331 | }(),
332 | RawQuery: s.ur.RawQuery,
333 | }
334 | }(),
335 | Header: gortsplib.Header{
336 | "Transport": []string{strings.Join([]string{
337 | "RTP/AVP/UDP",
338 | "unicast",
339 | fmt.Sprintf("client_port=%d-%d", rtpPort, rtcpPort),
340 | }, ";")},
341 | },
342 | })
343 | if err != nil {
344 | s.log("ERR: %s", err)
345 | udplRtp.close()
346 | udplRtcp.close()
347 | return true
348 | }
349 |
350 | if res.StatusCode != gortsplib.StatusOK {
351 | s.log("ERR: SETUP returned code %d (%s)", res.StatusCode, res.StatusMessage)
352 | udplRtp.close()
353 | udplRtcp.close()
354 | return true
355 | }
356 |
357 | tsRaw, ok := res.Header["Transport"]
358 | if !ok || len(tsRaw) != 1 {
359 | s.log("ERR: transport header not provided")
360 | udplRtp.close()
361 | udplRtcp.close()
362 | return true
363 | }
364 |
365 | th := gortsplib.ReadHeaderTransport(tsRaw[0])
366 | rtpServerPort, rtcpServerPort := th.GetPorts("server_port")
367 | if rtpServerPort == 0 {
368 | s.log("ERR: server ports not provided")
369 | udplRtp.close()
370 | udplRtcp.close()
371 | return true
372 | }
373 |
374 | udplRtp.publisherIp = publisherIp
375 | udplRtp.publisherPort = rtpServerPort
376 | udplRtp.trackId = i
377 | udplRtp.flow = _TRACK_FLOW_RTP
378 | udplRtp.path = s.path
379 |
380 | udplRtcp.publisherIp = publisherIp
381 | udplRtcp.publisherPort = rtcpServerPort
382 | udplRtcp.trackId = i
383 | udplRtcp.flow = _TRACK_FLOW_RTCP
384 | udplRtcp.path = s.path
385 |
386 | streamUdpListenerPairs = append(streamUdpListenerPairs, streamUdpListenerPair{
387 | udplRtp: udplRtp,
388 | udplRtcp: udplRtcp,
389 | })
390 | }
391 |
392 | res, err := conn.WriteRequest(&gortsplib.Request{
393 | Method: gortsplib.PLAY,
394 | Url: &url.URL{
395 | Scheme: "rtsp",
396 | Host: s.ur.Host,
397 | Path: s.ur.Path,
398 | RawQuery: s.ur.RawQuery,
399 | },
400 | })
401 | if err != nil {
402 | s.log("ERR: %s", err)
403 | return true
404 | }
405 |
406 | if res.StatusCode != gortsplib.StatusOK {
407 | s.log("ERR: PLAY returned code %d (%s)", res.StatusCode, res.StatusMessage)
408 | return true
409 | }
410 |
411 | for _, pair := range streamUdpListenerPairs {
412 | pair.udplRtp.start()
413 | pair.udplRtcp.start()
414 | }
415 |
416 | tickerSendKeepalive := time.NewTicker(_KEEPALIVE_INTERVAL)
417 | defer tickerSendKeepalive.Stop()
418 |
419 | tickerCheckStream := time.NewTicker(_CHECK_STREAM_INTERVAL)
420 | defer tickerSendKeepalive.Stop()
421 |
422 | func() {
423 | s.p.tcpl.mutex.Lock()
424 | defer s.p.tcpl.mutex.Unlock()
425 | s.state = _STREAM_STATE_READY
426 | }()
427 |
428 | defer func() {
429 | s.p.tcpl.mutex.Lock()
430 | defer s.p.tcpl.mutex.Unlock()
431 | s.state = _STREAM_STATE_STARTING
432 |
433 | // disconnect all clients
434 | for c := range s.p.tcpl.clients {
435 | if c.path == s.path {
436 | c.close()
437 | }
438 | }
439 | }()
440 |
441 | s.log("ready")
442 |
443 | for {
444 | select {
445 | case <-s.terminate:
446 | return false
447 |
448 | case <-tickerSendKeepalive.C:
449 | _, err = conn.WriteRequest(&gortsplib.Request{
450 | Method: gortsplib.OPTIONS,
451 | Url: &url.URL{
452 | Scheme: "rtsp",
453 | Host: s.ur.Host,
454 | Path: "/",
455 | },
456 | })
457 | if err != nil {
458 | s.log("ERR: %s", err)
459 | return true
460 | }
461 |
462 | case <-tickerCheckStream.C:
463 | lastFrameTime := time.Time{}
464 |
465 | getLastFrameTime := func(l *streamUdpListener) {
466 | l.mutex.Lock()
467 | defer l.mutex.Unlock()
468 | if l.lastFrameTime.After(lastFrameTime) {
469 | lastFrameTime = l.lastFrameTime
470 | }
471 | }
472 |
473 | for _, pair := range streamUdpListenerPairs {
474 | getLastFrameTime(pair.udplRtp)
475 | getLastFrameTime(pair.udplRtcp)
476 | }
477 |
478 | if time.Since(lastFrameTime) >= _STREAM_DEAD_AFTER {
479 | s.log("ERR: stream is dead")
480 | return true
481 | }
482 | }
483 | }
484 | }
485 |
486 | func (s *stream) runTcp(conn *gortsplib.ConnClient) bool {
487 | for i, media := range s.clientSdpParsed.Medias {
488 | interleaved := fmt.Sprintf("interleaved=%d-%d", (i * 2), (i*2)+1)
489 |
490 | res, err := conn.WriteRequest(&gortsplib.Request{
491 | Method: gortsplib.SETUP,
492 | Url: func() *url.URL {
493 | control := media.Attributes.Value("control")
494 |
495 | // no control attribute
496 | if control == "" {
497 | return s.ur
498 | }
499 |
500 | // absolute path
501 | if strings.HasPrefix(control, "rtsp://") {
502 | ur, err := url.Parse(control)
503 | if err != nil {
504 | return s.ur
505 | }
506 | return ur
507 | }
508 |
509 | // relative path
510 | return &url.URL{
511 | Scheme: "rtsp",
512 | Host: s.ur.Host,
513 | Path: func() string {
514 | ret := s.ur.Path
515 |
516 | if len(ret) == 0 || ret[len(ret)-1] != '/' {
517 | ret += "/"
518 | }
519 |
520 | control := media.Attributes.Value("control")
521 | if control != "" {
522 | ret += control
523 | } else {
524 | ret += "trackID=" + strconv.FormatInt(int64(i+1), 10)
525 | }
526 |
527 | return ret
528 | }(),
529 | RawQuery: s.ur.RawQuery,
530 | }
531 | }(),
532 | Header: gortsplib.Header{
533 | "Transport": []string{strings.Join([]string{
534 | "RTP/AVP/TCP",
535 | "unicast",
536 | interleaved,
537 | }, ";")},
538 | },
539 | })
540 | if err != nil {
541 | s.log("ERR: %s", err)
542 | return true
543 | }
544 |
545 | if res.StatusCode != gortsplib.StatusOK {
546 | s.log("ERR: SETUP returned code %d (%s)", res.StatusCode, res.StatusMessage)
547 | return true
548 | }
549 |
550 | tsRaw, ok := res.Header["Transport"]
551 | if !ok || len(tsRaw) != 1 {
552 | s.log("ERR: transport header not provided")
553 | return true
554 | }
555 |
556 | th := gortsplib.ReadHeaderTransport(tsRaw[0])
557 |
558 | _, ok = th[interleaved]
559 | if !ok {
560 | s.log("ERR: transport header does not have %s (%s)", interleaved, tsRaw[0])
561 | return true
562 | }
563 | }
564 |
565 | err := conn.WriteRequestNoResponse(&gortsplib.Request{
566 | Method: gortsplib.PLAY,
567 | Url: &url.URL{
568 | Scheme: "rtsp",
569 | Host: s.ur.Host,
570 | Path: s.ur.Path,
571 | RawQuery: s.ur.RawQuery,
572 | },
573 | })
574 | if err != nil {
575 | s.log("ERR: %s", err)
576 | return true
577 | }
578 |
579 | outer:
580 | for {
581 | frame := &gortsplib.InterleavedFrame{
582 | Content: make([]byte, 512*1024),
583 | }
584 | vres, err := conn.ReadInterleavedFrameOrResponse(frame)
585 | if err != nil {
586 | s.log("ERR: %s", err)
587 | return true
588 | }
589 |
590 | switch res := vres.(type) {
591 | case *gortsplib.Response:
592 | if res.StatusCode != gortsplib.StatusOK {
593 | s.log("ERR: PLAY returned code %d (%s)", res.StatusCode, res.StatusMessage)
594 | return true
595 | }
596 | break outer
597 |
598 | case *gortsplib.InterleavedFrame:
599 | // ignore the frames sent before the response
600 | }
601 | }
602 |
603 | func() {
604 | s.p.tcpl.mutex.Lock()
605 | defer s.p.tcpl.mutex.Unlock()
606 | s.state = _STREAM_STATE_READY
607 | }()
608 |
609 | defer func() {
610 | s.p.tcpl.mutex.Lock()
611 | defer s.p.tcpl.mutex.Unlock()
612 | s.state = _STREAM_STATE_STARTING
613 |
614 | // disconnect all clients
615 | for c := range s.p.tcpl.clients {
616 | if c.path == s.path {
617 | c.close()
618 | }
619 | }
620 | }()
621 |
622 | s.log("ready")
623 |
624 | chanConnError := make(chan struct{})
625 | go func() {
626 | for {
627 | frame := &gortsplib.InterleavedFrame{
628 | Content: make([]byte, 512*1024),
629 | }
630 | err := conn.ReadInterleavedFrame(frame)
631 | if err != nil {
632 | s.log("ERR: %s", err)
633 | close(chanConnError)
634 | break
635 | }
636 |
637 | trackId, trackFlow := interleavedChannelToTrack(frame.Channel)
638 |
639 | func() {
640 | s.p.tcpl.mutex.RLock()
641 | defer s.p.tcpl.mutex.RUnlock()
642 |
643 | s.p.tcpl.forwardTrack(s.path, trackId, trackFlow, frame.Content)
644 | }()
645 | }
646 | }()
647 |
648 | select {
649 | case <-s.terminate:
650 | return false
651 | case <-chanConnError:
652 | return true
653 | }
654 | }
655 |
656 | func (s *stream) close() {
657 | close(s.terminate)
658 | <-s.done
659 | }
660 |
--------------------------------------------------------------------------------
/test-images/ffmpeg/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM amd64/alpine:3.11
2 |
3 | RUN apk add --no-cache \
4 | ffmpeg
5 |
6 | COPY emptyvideo.ts /
7 |
8 | COPY start.sh /
9 | RUN chmod +x /start.sh
10 |
11 | ENTRYPOINT [ "/start.sh" ]
12 |
--------------------------------------------------------------------------------
/test-images/ffmpeg/emptyvideo.ts:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/aler9/rtsp-simple-proxy/cbe0a006b0b074eda1ab39bc9fe505ac6489ea67/test-images/ffmpeg/emptyvideo.ts
--------------------------------------------------------------------------------
/test-images/ffmpeg/start.sh:
--------------------------------------------------------------------------------
1 | #!/bin/sh -e
2 |
3 | ffmpeg $@
4 |
5 | echo "all right"
6 |
--------------------------------------------------------------------------------
/test-images/rtsp-simple-server/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM amd64/alpine:3.11
2 |
3 | ENV VERSION 0.6.0
4 | RUN wget -O- https://github.com/aler9/rtsp-simple-server/releases/download/v${VERSION}/rtsp-simple-server_v${VERSION}_linux_amd64.tar.gz | tar xvzf - \
5 | && mv rtsp-simple-server /usr/bin
6 |
7 | COPY start.sh /
8 | RUN chmod +x /start.sh
9 |
10 | ENTRYPOINT [ "/start.sh" ]
11 |
--------------------------------------------------------------------------------
/test-images/rtsp-simple-server/start.sh:
--------------------------------------------------------------------------------
1 | #!/bin/sh -e
2 |
3 | rtsp-simple-server $@ 2>&1
4 |
--------------------------------------------------------------------------------