├── .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 | [![Go Report Card](https://goreportcard.com/badge/github.com/aler9/rtsp-simple-proxy)](https://goreportcard.com/report/github.com/aler9/rtsp-simple-proxy) 13 | [![Build Status](https://travis-ci.org/aler9/rtsp-simple-proxy.svg?branch=master)](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 | --------------------------------------------------------------------------------