├── .dockerignore ├── .github ├── FUNDING.yml ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md ├── lint-filename.sh └── workflows │ ├── build-json-rpc.yaml │ ├── docker-allrpc.yaml │ ├── docker-grpc.yaml │ ├── docker-jsonrpc.yaml │ ├── lint.yaml │ └── test.yaml ├── .gitignore ├── .golangci.yml ├── Dockerfile ├── FAQ.md ├── LICENSE ├── Makefile ├── README.md ├── cmd └── signal │ ├── allrpc │ ├── Dockerfile │ ├── main.go │ └── server │ │ └── server.go │ ├── grpc │ ├── Dockerfile │ ├── README.md │ ├── main.go │ └── server │ │ ├── server.go │ │ └── wrapped.go │ └── json-rpc │ ├── Dockerfile │ ├── README.md │ ├── main.go │ └── server │ └── server.go ├── codecov.yml ├── config.toml ├── examples ├── README.md ├── echotest-jsonrpc │ ├── README.md │ ├── docker-compose.yaml │ └── index.html ├── gallerytest-jsonrpc │ ├── README.md │ ├── docker-compose.yaml │ └── index.html ├── pubsubtest-grpc │ ├── index.html │ └── main.js └── pubsubtest-jsonrpc │ ├── README.md │ ├── docker-compose.yaml │ ├── index.html │ └── main.js ├── go.mod ├── go.sum ├── pkg ├── buffer │ ├── bucket.go │ ├── bucket_test.go │ ├── buffer.go │ ├── buffer_test.go │ ├── errors.go │ ├── factory.go │ ├── helpers.go │ ├── helpers_test.go │ ├── nack.go │ ├── nack_test.go │ └── rtcpreader.go ├── logger │ ├── zerologr.go │ └── zerologr_test.go ├── middlewares │ └── datachannel │ │ ├── README.md │ │ ├── keepalive.go │ │ └── subscriberapi.go ├── relay │ ├── README.md │ └── relay.go ├── sfu │ ├── audioobserver.go │ ├── audioobserver_test.go │ ├── datachannel.go │ ├── downtrack.go │ ├── errors.go │ ├── helpers.go │ ├── helpers_test.go │ ├── mediaengine.go │ ├── peer.go │ ├── publisher.go │ ├── receiver.go │ ├── receiver_test.go │ ├── relay.go │ ├── relaypeer.go │ ├── router.go │ ├── sequencer.go │ ├── sequencer_test.go │ ├── session.go │ ├── sfu.go │ ├── sfu_test.go │ ├── simulcast.go │ ├── subscriber.go │ └── turn.go ├── stats │ └── stream.go └── twcc │ ├── twcc.go │ └── twcc_test.go └── renovate.json /.dockerignore: -------------------------------------------------------------------------------- 1 | .git 2 | .github 3 | .gitignore 4 | .idea 5 | Dockerfile 6 | docker-compose.yml 7 | docker 8 | docs 9 | screenshots 10 | npm-debug.log 11 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | open_collective: pion-ion 3 | 4 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | 19 | 20 | ### Your environment. 21 | - Version: *Release or SHA* 22 | - Client: *include version* 23 | - Environement: *GCP, VM, K8S, ect* 24 | - Are you using a TURN server? *ion-sfu turn, coturn or other?* 25 | - Other Information - *stacktraces, webrtc dumps, related issues, suggestions how to fix, links for us to have context* 26 | 27 | ### What did you do? 28 | 32 | 33 | ### What did you expect? 34 | 35 | ### What happened? 36 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 18 | 19 | ## Summary 20 | 21 | One paragraph explanation of the feature. 22 | 23 | ## Motivation 24 | 25 | Why are we doing this? What use cases does it support? What is the expected outcome? Is this available in other implementations? Can this not be implemented using primitives? 26 | 27 | ## Describe alternatives you've considered 28 | 29 | A clear and concise description of the alternative solutions you've considered. 30 | 31 | ## Additional context 32 | 33 | Add any other context or screenshots about the feature request here. 34 | -------------------------------------------------------------------------------- /.github/lint-filename.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -e 4 | 5 | SCRIPT_PATH=$( cd "$(dirname "${BASH_SOURCE[0]}")" ; pwd -P ) 6 | GO_REGEX="^[a-zA-Z][a-zA-Z0-9_]*\.(go|pb.go)$" 7 | 8 | find "$SCRIPT_PATH/.." -name "*.go" | while read fullpath; do 9 | filename=$(basename -- "$fullpath") 10 | 11 | if ! [[ $filename =~ $GO_REGEX ]]; then 12 | echo "$filename is not a valid filename for Go code, only alpha, numbers and underscores are supported" 13 | exit 1 14 | fi 15 | done 16 | -------------------------------------------------------------------------------- /.github/workflows/build-json-rpc.yaml: -------------------------------------------------------------------------------- 1 | name: build json-rpc 2 | on: 3 | push: 4 | branches: 5 | - master 6 | pull_request: 7 | branches: 8 | - master 9 | jobs: 10 | build: 11 | name: build json-rpc 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v2 15 | 16 | - name: build json-rpc 17 | run: make build_jsonrpc 18 | -------------------------------------------------------------------------------- /.github/workflows/docker-allrpc.yaml: -------------------------------------------------------------------------------- 1 | name: allrpc docker 2 | on: 3 | push: 4 | branches: 5 | - master 6 | release: 7 | types: [published] 8 | pull_request: 9 | branches: 10 | - master 11 | jobs: 12 | build: 13 | name: build and push 14 | runs-on: ubuntu-latest 15 | timeout-minutes: 3 16 | steps: 17 | - uses: actions/checkout@v2 18 | 19 | - name: build 20 | run: docker build --tag pionwebrtc/ion-sfu:latest-allrpc -f cmd/signal/allrpc/Dockerfile . 21 | 22 | - name: login 23 | if: github.event_name == 'release' 24 | run: echo "${{ secrets.DOCKER_PASSWORD }}" | docker login -u "${{ secrets.DOCKER_USERNAME }}" --password-stdin 25 | 26 | - name: tag 27 | if: github.event_name == 'release' 28 | run: docker tag pionwebrtc/ion-sfu:latest-allrpc pionwebrtc/ion-sfu:"$TAG"-allrpc 29 | env: 30 | TAG: ${{ github.event.release.tag_name }} 31 | 32 | - name: push 33 | if: github.event_name == 'release' 34 | run: docker push pionwebrtc/ion-sfu:latest-allrpc && docker push pionwebrtc/ion-sfu:"$TAG"-allrpc 35 | env: 36 | TAG: ${{ github.event.release.tag_name }} -------------------------------------------------------------------------------- /.github/workflows/docker-grpc.yaml: -------------------------------------------------------------------------------- 1 | name: grpc docker 2 | on: 3 | push: 4 | branches: 5 | - master 6 | release: 7 | types: [published] 8 | pull_request: 9 | branches: 10 | - master 11 | jobs: 12 | build: 13 | name: build and push 14 | runs-on: ubuntu-latest 15 | timeout-minutes: 3 16 | steps: 17 | - uses: actions/checkout@v2 18 | 19 | - name: build 20 | run: docker build --tag pionwebrtc/ion-sfu:latest-grpc -f cmd/signal/grpc/Dockerfile . 21 | 22 | - name: login 23 | if: github.event_name == 'release' 24 | run: echo "${{ secrets.DOCKER_PASSWORD }}" | docker login -u "${{ secrets.DOCKER_USERNAME }}" --password-stdin 25 | 26 | - name: tag 27 | if: github.event_name == 'release' 28 | run: docker tag pionwebrtc/ion-sfu:latest-grpc pionwebrtc/ion-sfu:"$TAG"-grpc 29 | env: 30 | TAG: ${{ github.event.release.tag_name }} 31 | 32 | - name: push 33 | if: github.event_name == 'release' 34 | run: docker push pionwebrtc/ion-sfu:latest-grpc && docker push pionwebrtc/ion-sfu:"$TAG"-grpc 35 | env: 36 | TAG: ${{ github.event.release.tag_name }} 37 | -------------------------------------------------------------------------------- /.github/workflows/docker-jsonrpc.yaml: -------------------------------------------------------------------------------- 1 | name: jsonrpc docker 2 | on: 3 | push: 4 | branches: 5 | - master 6 | release: 7 | types: [published] 8 | pull_request: 9 | branches: 10 | - master 11 | jobs: 12 | build: 13 | name: build and push 14 | runs-on: ubuntu-latest 15 | timeout-minutes: 3 16 | steps: 17 | - uses: actions/checkout@v2 18 | 19 | - name: build 20 | run: docker build --tag pionwebrtc/ion-sfu:latest-jsonrpc -f cmd/signal/json-rpc/Dockerfile . 21 | 22 | - name: login 23 | if: github.event_name == 'release' 24 | run: echo "${{ secrets.DOCKER_PASSWORD }}" | docker login -u "${{ secrets.DOCKER_USERNAME }}" --password-stdin 25 | 26 | - name: tag 27 | if: github.event_name == 'release' 28 | run: docker tag pionwebrtc/ion-sfu:latest-jsonrpc pionwebrtc/ion-sfu:"$TAG"-jsonrpc 29 | env: 30 | TAG: ${{ github.event.release.tag_name }} 31 | 32 | - name: push 33 | if: github.event_name == 'release' 34 | run: docker push pionwebrtc/ion-sfu:latest-jsonrpc && docker push pionwebrtc/ion-sfu:"$TAG"-jsonrpc 35 | env: 36 | TAG: ${{ github.event.release.tag_name }} 37 | -------------------------------------------------------------------------------- /.github/workflows/lint.yaml: -------------------------------------------------------------------------------- 1 | name: lint all 2 | on: 3 | pull_request: 4 | types: 5 | - opened 6 | - edited 7 | - synchronize 8 | jobs: 9 | lint: 10 | name: lint 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@v2 14 | 15 | - name: Generate 16 | run: go generate ./... 17 | 18 | - name: golangci-lint 19 | uses: golangci/golangci-lint-action@v2 20 | env: 21 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 22 | with: 23 | version: v1.29 24 | -------------------------------------------------------------------------------- /.github/workflows/test.yaml: -------------------------------------------------------------------------------- 1 | name: test 2 | on: 3 | push: 4 | branches: 5 | - master 6 | pull_request: 7 | branches: 8 | - master 9 | jobs: 10 | test: 11 | name: test 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v2 15 | 16 | - name: test 17 | run: make test 18 | 19 | - uses: codecov/codecov-action@v1 20 | with: 21 | file: ./cover.out 22 | name: codecov-umbrella 23 | fail_ci_if_error: true 24 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .*.sw[op] 2 | logs 3 | .idea 4 | cover.out 5 | main 6 | .DS_Store 7 | .vscode 8 | vendor 9 | *.code-workspace 10 | *debug.test 11 | *.pem 12 | bin 13 | *.generated.go 14 | .stignore 15 | okteto.yml 16 | -------------------------------------------------------------------------------- /.golangci.yml: -------------------------------------------------------------------------------- 1 | linters-settings: 2 | gocritic: 3 | disabled-checks: 4 | - ifElseChain 5 | - singleCaseSwitch 6 | govet: 7 | check-shadowing: true 8 | misspell: 9 | locale: US 10 | 11 | linters: 12 | disable-all: true 13 | enable: 14 | - gofmt 15 | - golint 16 | - scopelint 17 | - gocritic 18 | 19 | run: 20 | skip-dirs: 21 | - examples/pub-mediadevice 22 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:stretch 2 | 3 | WORKDIR $GOPATH/src/github.com/pion/ion-sfu 4 | 5 | COPY go.mod go.sum ./ 6 | RUN cd $GOPATH/src/github.com/pion/ion-sfu && go mod download 7 | 8 | COPY sfu/ $GOPATH/src/github.com/pion/ion-sfu/pkg 9 | COPY cmd/ $GOPATH/src/github.com/pion/ion-sfu/cmd 10 | COPY config.toml $GOPATH/src/github.com/pion/ion-sfu/config.toml 11 | -------------------------------------------------------------------------------- /FAQ.md: -------------------------------------------------------------------------------- 1 | # Frequenty Asked Questions 2 | 3 | ### How can I join a session receive-only, without sending any media? 4 | 5 | If you are using [ion-sdk-js](https://github.com/pion/ion-sdk-js) this should work by default. If not, read on. 6 | 7 | If a participant does not have a microphone or camera, or denies access to them from the browser, how can they join? Something needs to be added to the publisher connection to generate valid SDP. 8 | 9 | Add a datachannel to the publisher peer connection. e.g. `this.publisher.createDataChannel("ion-sfu")` 10 | 11 | ### Does ion-sfu support audio level indication, telling me who is talking? 12 | 13 | Yes. The subscriber peer connection will contain a data channel. It gets sent an array of stream id that are making noise, ordered loudest first. 14 | 15 | Stream id is the `msid` from the SDP, [defined here](https://tools.ietf.org/html/draft-ietf-mmusic-msid-17). It is up to your application to map that to a meaningful value, such as the user's name. 16 | 17 | Here is an example in Typescript: 18 | ``` 19 | subscriber.ondatachannel = (e: RTCDataChannelEvent) => { 20 | e.channel.onmessage = (e: MessageEvent) => { 21 | this.mySpeakingCallback(JSON.parse(e.data)); 22 | } 23 | } 24 | ``` 25 | 26 | Audio level indication can be tuned [here in the configuration file](https://github.com/pion/ion-sfu/blob/master/config.toml#L15-L28). 27 | 28 | ### Can I record the audio and/or video to disk? 29 | 30 | Recording (and general audio/video processing) is provided by separate project [ion-avp](https://github.com/pion/ion-avp/). 31 | 32 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 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 | GO_LDFLAGS = -ldflags "-s -w" 2 | GO_VERSION = 1.14 3 | GO_TESTPKGS:=$(shell go list ./... | grep -v cmd | grep -v examples) 4 | 5 | all: nodes 6 | 7 | go_init: 8 | go mod download 9 | go generate ./... 10 | 11 | clean: 12 | rm -rf bin 13 | 14 | 15 | build_grpc: go_init 16 | go build -o bin/sfu-grpc $(GO_LDFLAGS) ./cmd/signal/grpc/main.go 17 | 18 | build_jsonrpc: go_init 19 | go build -o bin/sfu-json-rpc $(GO_LDFLAGS) ./cmd/signal/json-rpc/main.go 20 | 21 | build_allrpc: go_init 22 | go build -o bin/sfu-allrpc $(GO_LDFLAGS) ./cmd/signal/allrpc/main.go 23 | 24 | test: go_init 25 | go test \ 26 | -timeout 240s \ 27 | -coverprofile=cover.out -covermode=atomic \ 28 | -v -race ${GO_TESTPKGS} 29 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 |
3 | Ion SFU 4 |
5 |

6 |

Go implementation of a WebRTC Selective Forwarding Unit

7 |

8 | Slack Widget 9 | GoDoc 10 | Coverage Status 11 | Go Report Card 12 | License: MIT 13 |

14 |
15 | 16 | A [selective forwarding unit](https://webrtcglossary.com/sfu/) is a video routing service which allows webrtc sessions to scale more efficiently. This package provides a simple, flexible, high performance Go implementation of a WebRTC SFU. It can be called directly or through a [gRPC](cmd/signal/grpc) or [json-rpc](cmd/signal/json-rpc) interface. 17 | 18 | ## Features 19 | 20 | * Audio/Video/Datachannel forwarding 21 | * Congestion Control (TWCC, REMB, RR/SR) 22 | * Unified plan semantics 23 | * Pub/Sub Peer Connection (`O(n)` port usage) 24 | * Audio level indication (RFC6464). "X is speaking" 25 | 26 | ## End to end solutions 27 | 28 | ion-sfu is the engine behind several projects. It's designed to be focused, with minimal signaling or external dependencies. It's simple to embed ion-sfu within your service: we include a few examples inside `cmd/signal`. 29 | 30 | For "batteries-included", end-to-end solutions that are easier to deploy, check out: 31 | 32 | * [LiveKit](https://github.com/livekit/livekit-server): Open source platform for real-time communication (SDKs for all major platforms) 33 | * [Ion](https://github.com/pion/ion): Real-Distributed RTC System by pure Go and Flutter 34 | 35 | ## Quickstart 36 | 37 | Run the Echo Test example 38 | 39 | ``` 40 | docker-compose -f examples/echotest-jsonrpc/docker-compose.yaml up 41 | ``` 42 | 43 | Open the client 44 | ``` 45 | http://localhost:8000/ 46 | ``` 47 | 48 | ### SFU with json-rpc signaling 49 | 50 | The json-rpc signaling service can be used to easily get up and running with the sfu. It can be used with the [corresponding javascript signaling module](https://github.com/pion/ion-sdk-js/blob/master/src/signal/json-rpc-impl.ts). 51 | 52 | ##### Using golang environment 53 | 54 | ``` 55 | go build ./cmd/signal/json-rpc/main.go && ./main -c config.toml 56 | ``` 57 | 58 | ##### Using docker 59 | 60 | ``` 61 | docker run -p 7000:7000 -p 5000-5200:5000-5200/udp pionwebrtc/ion-sfu:latest-jsonrpc 62 | ``` 63 | 64 | ### SFU with gRPC signaling 65 | 66 | For service-to-service communication, you can use the grpc interface. A common pattern is to call the grpc endpoints from a custom signaling service. 67 | 68 | ##### Using golang environment 69 | 70 | ``` 71 | go build ./cmd/signal/grpc/main.go && ./main -c config.toml 72 | ``` 73 | 74 | ##### Using docker 75 | 76 | ``` 77 | docker run -p 50051:50051 -p 5000-5200:5000-5200/udp pionwebrtc/ion-sfu:latest-grpc 78 | ``` 79 | 80 | ## Documentation 81 | 82 | Answers to some [Frequenty Asked Questions](FAQ.md). 83 | 84 | ## Examples 85 | 86 | To see some other ways of interacting with the ion-sfu instance, check out our [examples](examples). 87 | 88 | ## Media Processing 89 | 90 | `ion-sfu` supports real-time processing on media streamed through the sfu using [`ion-avp`](https://github.com/pion/ion-avp). 91 | 92 | For an example of recording a MediaStream to webm, checkout the [save-to-webm](https://github.com/pion/ion-avp/tree/master/examples/save-to-webm) example. 93 | 94 | ### License 95 | 96 | MIT License - see [LICENSE](LICENSE) for full text 97 | 98 | ## Development 99 | 100 | Generate the protocol buffers and grpc code: 101 | 102 | 1. Best choice (uses docker): `make protos`. 103 | 2. Manually: 104 | - Install protocol buffers and the protcol buffers compiler. On Fedora `dnf install protobuf protobuf-compiler`. 105 | - `go get google.golang.org/grpc/cmd/protoc-gen-go-grpc` 106 | - `go get google.golang.org/protobuf/cmd/protoc-gen-go` 107 | - `protoc --go_out=. --go-grpc_out=. --go_opt=paths=source_relative --go-grpc_opt=paths=source_relative cmd/signal/grpc/proto/sfu.proto` 108 | -------------------------------------------------------------------------------- /cmd/signal/allrpc/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:stretch 2 | 3 | WORKDIR $GOPATH/src/github.com/pion/ion-sfu 4 | 5 | COPY go.mod go.sum ./ 6 | RUN cd $GOPATH/src/github.com/pion/ion-sfu && go mod download 7 | 8 | COPY pkg/ $GOPATH/src/github.com/pion/ion-sfu/pkg 9 | COPY cmd/ $GOPATH/src/github.com/pion/ion-sfu/cmd 10 | 11 | WORKDIR $GOPATH/src/github.com/pion/ion-sfu/cmd/signal/allrpc 12 | RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o /sfu . 13 | 14 | FROM alpine:3.12.3 15 | 16 | RUN apk --no-cache add ca-certificates 17 | COPY --from=0 /sfu /usr/local/bin/sfu 18 | 19 | COPY config.toml /configs/sfu.toml 20 | 21 | ENTRYPOINT ["/usr/local/bin/sfu"] 22 | CMD ["-c", "/configs/sfu.toml"] 23 | -------------------------------------------------------------------------------- /cmd/signal/allrpc/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | "os" 7 | 8 | "github.com/pion/ion-sfu/cmd/signal/allrpc/server" 9 | log "github.com/pion/ion-sfu/pkg/logger" 10 | "github.com/pion/ion-sfu/pkg/sfu" 11 | "github.com/spf13/viper" 12 | ) 13 | 14 | var ( 15 | file string 16 | cert string 17 | key string 18 | gaddr, jaddr, paddr, maddr string 19 | verbosityLevel int 20 | logger = log.New() 21 | ) 22 | 23 | // Config defines parameters for configuring the sfu instance 24 | type Config struct { 25 | sfu.Config `mapstructure:",squash"` 26 | LogConfig log.GlobalConfig `mapstructure:"log"` 27 | } 28 | 29 | var ( 30 | conf = Config{} 31 | ) 32 | 33 | func showHelp() { 34 | fmt.Printf("Usage:%s {params}\n", os.Args[0]) 35 | fmt.Println(" -c {config file}") 36 | fmt.Println(" -cert {cert file used by jsonrpc}") 37 | fmt.Println(" -key {key file used by jsonrpc}") 38 | fmt.Println(" -gaddr {grpc listen addr}") 39 | fmt.Println(" -jaddr {jsonrpc listen addr}") 40 | fmt.Println(" -paddr {pprof listen addr}") 41 | fmt.Println(" {grpc and jsonrpc addrs should be set at least one}") 42 | fmt.Println(" -maddr {metrics listen addr}") 43 | fmt.Println(" -h (show help info)") 44 | fmt.Println(" -v {0-10} (verbosity level, default 0)") 45 | } 46 | 47 | func load() bool { 48 | _, err := os.Stat(file) 49 | if err != nil { 50 | return false 51 | } 52 | 53 | viper.SetConfigFile(file) 54 | viper.SetConfigType("toml") 55 | 56 | err = viper.ReadInConfig() 57 | if err != nil { 58 | logger.Error(err, "config file read failed", "file", file) 59 | return false 60 | } 61 | err = viper.GetViper().Unmarshal(&conf) 62 | if err != nil { 63 | logger.Error(err, "sfu config file loaded failed", "file", file) 64 | return false 65 | } 66 | 67 | if len(conf.WebRTC.ICEPortRange) > 2 { 68 | logger.Error(nil, "config file loaded failed. webrtc range port must be [min,max]", "file", file) 69 | return false 70 | } 71 | 72 | if len(conf.Turn.PortRange) > 2 { 73 | logger.Error(nil, "config file loaded failed. turn port must be [min,max]", "file", file) 74 | return false 75 | } 76 | 77 | if conf.LogConfig.V < 0 { 78 | logger.Error(nil, "Logger V-Level cannot be less than 0") 79 | return false 80 | } 81 | 82 | logger.Info("Config file loaded", "file", file) 83 | return true 84 | } 85 | 86 | func parse() bool { 87 | flag.StringVar(&file, "c", "config.toml", "config file") 88 | flag.StringVar(&cert, "cert", "", "cert file") 89 | flag.StringVar(&key, "key", "", "key file") 90 | flag.StringVar(&jaddr, "jaddr", "", "jsonrpc listening address") 91 | flag.StringVar(&gaddr, "gaddr", "", "grpc listening address") 92 | flag.StringVar(&paddr, "paddr", "", "pprof listening address") 93 | flag.StringVar(&maddr, "maddr", "", "metrics listening address") 94 | flag.IntVar(&verbosityLevel, "v", -1, "verbosity level, higher value - more logs") 95 | help := flag.Bool("h", false, "help info") 96 | flag.Parse() 97 | 98 | if gaddr == "" { 99 | gaddr = getEnv("gaddr") 100 | } 101 | 102 | if jaddr == "" { 103 | jaddr = getEnv("jaddr") 104 | } 105 | 106 | if paddr == "" { 107 | paddr = getEnv("paddr") 108 | } 109 | 110 | if maddr == "" { 111 | maddr = getEnv("maddr") 112 | } 113 | 114 | // at least set one 115 | if gaddr == "" && jaddr == "" { 116 | return false 117 | } 118 | 119 | if !load() { 120 | return false 121 | } 122 | 123 | if *help { 124 | return false 125 | } 126 | return true 127 | } 128 | 129 | func getEnv(key string) string { 130 | if value, exists := os.LookupEnv(key); exists { 131 | return value 132 | } 133 | 134 | return "" 135 | } 136 | 137 | func main() { 138 | if !parse() { 139 | showHelp() 140 | os.Exit(-1) 141 | } 142 | // Check that the -v is not set (default -1) 143 | if verbosityLevel < 0 { 144 | verbosityLevel = conf.LogConfig.V 145 | } 146 | 147 | log.SetGlobalOptions(log.GlobalConfig{V: verbosityLevel}) 148 | 149 | node := server.New(conf.Config, logger) 150 | 151 | if gaddr != "" { 152 | go node.ServeGRPC(gaddr, cert, key) 153 | } 154 | 155 | if jaddr != "" { 156 | go node.ServeJSONRPC(jaddr, cert, key) 157 | } 158 | 159 | if paddr != "" { 160 | go node.ServePProf(paddr) 161 | } 162 | 163 | if maddr != "" { 164 | go node.ServeMetrics(maddr) 165 | } 166 | 167 | select {} 168 | } 169 | -------------------------------------------------------------------------------- /cmd/signal/allrpc/server/server.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "net" 5 | "net/http" 6 | 7 | "github.com/go-logr/logr" 8 | "github.com/pion/ion-sfu/pkg/middlewares/datachannel" 9 | 10 | "github.com/pion/ion-sfu/cmd/signal/grpc/server" 11 | jsonrpcServer "github.com/pion/ion-sfu/cmd/signal/json-rpc/server" 12 | "github.com/pion/ion-sfu/pkg/sfu" 13 | "github.com/prometheus/client_golang/prometheus/promhttp" 14 | 15 | // pprof 16 | _ "net/http/pprof" 17 | 18 | "github.com/gorilla/websocket" 19 | "github.com/sourcegraph/jsonrpc2" 20 | websocketjsonrpc2 "github.com/sourcegraph/jsonrpc2/websocket" 21 | ) 22 | 23 | type Server struct { 24 | sfu *sfu.SFU 25 | logger logr.Logger 26 | } 27 | 28 | // New create a server which support grpc/jsonrpc 29 | func New(c sfu.Config, logger logr.Logger) *Server { // Register default middlewares 30 | s := sfu.NewSFU(c) 31 | sfu.Logger = logger 32 | dc := s.NewDatachannel(sfu.APIChannelLabel) 33 | dc.Use(datachannel.SubscriberAPI) 34 | return &Server{ 35 | sfu: s, 36 | logger: logger, 37 | } 38 | } 39 | 40 | // ServeGRPC serve grpc 41 | func (s *Server) ServeGRPC(gaddr, cert, key string) error { 42 | return server.WrapperedGRPCWebServe(s.sfu, gaddr, cert, key) 43 | } 44 | 45 | // ServeJSONRPC serve jsonrpc 46 | func (s *Server) ServeJSONRPC(jaddr, cert, key string) error { 47 | upgrader := websocket.Upgrader{ 48 | CheckOrigin: func(r *http.Request) bool { 49 | return true 50 | }, 51 | ReadBufferSize: 1024, 52 | WriteBufferSize: 1024, 53 | } 54 | 55 | http.Handle("/ws", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 56 | c, err := upgrader.Upgrade(w, r, nil) 57 | if err != nil { 58 | panic(err) 59 | } 60 | defer c.Close() 61 | 62 | p := jsonrpcServer.NewJSONSignal(sfu.NewPeer(s.sfu), s.logger) 63 | defer p.Close() 64 | 65 | jc := jsonrpc2.NewConn(r.Context(), websocketjsonrpc2.NewObjectStream(c), p) 66 | <-jc.DisconnectNotify() 67 | })) 68 | 69 | var err error 70 | if key != "" && cert != "" { 71 | s.logger.Info("JsonRPC Listening", "addr", "https://"+jaddr) 72 | err = http.ListenAndServeTLS(jaddr, cert, key, nil) 73 | } else { 74 | s.logger.Info("JsonRPC Listening", "addr", "http://"+jaddr) 75 | err = http.ListenAndServe(jaddr, nil) 76 | } 77 | if err != nil { 78 | s.logger.Error(err, "JsonRPC starting error") 79 | } 80 | return err 81 | } 82 | 83 | // ServePProf 84 | func (s *Server) ServePProf(paddr string) { 85 | s.logger.Info("PProf Listening", "addr", paddr) 86 | http.ListenAndServe(paddr, nil) 87 | } 88 | 89 | // ServeMetrics 90 | func (s *Server) ServeMetrics(maddr string) { 91 | // start metrics server 92 | m := http.NewServeMux() 93 | m.Handle("/metrics", promhttp.Handler()) 94 | srv := &http.Server{ 95 | Handler: m, 96 | } 97 | 98 | metricsLis, err := net.Listen("tcp", maddr) 99 | if err != nil { 100 | s.logger.Error(err, "Cannot bind to metrics endpoint", "addr", maddr) 101 | } 102 | s.logger.Info("Metrics Listening", "addr", "http://"+maddr) 103 | 104 | err = srv.Serve(metricsLis) 105 | if err != nil { 106 | s.logger.Error(err, "Metrics server stopped with error") 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /cmd/signal/grpc/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:stretch 2 | 3 | WORKDIR $GOPATH/src/github.com/pion/ion-sfu 4 | 5 | COPY go.mod go.sum ./ 6 | RUN cd $GOPATH/src/github.com/pion/ion-sfu && go mod download 7 | 8 | COPY pkg/ $GOPATH/src/github.com/pion/ion-sfu/pkg 9 | COPY cmd/ $GOPATH/src/github.com/pion/ion-sfu/cmd 10 | 11 | WORKDIR $GOPATH/src/github.com/pion/ion-sfu/cmd/signal/grpc 12 | RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o /sfu . 13 | 14 | FROM alpine:3.12.3 15 | 16 | RUN apk --no-cache add ca-certificates 17 | COPY --from=0 /sfu /usr/local/bin/sfu 18 | 19 | COPY config.toml /configs/sfu.toml 20 | 21 | ENTRYPOINT ["/usr/local/bin/sfu"] 22 | CMD ["-c", "/configs/sfu.toml"] 23 | -------------------------------------------------------------------------------- /cmd/signal/grpc/README.md: -------------------------------------------------------------------------------- 1 | # grpc 2 | 3 | `ion-sfu` supports a `grpc` interface for connecting peers to the sfu 4 | 5 | ## Quick Start 6 | ``` 7 | go build cmd/signal/grpc/main.go 8 | ./main -c config.toml 9 | ``` 10 | -------------------------------------------------------------------------------- /cmd/signal/grpc/main.go: -------------------------------------------------------------------------------- 1 | // Package cmd contains an entrypoint for running an ion-sfu instance. 2 | package main 3 | 4 | import ( 5 | "flag" 6 | "fmt" 7 | "net" 8 | "net/http" 9 | _ "net/http/pprof" 10 | "os" 11 | 12 | "github.com/pion/ion-sfu/cmd/signal/grpc/server" 13 | "github.com/pion/ion-sfu/pkg/middlewares/datachannel" 14 | 15 | log "github.com/pion/ion-sfu/pkg/logger" 16 | "github.com/pion/ion-sfu/pkg/sfu" 17 | "github.com/prometheus/client_golang/prometheus/promhttp" 18 | "github.com/spf13/viper" 19 | ) 20 | 21 | type grpcConfig struct { 22 | Port string `mapstructure:"port"` 23 | } 24 | 25 | // Config defines parameters for configuring the sfu instance 26 | type Config struct { 27 | sfu.Config `mapstructure:",squash"` 28 | GRPC grpcConfig `mapstructure:"grpc"` 29 | LogConfig log.GlobalConfig `mapstructure:"log"` 30 | } 31 | 32 | var ( 33 | conf = Config{} 34 | file string 35 | addr string 36 | metricsAddr string 37 | verbosityLevel int 38 | paddr string 39 | 40 | enableTLS bool 41 | cert string 42 | key string 43 | 44 | logger = log.New() 45 | ) 46 | 47 | const ( 48 | portRangeLimit = 100 49 | ) 50 | 51 | func showHelp() { 52 | fmt.Printf("Usage:%s {params}\n", os.Args[0]) 53 | fmt.Println(" -c {config file}") 54 | fmt.Println(" -a {listen addr}") 55 | fmt.Println(" -tls (enable tls)") 56 | fmt.Println(" -cert {cert file}") 57 | fmt.Println(" -key {key file}") 58 | fmt.Println(" -h (show help info)") 59 | fmt.Println(" -v {0-10} (verbosity level, default 0)") 60 | fmt.Println(" -paddr {pprof listen addr}") 61 | 62 | } 63 | 64 | func load() bool { 65 | _, err := os.Stat(file) 66 | if err != nil { 67 | return false 68 | } 69 | 70 | viper.SetConfigFile(file) 71 | viper.SetConfigType("toml") 72 | 73 | err = viper.ReadInConfig() 74 | if err != nil { 75 | logger.Error(err, "config file read failed", "file", file) 76 | return false 77 | } 78 | err = viper.GetViper().Unmarshal(&conf) 79 | if err != nil { 80 | logger.Error(err, "sfu config file loaded failed", "file", file) 81 | return false 82 | } 83 | 84 | if len(conf.WebRTC.ICEPortRange) > 2 { 85 | logger.Error(nil, "config file loaded failed. webrtc port must be [min,max]", "file", file) 86 | return false 87 | } 88 | 89 | if len(conf.WebRTC.ICEPortRange) != 0 && conf.WebRTC.ICEPortRange[1]-conf.WebRTC.ICEPortRange[0] < portRangeLimit { 90 | logger.Error(nil, "config file loaded failed. webrtc port must be [min, max] and max - min >= portRangeLimit", "file", file, "portRangeLimit", portRangeLimit) 91 | return false 92 | } 93 | 94 | if len(conf.Turn.PortRange) > 2 { 95 | logger.Error(nil, "config file loaded failed. turn port must be [min,max]", "file", file) 96 | return false 97 | } 98 | 99 | logger.V(0).Info("Config file loaded", "file", file) 100 | return true 101 | } 102 | 103 | func parse() bool { 104 | flag.StringVar(&file, "c", "config.toml", "config file") 105 | flag.StringVar(&addr, "a", ":50051", "address to use") 106 | flag.BoolVar(&enableTLS, "tls", false, "enable tls") 107 | flag.StringVar(&cert, "cert", "", "cert file") 108 | flag.StringVar(&key, "key", "", "key file") 109 | flag.StringVar(&metricsAddr, "m", ":8100", "merics to use") 110 | flag.IntVar(&verbosityLevel, "v", -1, "verbosity level, higher value - more logs") 111 | flag.StringVar(&paddr, "paddr", "", "pprof listening address") 112 | help := flag.Bool("h", false, "help info") 113 | flag.Parse() 114 | 115 | if paddr == "" { 116 | paddr = getEnv("paddr") 117 | } 118 | 119 | if !load() { 120 | return false 121 | } 122 | 123 | if *help { 124 | return false 125 | } 126 | return true 127 | } 128 | 129 | func getEnv(key string) string { 130 | if value, exists := os.LookupEnv(key); exists { 131 | return value 132 | } 133 | 134 | return "" 135 | } 136 | 137 | func startMetrics(addr string) { 138 | // start metrics server 139 | m := http.NewServeMux() 140 | m.Handle("/metrics", promhttp.Handler()) 141 | srv := &http.Server{ 142 | Handler: m, 143 | } 144 | 145 | metricsLis, err := net.Listen("tcp", addr) 146 | if err != nil { 147 | logger.Error(err, "cannot bind to metrics endpoint", "addr", addr) 148 | os.Exit(1) 149 | } 150 | logger.Info("Metrics Listening starter", "addr", addr) 151 | 152 | err = srv.Serve(metricsLis) 153 | if err != nil { 154 | logger.Error(err, "debug server stopped. got err: %s") 155 | } 156 | } 157 | 158 | func main() { 159 | if !parse() { 160 | showHelp() 161 | os.Exit(-1) 162 | } 163 | 164 | // Check that the -v is not set (default -1) 165 | if verbosityLevel < 0 { 166 | verbosityLevel = conf.LogConfig.V 167 | } 168 | 169 | log.SetGlobalOptions(log.GlobalConfig{V: verbosityLevel}) 170 | logger := log.New() 171 | 172 | logger.Info("--- Starting SFU Node ---") 173 | 174 | if paddr != "" { 175 | go func() { 176 | logger.Info("PProf Listening", "addr", paddr) 177 | _ = http.ListenAndServe(paddr, http.DefaultServeMux) 178 | }() 179 | } 180 | go startMetrics(metricsAddr) 181 | 182 | // SFU instance needs to be created with logr implementation 183 | sfu.Logger = logger 184 | 185 | nsfu := sfu.NewSFU(conf.Config) 186 | dc := nsfu.NewDatachannel(sfu.APIChannelLabel) 187 | dc.Use(datachannel.SubscriberAPI) 188 | 189 | err := server.WrapperedGRPCWebServe(nsfu, addr, cert, key) 190 | if err != nil { 191 | logger.Error(err, "failed to serve SFU") 192 | os.Exit(1) 193 | } 194 | } 195 | -------------------------------------------------------------------------------- /cmd/signal/grpc/server/wrapped.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "crypto/tls" 5 | "net" 6 | "net/http" 7 | "time" 8 | 9 | grpc_prometheus "github.com/grpc-ecosystem/go-grpc-prometheus" 10 | "github.com/improbable-eng/grpc-web/go/grpcweb" 11 | log "github.com/pion/ion-log" 12 | "github.com/pion/ion-sfu/pkg/sfu" 13 | rtc "github.com/pion/ion/proto/rtc" 14 | "github.com/soheilhy/cmux" 15 | "golang.org/x/sync/errgroup" 16 | "google.golang.org/grpc" 17 | "google.golang.org/grpc/health" 18 | "google.golang.org/grpc/health/grpc_health_v1" 19 | ) 20 | 21 | type WrapperedServerOptions struct { 22 | Addr string 23 | Cert string 24 | Key string 25 | AllowAllOrigins bool 26 | AllowedOrigins *[]string 27 | AllowedHeaders *[]string 28 | UseWebSocket bool 29 | WebsocketPingInterval time.Duration 30 | } 31 | 32 | func DefaultWrapperedServerOptions() WrapperedServerOptions { 33 | return WrapperedServerOptions{ 34 | Addr: ":9090", 35 | Cert: "", 36 | Key: "", 37 | AllowAllOrigins: true, 38 | AllowedHeaders: &[]string{}, 39 | AllowedOrigins: &[]string{}, 40 | UseWebSocket: true, 41 | WebsocketPingInterval: 0, 42 | } 43 | } 44 | 45 | func NewWrapperedServerOptions(addr, cert, key string, websocket bool) WrapperedServerOptions { 46 | return WrapperedServerOptions{ 47 | Addr: addr, 48 | Cert: cert, 49 | Key: key, 50 | AllowAllOrigins: true, 51 | AllowedHeaders: &[]string{}, 52 | AllowedOrigins: &[]string{}, 53 | UseWebSocket: true, 54 | WebsocketPingInterval: 0, 55 | } 56 | } 57 | 58 | type WrapperedGRPCWebServer struct { 59 | options WrapperedServerOptions 60 | GRPCServer *grpc.Server 61 | } 62 | 63 | func NewWrapperedGRPCWebServer(options WrapperedServerOptions, s *grpc.Server) *WrapperedGRPCWebServer { 64 | return &WrapperedGRPCWebServer{ 65 | options: options, 66 | GRPCServer: s, 67 | } 68 | } 69 | 70 | type allowedOrigins struct { 71 | origins map[string]struct{} 72 | } 73 | 74 | func (a *allowedOrigins) IsAllowed(origin string) bool { 75 | _, ok := a.origins[origin] 76 | return ok 77 | } 78 | 79 | func makeAllowedOrigins(origins []string) *allowedOrigins { 80 | o := map[string]struct{}{} 81 | for _, allowedOrigin := range origins { 82 | o[allowedOrigin] = struct{}{} 83 | } 84 | return &allowedOrigins{ 85 | origins: o, 86 | } 87 | } 88 | 89 | func (s *WrapperedGRPCWebServer) makeHTTPOriginFunc(allowedOrigins *allowedOrigins) func(origin string) bool { 90 | if s.options.AllowAllOrigins { 91 | return func(origin string) bool { 92 | return true 93 | } 94 | } 95 | return allowedOrigins.IsAllowed 96 | } 97 | 98 | func (s *WrapperedGRPCWebServer) makeWebsocketOriginFunc(allowedOrigins *allowedOrigins) func(req *http.Request) bool { 99 | if s.options.AllowAllOrigins { 100 | return func(req *http.Request) bool { 101 | return true 102 | } 103 | } 104 | return func(req *http.Request) bool { 105 | origin, err := grpcweb.WebsocketRequestOrigin(req) 106 | if err != nil { 107 | log.Warnf("%v", err) 108 | return false 109 | } 110 | return allowedOrigins.IsAllowed(origin) 111 | } 112 | } 113 | 114 | func (s *WrapperedGRPCWebServer) Serve() error { 115 | addr := s.options.Addr 116 | 117 | if s.options.AllowAllOrigins && s.options.AllowedOrigins != nil && len(*s.options.AllowedOrigins) != 0 { 118 | log.Errorf("Ambiguous --allow_all_origins and --allow_origins configuration. Either set --allow_all_origins=true OR specify one or more origins to whitelist with --allow_origins, not both.") 119 | } 120 | 121 | allowedOrigins := makeAllowedOrigins(*s.options.AllowedOrigins) 122 | 123 | options := []grpcweb.Option{ 124 | grpcweb.WithCorsForRegisteredEndpointsOnly(false), 125 | grpcweb.WithOriginFunc(s.makeHTTPOriginFunc(allowedOrigins)), 126 | } 127 | 128 | if s.options.UseWebSocket { 129 | log.Infof("Using websockets") 130 | options = append( 131 | options, 132 | grpcweb.WithWebsockets(true), 133 | grpcweb.WithWebsocketOriginFunc(s.makeWebsocketOriginFunc(allowedOrigins)), 134 | ) 135 | 136 | if s.options.WebsocketPingInterval >= time.Second { 137 | log.Infof("websocket keepalive pinging enabled, the timeout interval is %s", s.options.WebsocketPingInterval.String()) 138 | options = append( 139 | options, 140 | grpcweb.WithWebsocketPingInterval(s.options.WebsocketPingInterval), 141 | ) 142 | } 143 | } 144 | 145 | if s.options.AllowedHeaders != nil && len(*s.options.AllowedHeaders) > 0 { 146 | options = append( 147 | options, 148 | grpcweb.WithAllowedRequestHeaders(*s.options.AllowedHeaders), 149 | ) 150 | } 151 | 152 | wrappedServer := grpcweb.WrapServer(s.GRPCServer, options...) 153 | handler := func(resp http.ResponseWriter, req *http.Request) { 154 | wrappedServer.ServeHTTP(resp, req) 155 | } 156 | 157 | httpServer := http.Server{ 158 | Addr: addr, 159 | Handler: http.HandlerFunc(handler), 160 | } 161 | 162 | var listener net.Listener 163 | 164 | enableTLS := s.options.Cert != "" && s.options.Key != "" 165 | 166 | if enableTLS { 167 | cer, err := tls.LoadX509KeyPair(s.options.Cert, s.options.Key) 168 | if err != nil { 169 | log.Panicf("failed to load x509 key pair: %v", err) 170 | return err 171 | } 172 | config := &tls.Config{Certificates: []tls.Certificate{cer}} 173 | tls, err := tls.Listen("tcp", addr, config) 174 | if err != nil { 175 | log.Panicf("failed to listen: tls %v", err) 176 | return err 177 | } 178 | listener = tls 179 | } else { 180 | tcp, err := net.Listen("tcp", addr) 181 | if err != nil { 182 | log.Panicf("failed to listen: tcp %v", err) 183 | return err 184 | } 185 | listener = tcp 186 | } 187 | 188 | log.Infof("Starting gRPC/gRPC-Web combo server, bind: %s, with TLS: %v", addr, enableTLS) 189 | 190 | m := cmux.New(listener) 191 | grpcListener := m.Match(cmux.HTTP2()) 192 | httpListener := m.Match(cmux.HTTP1Fast()) 193 | g := new(errgroup.Group) 194 | g.Go(func() error { return s.GRPCServer.Serve(grpcListener) }) 195 | g.Go(func() error { return httpServer.Serve(httpListener) }) 196 | g.Go(m.Serve) 197 | log.Infof("Run server: %v", g.Wait()) 198 | return nil 199 | } 200 | 201 | func WrapperedGRPCWebServe(sfu *sfu.SFU, addr, cert, key string) error { 202 | grpcServer := grpc.NewServer( 203 | grpc.StreamInterceptor(grpc_prometheus.StreamServerInterceptor), 204 | ) 205 | 206 | rtc.RegisterRTCServer(grpcServer, NewSFUServer(sfu)) 207 | grpc_health_v1.RegisterHealthServer(grpcServer, health.NewServer()) 208 | grpc_prometheus.Register(grpcServer) 209 | 210 | log.Infof("wrappered grpc listening %v", addr) 211 | options := NewWrapperedServerOptions(addr, cert, key, true) 212 | wrapperedSrv := NewWrapperedGRPCWebServer(options, grpcServer) 213 | if err := wrapperedSrv.Serve(); err != nil { 214 | log.Errorf("wrappered grpc listening error: %v", err) 215 | return err 216 | } 217 | return nil 218 | } 219 | -------------------------------------------------------------------------------- /cmd/signal/json-rpc/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:stretch 2 | 3 | WORKDIR $GOPATH/src/github.com/pion/ion-sfu 4 | 5 | COPY go.mod go.sum ./ 6 | RUN cd $GOPATH/src/github.com/pion/ion-sfu && go mod download 7 | 8 | COPY pkg/ $GOPATH/src/github.com/pion/ion-sfu/pkg 9 | COPY cmd/ $GOPATH/src/github.com/pion/ion-sfu/cmd 10 | 11 | WORKDIR $GOPATH/src/github.com/pion/ion-sfu/cmd/signal/json-rpc 12 | RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o /sfu . 13 | 14 | FROM alpine:3.12.3 15 | 16 | RUN apk --no-cache add ca-certificates 17 | COPY --from=0 /sfu /usr/local/bin/sfu 18 | 19 | COPY config.toml /configs/sfu.toml 20 | 21 | ENTRYPOINT ["/usr/local/bin/sfu"] 22 | CMD ["-c", "/configs/sfu.toml"] 23 | -------------------------------------------------------------------------------- /cmd/signal/json-rpc/README.md: -------------------------------------------------------------------------------- 1 | # json-rpc 2 | 3 | `ion-sfu` supports a `json-rpc` interface for connecting peers to the sfu 4 | 5 | ## Quick Start 6 | ### Serving over http 7 | ``` 8 | go build cmd/signal/json-rpc/main.go 9 | ./main -c config.toml -a ":7000" 10 | ``` 11 | 12 | ### Serving over `https` 13 | Generate a keypair and run: 14 | ``` 15 | go build cmd/signal/json-rpc/main.go 16 | ./main -c config.toml -key ./key.pem -cert ./cert.pem -a "0.0.0.0:10000" 17 | ``` 18 | 19 | ## API 20 | 21 | ### Join 22 | Initialize a peer connection and join a session. 23 | ```json 24 | { 25 | "sid": "defaultroom", 26 | "offer": { 27 | "type": "offer", 28 | "sdp": "..." 29 | } 30 | } 31 | ``` 32 | 33 | ### Offer 34 | Offer a new sdp to the sfu. Called to renegotiate the peer connection, typically when tracks are added/removed. 35 | ```json 36 | { 37 | "desc": { 38 | "type": "offer", 39 | "sdp": "..." 40 | } 41 | } 42 | ``` 43 | 44 | 45 | ### Answer 46 | Answer a remote offer from the sfu. Called in response to a remote renegotiation. Typically when new tracks are added to/removed from other peers. 47 | ```json 48 | { 49 | "desc": { 50 | "type": "answer", 51 | "sdp": "..." 52 | } 53 | } 54 | ``` 55 | 56 | ### Trickle 57 | Provide an ICE candidate. 58 | ```json 59 | { 60 | "candidate": "..." 61 | } 62 | ``` 63 | -------------------------------------------------------------------------------- /cmd/signal/json-rpc/main.go: -------------------------------------------------------------------------------- 1 | // Package cmd contains an entrypoint for running an ion-sfu instance. 2 | package main 3 | 4 | import ( 5 | "flag" 6 | "fmt" 7 | "net" 8 | "net/http" 9 | _ "net/http/pprof" 10 | "os" 11 | 12 | "github.com/gorilla/websocket" 13 | "github.com/pion/ion-sfu/cmd/signal/json-rpc/server" 14 | log "github.com/pion/ion-sfu/pkg/logger" 15 | "github.com/pion/ion-sfu/pkg/middlewares/datachannel" 16 | "github.com/pion/ion-sfu/pkg/sfu" 17 | "github.com/prometheus/client_golang/prometheus/promhttp" 18 | "github.com/sourcegraph/jsonrpc2" 19 | websocketjsonrpc2 "github.com/sourcegraph/jsonrpc2/websocket" 20 | "github.com/spf13/viper" 21 | ) 22 | 23 | // logC need to get logger options from config 24 | type logC struct { 25 | Config log.GlobalConfig `mapstructure:"log"` 26 | } 27 | 28 | var ( 29 | conf = sfu.Config{} 30 | file string 31 | cert string 32 | key string 33 | addr string 34 | metricsAddr string 35 | verbosityLevel int 36 | logConfig logC 37 | logger = log.New() 38 | ) 39 | 40 | const ( 41 | portRangeLimit = 100 42 | ) 43 | 44 | func showHelp() { 45 | fmt.Printf("Usage:%s {params}\n", os.Args[0]) 46 | fmt.Println(" -c {config file}") 47 | fmt.Println(" -cert {cert file}") 48 | fmt.Println(" -key {key file}") 49 | fmt.Println(" -a {listen addr}") 50 | fmt.Println(" -h (show help info)") 51 | fmt.Println(" -v {0-10} (verbosity level, default 0)") 52 | } 53 | 54 | func load() bool { 55 | _, err := os.Stat(file) 56 | if err != nil { 57 | return false 58 | } 59 | 60 | viper.SetConfigFile(file) 61 | viper.SetConfigType("toml") 62 | 63 | err = viper.ReadInConfig() 64 | if err != nil { 65 | logger.Error(err, "config file read failed", "file", file) 66 | return false 67 | } 68 | err = viper.GetViper().Unmarshal(&conf) 69 | if err != nil { 70 | logger.Error(err, "sfu config file loaded failed", "file", file) 71 | return false 72 | } 73 | 74 | if len(conf.WebRTC.ICEPortRange) > 2 { 75 | logger.Error(nil, "config file loaded failed. webrtc port must be [min,max]", "file", file) 76 | return false 77 | } 78 | 79 | if len(conf.WebRTC.ICEPortRange) != 0 && conf.WebRTC.ICEPortRange[1]-conf.WebRTC.ICEPortRange[0] < portRangeLimit { 80 | logger.Error(nil, "config file loaded failed. webrtc port must be [min, max] and max - min >= portRangeLimit", "file", file, "portRangeLimit", portRangeLimit) 81 | return false 82 | } 83 | 84 | if len(conf.Turn.PortRange) > 2 { 85 | logger.Error(nil, "config file loaded failed. turn port must be [min,max]", "file", file) 86 | return false 87 | } 88 | 89 | if logConfig.Config.V < 0 { 90 | logger.Error(nil, "Logger V-Level cannot be less than 0") 91 | return false 92 | } 93 | 94 | logger.V(0).Info("Config file loaded", "file", file) 95 | return true 96 | } 97 | 98 | func parse() bool { 99 | flag.StringVar(&file, "c", "config.toml", "config file") 100 | flag.StringVar(&cert, "cert", "", "cert file") 101 | flag.StringVar(&key, "key", "", "key file") 102 | flag.StringVar(&addr, "a", ":7000", "address to use") 103 | flag.StringVar(&metricsAddr, "m", ":8100", "merics to use") 104 | flag.IntVar(&verbosityLevel, "v", -1, "verbosity level, higher value - more logs") 105 | help := flag.Bool("h", false, "help info") 106 | flag.Parse() 107 | if !load() { 108 | return false 109 | } 110 | 111 | if *help { 112 | return false 113 | } 114 | return true 115 | } 116 | 117 | func startMetrics(addr string) { 118 | // start metrics server 119 | m := http.NewServeMux() 120 | m.Handle("/metrics", promhttp.Handler()) 121 | srv := &http.Server{ 122 | Handler: m, 123 | } 124 | 125 | metricsLis, err := net.Listen("tcp", addr) 126 | if err != nil { 127 | logger.Error(err, "cannot bind to metrics endpoint", "addr", addr) 128 | os.Exit(1) 129 | } 130 | logger.Info("Metrics Listening", "addr", addr) 131 | 132 | err = srv.Serve(metricsLis) 133 | if err != nil { 134 | logger.Error(err, "Metrics server stopped") 135 | } 136 | } 137 | 138 | func main() { 139 | 140 | if !parse() { 141 | showHelp() 142 | os.Exit(-1) 143 | } 144 | 145 | // Check that the -v is not set (default -1) 146 | if verbosityLevel < 0 { 147 | verbosityLevel = logConfig.Config.V 148 | } 149 | 150 | log.SetGlobalOptions(log.GlobalConfig{V: verbosityLevel}) 151 | logger.Info("--- Starting SFU Node ---") 152 | 153 | // Pass logr instance 154 | sfu.Logger = logger 155 | s := sfu.NewSFU(conf) 156 | dc := s.NewDatachannel(sfu.APIChannelLabel) 157 | dc.Use(datachannel.SubscriberAPI) 158 | 159 | upgrader := websocket.Upgrader{ 160 | CheckOrigin: func(r *http.Request) bool { 161 | return true 162 | }, 163 | ReadBufferSize: 1024, 164 | WriteBufferSize: 1024, 165 | } 166 | 167 | http.Handle("/ws", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 168 | c, err := upgrader.Upgrade(w, r, nil) 169 | if err != nil { 170 | panic(err) 171 | } 172 | defer c.Close() 173 | 174 | p := server.NewJSONSignal(sfu.NewPeer(s), logger) 175 | defer p.Close() 176 | 177 | jc := jsonrpc2.NewConn(r.Context(), websocketjsonrpc2.NewObjectStream(c), p) 178 | <-jc.DisconnectNotify() 179 | })) 180 | 181 | go startMetrics(metricsAddr) 182 | 183 | var err error 184 | if key != "" && cert != "" { 185 | logger.Info("Started listening", "addr", "https://"+addr) 186 | err = http.ListenAndServeTLS(addr, cert, key, nil) 187 | } else { 188 | logger.Info("Started listening", "addr", "http://"+addr) 189 | err = http.ListenAndServe(addr, nil) 190 | } 191 | if err != nil { 192 | panic(err) 193 | } 194 | } 195 | -------------------------------------------------------------------------------- /cmd/signal/json-rpc/server/server.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "fmt" 7 | 8 | "github.com/go-logr/logr" 9 | "github.com/pion/ion-sfu/pkg/sfu" 10 | "github.com/pion/webrtc/v3" 11 | "github.com/sourcegraph/jsonrpc2" 12 | ) 13 | 14 | // Join message sent when initializing a peer connection 15 | type Join struct { 16 | SID string `json:"sid"` 17 | UID string `json:"uid"` 18 | Offer webrtc.SessionDescription `json:"offer"` 19 | Config sfu.JoinConfig `json:"config"` 20 | } 21 | 22 | // Negotiation message sent when renegotiating the peer connection 23 | type Negotiation struct { 24 | Desc webrtc.SessionDescription `json:"desc"` 25 | } 26 | 27 | // Trickle message sent when renegotiating the peer connection 28 | type Trickle struct { 29 | Target int `json:"target"` 30 | Candidate webrtc.ICECandidateInit `json:"candidate"` 31 | } 32 | 33 | type JSONSignal struct { 34 | *sfu.PeerLocal 35 | logr.Logger 36 | } 37 | 38 | func NewJSONSignal(p *sfu.PeerLocal, l logr.Logger) *JSONSignal { 39 | return &JSONSignal{p, l} 40 | } 41 | 42 | // Handle incoming RPC call events like join, answer, offer and trickle 43 | func (p *JSONSignal) Handle(ctx context.Context, conn *jsonrpc2.Conn, req *jsonrpc2.Request) { 44 | replyError := func(err error) { 45 | _ = conn.ReplyWithError(ctx, req.ID, &jsonrpc2.Error{ 46 | Code: 500, 47 | Message: fmt.Sprintf("%s", err), 48 | }) 49 | } 50 | 51 | switch req.Method { 52 | case "join": 53 | var join Join 54 | err := json.Unmarshal(*req.Params, &join) 55 | if err != nil { 56 | p.Logger.Error(err, "connect: error parsing offer") 57 | replyError(err) 58 | break 59 | } 60 | 61 | p.OnOffer = func(offer *webrtc.SessionDescription) { 62 | if err := conn.Notify(ctx, "offer", offer); err != nil { 63 | p.Logger.Error(err, "error sending offer") 64 | } 65 | 66 | } 67 | p.OnIceCandidate = func(candidate *webrtc.ICECandidateInit, target int) { 68 | if err := conn.Notify(ctx, "trickle", Trickle{ 69 | Candidate: *candidate, 70 | Target: target, 71 | }); err != nil { 72 | p.Logger.Error(err, "error sending ice candidate") 73 | } 74 | } 75 | 76 | err = p.Join(join.SID, join.UID, join.Config) 77 | if err != nil { 78 | replyError(err) 79 | break 80 | } 81 | 82 | answer, err := p.Answer(join.Offer) 83 | if err != nil { 84 | replyError(err) 85 | break 86 | } 87 | 88 | _ = conn.Reply(ctx, req.ID, answer) 89 | 90 | case "offer": 91 | var negotiation Negotiation 92 | err := json.Unmarshal(*req.Params, &negotiation) 93 | if err != nil { 94 | p.Logger.Error(err, "connect: error parsing offer") 95 | replyError(err) 96 | break 97 | } 98 | 99 | answer, err := p.Answer(negotiation.Desc) 100 | if err != nil { 101 | replyError(err) 102 | break 103 | } 104 | _ = conn.Reply(ctx, req.ID, answer) 105 | 106 | case "answer": 107 | var negotiation Negotiation 108 | err := json.Unmarshal(*req.Params, &negotiation) 109 | if err != nil { 110 | p.Logger.Error(err, "connect: error parsing answer") 111 | replyError(err) 112 | break 113 | } 114 | 115 | err = p.SetRemoteDescription(negotiation.Desc) 116 | if err != nil { 117 | replyError(err) 118 | } 119 | 120 | case "trickle": 121 | var trickle Trickle 122 | err := json.Unmarshal(*req.Params, &trickle) 123 | if err != nil { 124 | p.Logger.Error(err, "connect: error parsing candidate") 125 | replyError(err) 126 | break 127 | } 128 | 129 | err = p.Trickle(trickle.Candidate, trickle.Target) 130 | if err != nil { 131 | replyError(err) 132 | } 133 | } 134 | } 135 | -------------------------------------------------------------------------------- /codecov.yml: -------------------------------------------------------------------------------- 1 | coverage: 2 | status: 3 | project: 4 | default: 5 | # Allow decreasing 2% of total coverage to avoid noise. 6 | threshold: 2% 7 | patch: off 8 | 9 | ignore: 10 | - "cmd/*" 11 | -------------------------------------------------------------------------------- /config.toml: -------------------------------------------------------------------------------- 1 | [sfu] 2 | # Ballast size in MiB, will allocate memory to reduce the GC trigger upto 2x the 3 | # size of ballast. Be aware that the ballast should be less than the half of memory 4 | # available. 5 | ballast = 0 6 | # enable prometheus sfu statistics 7 | withstats = false 8 | 9 | [router] 10 | # Limit the remb bandwidth in kbps 11 | # zero means no limits 12 | maxbandwidth = 1500 13 | # max number of video tracks packets the SFU will keep track 14 | maxpackettrack = 500 15 | # Sets the audio level volume threshold. 16 | # Values from [0-127] where 0 is the loudest. 17 | # Audio levels are read from rtp extension header according to: 18 | # https://tools.ietf.org/html/rfc6464 19 | audiolevelthreshold = 40 20 | # Sets the interval in which the SFU will check the audio level 21 | # in [ms]. If the active speaker has changed, the sfu will 22 | # emit an event to clients. 23 | audiolevelinterval=1000 24 | # Sets minimum percentage of events required to fire an audio level 25 | # according to the expected events from the audiolevelinterval, 26 | # calculated as audiolevelinterval/packetization time (20ms for 8kHz) 27 | # Values from [0-100] 28 | audiolevelfilter = 20 29 | 30 | [router.simulcast] 31 | # Prefer best quality initially 32 | bestqualityfirst = true 33 | # EXPERIMENTAL enable temporal layer change is currently an experimental feature, 34 | # enable only for testing. 35 | enabletemporallayer = false 36 | 37 | [webrtc] 38 | # Single port, portrange will not work if you enable this 39 | # singleport = 5000 40 | 41 | # Range of ports that ion accepts WebRTC traffic on 42 | # Format: [min, max] and max - min >= 100 43 | portrange = [5000, 5200] 44 | # if sfu behind nat, set iceserver 45 | # [[webrtc.iceserver]] 46 | # urls = ["stun:stun.stunprotocol.org:3478"] 47 | # [[webrtc.iceserver]] 48 | # urls = ["turn:turn.awsome.org:3478"] 49 | # username = "awsome" 50 | # credential = "awsome" 51 | 52 | # sdp semantics: 53 | # "unified-plan" 54 | # "plan-b" 55 | # "unified-plan-with-fallback" 56 | sdpsemantics = "unified-plan" 57 | # toggle multicast dns support: https://tools.ietf.org/html/draft-mdns-ice-candidates-00 58 | mdns = true 59 | 60 | [webrtc.candidates] 61 | # In case you're deploying ion-sfu on a server which is configured with 62 | # a 1:1 NAT (e.g., Amazon EC2), you might want to also specify the public 63 | # address of the machine using the setting below. This will result in 64 | # all host candidates (which normally have a private IP address) to 65 | # be rewritten with the public address provided in the settings. As 66 | # such, use the option with caution and only if you know what you're doing. 67 | # Multiple public IP addresses can be specified as a comma separated list 68 | # if the sfu is deployed in a DMZ between two 1-1 NAT for internal and 69 | # external users. 70 | # nat1to1 = ["1.2.3.4"] 71 | # icelite = true 72 | 73 | [webrtc.timeouts] 74 | # The duration in [sec] without network activity before a ICE Agent is considered disconnected 75 | disconnected = 5 76 | # The duration in [sec] without network activity before a ICE Agent is considered failed after disconnected 77 | failed = 25 78 | # How often in [sec] the ICE Agent sends extra traffic if there is no activity, if media is flowing no traffic will be sent 79 | keepalive = 2 80 | 81 | [turn] 82 | # Enables embeded turn server 83 | enabled = false 84 | # Sets the realm for turn server 85 | realm = "ion" 86 | # The address the TURN server will listen on. 87 | address = "0.0.0.0:3478" 88 | # Certs path to config tls/dtls 89 | # cert="path/to/cert.pem" 90 | # key="path/to/key.pem" 91 | # Port range that turn relays to SFU 92 | # WARNING: It shouldn't overlap webrtc.portrange 93 | # Format: [min, max] 94 | # portrange = [5201, 5400] 95 | [turn.auth] 96 | # Use an auth secret to generate long-term credentials defined in RFC5389-10.2 97 | # NOTE: This takes precedence over `credentials` if defined. 98 | # secret = "secret" 99 | # Sets the credentials pairs 100 | credentials = "pion=ion,pion2=ion2" 101 | 102 | [log] 103 | # 0 - INFO 1 - DEBUG 2 - TRACE 104 | v = 1 105 | -------------------------------------------------------------------------------- /examples/README.md: -------------------------------------------------------------------------------- 1 |

2 | Examples 3 |

4 | 5 | We've built an extensive collection of examples covering common use-cases. You can modify and extend these examples to quickly get started. 6 | 7 | For more full featured examples see the [ion-examples](https://github.com/pion/ion-examples/tree/master/ion-sfu) repository. 8 | 9 | Please feel free to extend and add additional examples! 10 | 11 | ### Overview 12 | 13 | * [echotest](echotest-jsonrpc): Demonstrates how to establish a connection to `ion-sfu` as well as publish and receive a video stream. 14 | -------------------------------------------------------------------------------- /examples/echotest-jsonrpc/README.md: -------------------------------------------------------------------------------- 1 | # Echo Test 2 | 3 | Echo test provides a simple demonstration of sfu functionality. 4 | 5 | ## Instructions 6 | 7 | ### Run docker-compose 8 | 9 | ``` 10 | docker-compose up 11 | ``` 12 | 13 | ### Navigate to demo 14 | 15 | ``` 16 | http://localhost:8000/ 17 | ``` 18 | 19 | Play around! 🚀 20 | -------------------------------------------------------------------------------- /examples/echotest-jsonrpc/docker-compose.yaml: -------------------------------------------------------------------------------- 1 | version: "3" 2 | services: 3 | pubsub: 4 | image: nginx 5 | volumes: 6 | - ./:/usr/share/nginx/html 7 | ports: 8 | - 8000:80 9 | 10 | sfu: 11 | image: pionwebrtc/ion-sfu:latest-jsonrpc 12 | ports: 13 | - "5000-5200:5000-5200/udp" 14 | - 7000:7000 15 | -------------------------------------------------------------------------------- /examples/gallerytest-jsonrpc/README.md: -------------------------------------------------------------------------------- 1 | # Gallery Test 2 | 3 | Gallery test provides a demonstration large session negotiation. 4 | 5 | ## Instructions 6 | 7 | ### Run docker-compose 8 | 9 | ``` 10 | docker-compose up 11 | ``` 12 | 13 | ### Navigate to demo 14 | 15 | ``` 16 | http://localhost:8000/ 17 | ``` 18 | 19 | Build cool stuff! 🚀 20 | -------------------------------------------------------------------------------- /examples/gallerytest-jsonrpc/docker-compose.yaml: -------------------------------------------------------------------------------- 1 | version: "3" 2 | services: 3 | pubsub: 4 | image: nginx 5 | volumes: 6 | - ./:/usr/share/nginx/html 7 | ports: 8 | - 8000:80 9 | 10 | sfu: 11 | image: pionwebrtc/ion-sfu:latest-jsonrpc 12 | ports: 13 | - "5000-5200:5000-5200/udp" 14 | - 7000:7000 15 | -------------------------------------------------------------------------------- /examples/gallerytest-jsonrpc/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 10 | 11 | 12 | 18 | 19 | 24 | 25 | Pion ion-sfu | Gallery Test 26 | 27 | 28 | 29 | 32 |
33 |
34 |
35 | 38 |
39 | 47 |
48 | 49 |
50 |
51 | Remotes 56 |
57 |
58 |
59 | 60 | 61 | 66 | 71 | 76 | 77 | 78 | 154 | 155 | 156 | -------------------------------------------------------------------------------- /examples/pubsubtest-grpc/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 11 | 16 | 17 | 20 | 23 | 26 | 27 | Pion ion-cluster | Pub Sub test 28 | 29 | 30 | 31 | 34 |
35 |
36 |
37 |
38 | 41 | 44 | 47 | 50 |
51 |
52 |
53 |
54 |
55 | 59 | 67 |
68 |
69 | 70 |
71 |
72 | Local 73 | 74 | 102 |
103 |
104 | Remotes 105 |
106 | 111 |
112 | 117 | 122 |
123 | 124 |
125 | 126 |
127 |
RTC Event
128 |
129 |
130 |
131 |

144 |         
145 |
146 |
147 |
DataChannel Message
148 |
149 |
150 |
151 | 163 | 166 |
167 |
168 |

178 |         
179 |
180 | 181 |
182 | 183 | 184 | 185 | 186 | 187 | 188 | -------------------------------------------------------------------------------- /examples/pubsubtest-grpc/main.js: -------------------------------------------------------------------------------- 1 | const localVideo = document.getElementById("local-video"); 2 | const remotesDiv = document.getElementById("remotes"); 3 | 4 | /* eslint-env browser */ 5 | const joinBtn = document.getElementById("join-btn"); 6 | const leaveBtn = document.getElementById("leave-btn"); 7 | const publishBtn = document.getElementById("publish-btn"); 8 | const publishSBtn = document.getElementById("publish-simulcast-btn"); 9 | 10 | const codecBox = document.getElementById("select-box1"); 11 | const resolutionBox = document.getElementById("select-box2"); 12 | const simulcastBox = document.getElementById("check-box"); 13 | const localData = document.getElementById("local-data"); 14 | const remoteData = document.getElementById("remote-data"); 15 | const remoteSignal= document.getElementById("remote-signal"); 16 | const subscribeBox = document.getElementById("select-box3"); 17 | const sizeTag = document.getElementById("size-tag"); 18 | const brTag = document.getElementById("br-tag"); 19 | let localDataChannel; 20 | let trackEvent; 21 | 22 | const url = 'http://localhost:5551'; 23 | const uid = uuidv4(); 24 | const sid = "ion"; 25 | let room; 26 | let rtc; 27 | let localStream; 28 | let start; 29 | 30 | const join = async () => { 31 | console.log("[join]: sid="+sid+" uid=", uid) 32 | const connector = new Ion.Connector(url, "token"); 33 | 34 | connector.onopen = function (service){ 35 | console.log("[onopen]: service = ", service.name); 36 | }; 37 | 38 | connector.onclose = function (service){ 39 | console.log('[onclose]: service = ' + service.name); 40 | }; 41 | 42 | joinBtn.disabled = "true"; 43 | leaveBtn.removeAttribute('disabled'); 44 | publishBtn.removeAttribute('disabled'); 45 | publishSBtn.removeAttribute('disabled'); 46 | 47 | rtc = new Ion.RTC(connector); 48 | 49 | rtc.ontrack = (track, stream) => { 50 | console.log("got ", track.kind, " track", track.id, "for stream", stream.id); 51 | if (track.kind === "video") { 52 | track.onunmute = () => { 53 | if (!streams[stream.id]) { 54 | const remoteVideo = document.createElement("video"); 55 | remoteVideo.srcObject = stream; 56 | remoteVideo.autoplay = true; 57 | remoteVideo.muted = true; 58 | remoteVideo.addEventListener("loadedmetadata", function () { 59 | sizeTag.innerHTML = `${remoteVideo.videoWidth}x${remoteVideo.videoHeight}`; 60 | }); 61 | 62 | remoteVideo.onresize = function () { 63 | sizeTag.innerHTML = `${remoteVideo.videoWidth}x${remoteVideo.videoHeight}`; 64 | }; 65 | remotesDiv.appendChild(remoteVideo); 66 | streams[stream.id] = stream; 67 | stream.onremovetrack = () => { 68 | if (streams[stream.id]) { 69 | remotesDiv.removeChild(remoteVideo); 70 | streams[stream.id] = null; 71 | } 72 | }; 73 | getStats(); 74 | } 75 | }; 76 | } 77 | }; 78 | 79 | rtc.ontrackevent = function (ev) { 80 | console.log("ontrackevent: \nuid = ", ev.uid, " \nstate = ", ev.state, ", \ntracks = ", JSON.stringify(ev.tracks)); 81 | if (trackEvent === undefined) { 82 | console.log("store trackEvent=", ev) 83 | trackEvent = ev; 84 | } 85 | remoteSignal.innerHTML = remoteSignal.innerHTML + JSON.stringify(ev) + '\n'; 86 | }; 87 | 88 | rtc.join(sid, uid); 89 | 90 | const streams = {}; 91 | 92 | start = (sc) => { 93 | publishSBtn.disabled = "true"; 94 | publishBtn.disabled = "true"; 95 | 96 | let constraints = { 97 | resolution: resolutionBox.options[resolutionBox.selectedIndex].value, 98 | codec: codecBox.options[codecBox.selectedIndex].value, 99 | audio: true, 100 | simulcast: sc, 101 | } 102 | console.log("getUserMedia constraints=", constraints) 103 | Ion.LocalStream.getUserMedia(constraints) 104 | .then((media) => { 105 | localStream = media; 106 | localVideo.srcObject = media; 107 | localVideo.autoplay = true; 108 | localVideo.controls = true; 109 | localVideo.muted = true; 110 | 111 | rtc.publish(media); 112 | localDataChannel = rtc.createDataChannel(uid); 113 | }) 114 | .catch(console.error); 115 | }; 116 | 117 | rtc.ondatachannel = ({ channel }) => { 118 | channel.onmessage = ({ data }) => { 119 | console.log("datachannel msg:", data) 120 | remoteData.innerHTML = data; 121 | }; 122 | }; 123 | } 124 | 125 | const send = () => { 126 | if (!localDataChannel || !localDataChannel.readyState) { 127 | alert('publish first!', '', { 128 | confirmButtonText: 'OK', 129 | }); 130 | return 131 | } 132 | 133 | if (localDataChannel.readyState === "open") { 134 | console.log("datachannel send:", localData.value) 135 | localDataChannel.send(localData.value); 136 | } 137 | }; 138 | 139 | const leave = () => { 140 | console.log("[leave]: sid=" + sid + " uid=", uid) 141 | rtc.leave(sid, uid); 142 | joinBtn.removeAttribute('disabled'); 143 | leaveBtn.disabled = "true"; 144 | publishBtn.disabled = "true"; 145 | publishSBtn.disabled = "true"; 146 | location.reload(); 147 | } 148 | 149 | const subscribe = () => { 150 | let layer = subscribeBox.value 151 | console.log("subscribe trackEvent=", trackEvent, "layer=", layer) 152 | var infos = []; 153 | trackEvent.tracks.forEach(t => { 154 | if (t.layer === layer && t.kind === "video"){ 155 | infos.push({ 156 | track_id: t.id, 157 | mute: t.muted, 158 | layer: t.layer, 159 | subscribe: true 160 | }); 161 | } 162 | 163 | if (t.kind === "audio"){ 164 | infos.push({ 165 | track_id: t.id, 166 | mute: t.muted, 167 | layer: t.layer, 168 | subscribe: true 169 | }); 170 | } 171 | }); 172 | console.log("subscribe infos=", infos) 173 | rtc.subscribe(infos); 174 | } 175 | 176 | 177 | 178 | const controlLocalVideo = (radio) => { 179 | if (radio.value === "false") { 180 | localStream.mute("video"); 181 | } else { 182 | localStream.unmute("video"); 183 | } 184 | }; 185 | 186 | const controlLocalAudio = (radio) => { 187 | if (radio.value === "false") { 188 | localStream.mute("audio"); 189 | } else { 190 | localStream.unmute("audio"); 191 | } 192 | }; 193 | 194 | const getStats = () => { 195 | let bytesPrev; 196 | let timestampPrev; 197 | setInterval(() => { 198 | rtc.getSubStats(null).then((results) => { 199 | results.forEach((report) => { 200 | const now = report.timestamp; 201 | 202 | let bitrate; 203 | if ( 204 | report.type === "inbound-rtp" && 205 | report.mediaType === "video" 206 | ) { 207 | const bytes = report.bytesReceived; 208 | if (timestampPrev) { 209 | bitrate = (8 * (bytes - bytesPrev)) / (now - timestampPrev); 210 | bitrate = Math.floor(bitrate); 211 | } 212 | bytesPrev = bytes; 213 | timestampPrev = now; 214 | } 215 | if (bitrate) { 216 | brTag.innerHTML = `${bitrate} kbps @ ${report.framesPerSecond} fps`; 217 | } 218 | }); 219 | }); 220 | }, 1000); 221 | }; 222 | 223 | function syntaxHighlight(json) { 224 | json = json 225 | .replace(/&/g, "&") 226 | .replace(//g, ">"); 228 | return json.replace( 229 | /("(\\u[a-zA-Z0-9]{4}|\\[^u]|[^\\"])*"(\s*:)?|\b(true|false|null)\b|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?)/g, 230 | function (match) { 231 | let cls = "number"; 232 | if (/^"/.test(match)) { 233 | if (/:$/.test(match)) { 234 | cls = "key"; 235 | } else { 236 | cls = "string"; 237 | } 238 | } else if (/true|false/.test(match)) { 239 | cls = "boolean"; 240 | } else if (/null/.test(match)) { 241 | cls = "null"; 242 | } 243 | return '' + match + ""; 244 | } 245 | ); 246 | } -------------------------------------------------------------------------------- /examples/pubsubtest-jsonrpc/README.md: -------------------------------------------------------------------------------- 1 | # Pub Sub Test 2 | 3 | Pub Sub test provides a simple demonstration of multi client sfu session. 4 | 5 | ## Instructions 6 | 7 | ### Run docker-compose 8 | 9 | ``` 10 | docker-compose up 11 | ``` 12 | 13 | ### Navigate to demo 14 | Open the pub sub page in multiple tabs/browsers to simulate multiple clients. 15 | ``` 16 | http://localhost:8000/ 17 | ``` 18 | 19 | Have fun! 🚀 20 | -------------------------------------------------------------------------------- /examples/pubsubtest-jsonrpc/docker-compose.yaml: -------------------------------------------------------------------------------- 1 | version: "3" 2 | services: 3 | pubsub: 4 | image: nginx 5 | volumes: 6 | - ./:/usr/share/nginx/html 7 | ports: 8 | - 8000:80 9 | 10 | sfu: 11 | image: pionwebrtc/ion-sfu:latest-allrpc 12 | restart: always 13 | environment: 14 | - gaddr=0.0.0.0:50051 15 | - jaddr=0.0.0.0:7000 16 | volumes: 17 | - "../../config.toml:/configs/sfu.toml" 18 | ports: 19 | - 7000:7000 20 | - 50051:50051 21 | - 5000-5200:5000-5200/udp 22 | -------------------------------------------------------------------------------- /examples/pubsubtest-jsonrpc/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 10 | 11 | 12 | 18 | 19 | 24 | 25 | Pion ion-sfu | Pubsubtest 26 | 27 | 28 | 29 | 32 |
33 |
34 |
35 | 38 |
39 |
40 | 41 |
42 |
43 | Local 48 | 54 | 110 |
111 |
112 | Remotes 113 | 114 |
115 |
116 | 124 |
125 |
126 |
127 |
128 |
129 | 130 | 131 | 136 | 141 | 146 | 147 | 148 | 149 | 150 | 151 | -------------------------------------------------------------------------------- /examples/pubsubtest-jsonrpc/main.js: -------------------------------------------------------------------------------- 1 | const localVideo = document.getElementById("local-video"); 2 | const remotesDiv = document.getElementById("remotes"); 3 | 4 | const params = new URLSearchParams(window.location.search) 5 | 6 | const serverUrl = "ws://localhost:7000/ws"; 7 | 8 | /* eslint-env browser */ 9 | const joinBtns = document.getElementById("start-btns"); 10 | 11 | const config = { 12 | iceServers: [ 13 | { 14 | urls: "stun:stun.l.google.com:19302", 15 | }, 16 | ], 17 | }; 18 | 19 | const signalLocal = new Signal.IonSFUJSONRPCSignal(serverUrl); 20 | 21 | const clientLocal = new IonSDK.Client(signalLocal, config); 22 | signalLocal.onopen = () => clientLocal.join(params.has("session") ? params.get("session") : "ion"); 23 | 24 | /** 25 | * For every remote stream this object will hold the follwing information: 26 | * { 27 | * "id-of-the-remote-stream": { 28 | * stream: [Object], // Reference to the stream object 29 | * videoElement: [Object] // Reference to the video element that's rendering the stream. 30 | * } 31 | * } 32 | */ 33 | const streams = {}; 34 | 35 | /** 36 | * When we click the Enable Audio button this function gets called, and 37 | * unmutes all the remote videos that might be there and also any future ones. 38 | * This little party trick is according to 39 | * https://developer.mozilla.org/en-US/docs/Web/Media/Autoplay_guide. 40 | */ 41 | let remoteVideoIsMuted = true; 42 | function enableAudio() { 43 | if (remoteVideoIsMuted) { 44 | // Unmute all the current videoElements. 45 | for (const streamInfo of Object.values(streams)) { 46 | let { videoElement } = streamInfo; 47 | videoElement.pause(); 48 | videoElement.muted = false; 49 | videoElement.play(); 50 | } 51 | // Set remoteVideoIsMuted to false so that all future autoplays 52 | // work. 53 | remoteVideoIsMuted = false; 54 | 55 | const button = document.getElementById("enable-audio-button"); 56 | button.remove(); 57 | } 58 | } 59 | 60 | let localStream; 61 | const start = () => { 62 | IonSDK.LocalStream.getUserMedia({ 63 | resolution: "vga", 64 | audio: true, 65 | codec: params.has("codec") ? params.get("codec") : "vp8", 66 | }) 67 | .then((media) => { 68 | localStream = media; 69 | localVideo.srcObject = media; 70 | localVideo.autoplay = true; 71 | localVideo.controls = true; 72 | localVideo.muted = true; 73 | joinBtns.style.display = "none"; 74 | clientLocal.publish(media); 75 | }) 76 | .catch(console.error); 77 | }; 78 | 79 | clientLocal.ontrack = (track, stream) => { 80 | console.log("got track", track.id, "for stream", stream.id); 81 | track.onunmute = () => { 82 | // If the stream is not there in the streams map. 83 | if (!streams[stream.id]) { 84 | // Create a video element for rendering the stream 85 | const remoteVideo = document.createElement("video"); 86 | remoteVideo.srcObject = stream; 87 | remoteVideo.autoplay = true; 88 | remoteVideo.muted = remoteVideoIsMuted; 89 | remotesDiv.appendChild(remoteVideo); 90 | 91 | // Save the stream and video element in the map. 92 | streams[stream.id] = { stream, videoElement: remoteVideo }; 93 | 94 | // When this stream removes a track, assume 95 | // that its going away and remove it. 96 | stream.onremovetrack = () => { 97 | try { 98 | if (streams[stream.id]) { 99 | const { videoElement } = streams[stream.id]; 100 | remotesDiv.removeChild(videoElement); 101 | delete streams[stream.id]; 102 | } 103 | } catch (err) {} 104 | }; 105 | } 106 | }; 107 | }; 108 | 109 | const controlLocalVideo = (radio) => { 110 | if (radio.value === "false") { 111 | localStream.mute("video"); 112 | } else { 113 | localStream.unmute("video"); 114 | } 115 | }; 116 | 117 | const controlLocalAudio = (radio) => { 118 | if (radio.value === "false") { 119 | localStream.mute("audio"); 120 | } else { 121 | localStream.unmute("audio"); 122 | } 123 | }; 124 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/pion/ion-sfu 2 | 3 | go 1.13 4 | 5 | require ( 6 | github.com/bep/debounce v1.2.0 7 | github.com/gammazero/deque v0.1.0 8 | github.com/gammazero/workerpool v1.1.2 9 | github.com/go-logr/logr v1.2.0 10 | github.com/go-logr/zerologr v1.2.1 11 | github.com/gorilla/websocket v1.4.2 12 | github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0 13 | github.com/improbable-eng/grpc-web v0.14.1 14 | github.com/lucsky/cuid v1.2.1 15 | github.com/pion/dtls/v2 v2.1.3 16 | github.com/pion/ice/v2 v2.2.2 17 | github.com/pion/ion v1.10.0 18 | github.com/pion/ion-log v1.2.2 19 | github.com/pion/logging v0.2.2 20 | github.com/pion/rtcp v1.2.9 21 | github.com/pion/rtp v1.7.7 22 | github.com/pion/sdp/v3 v3.0.4 23 | github.com/pion/transport v0.13.0 24 | github.com/pion/turn/v2 v2.0.8 25 | github.com/pion/webrtc/v3 v3.1.25 26 | github.com/prometheus/client_golang v1.11.0 27 | github.com/rs/zerolog v1.26.0 28 | github.com/soheilhy/cmux v0.1.5 29 | github.com/sourcegraph/jsonrpc2 v0.1.0 30 | github.com/spf13/viper v1.9.0 31 | github.com/stretchr/testify v1.7.0 32 | golang.org/x/sync v0.0.0-20210220032951-036812b2e83c 33 | google.golang.org/grpc v1.41.0 34 | ) 35 | -------------------------------------------------------------------------------- /pkg/buffer/bucket.go: -------------------------------------------------------------------------------- 1 | package buffer 2 | 3 | import ( 4 | "encoding/binary" 5 | "math" 6 | ) 7 | 8 | const maxPktSize = 1500 9 | 10 | type Bucket struct { 11 | buf []byte 12 | src *[]byte 13 | 14 | init bool 15 | step int 16 | headSN uint16 17 | maxSteps int 18 | } 19 | 20 | func NewBucket(buf *[]byte) *Bucket { 21 | return &Bucket{ 22 | src: buf, 23 | buf: *buf, 24 | maxSteps: int(math.Floor(float64(len(*buf))/float64(maxPktSize))) - 1, 25 | } 26 | } 27 | 28 | func (b *Bucket) AddPacket(pkt []byte, sn uint16, latest bool) ([]byte, error) { 29 | if !b.init { 30 | b.headSN = sn - 1 31 | b.init = true 32 | } 33 | if !latest { 34 | return b.set(sn, pkt) 35 | } 36 | diff := sn - b.headSN 37 | b.headSN = sn 38 | for i := uint16(1); i < diff; i++ { 39 | b.step++ 40 | if b.step >= b.maxSteps { 41 | b.step = 0 42 | } 43 | } 44 | return b.push(pkt), nil 45 | } 46 | 47 | func (b *Bucket) GetPacket(buf []byte, sn uint16) (i int, err error) { 48 | p := b.get(sn) 49 | if p == nil { 50 | err = errPacketNotFound 51 | return 52 | } 53 | i = len(p) 54 | if cap(buf) < i { 55 | err = errBufferTooSmall 56 | return 57 | } 58 | if len(buf) < i { 59 | buf = buf[:i] 60 | } 61 | copy(buf, p) 62 | return 63 | } 64 | 65 | func (b *Bucket) push(pkt []byte) []byte { 66 | binary.BigEndian.PutUint16(b.buf[b.step*maxPktSize:], uint16(len(pkt))) 67 | off := b.step*maxPktSize + 2 68 | copy(b.buf[off:], pkt) 69 | b.step++ 70 | if b.step > b.maxSteps { 71 | b.step = 0 72 | } 73 | return b.buf[off : off+len(pkt)] 74 | } 75 | 76 | func (b *Bucket) get(sn uint16) []byte { 77 | pos := b.step - int(b.headSN-sn+1) 78 | if pos < 0 { 79 | if pos*-1 > b.maxSteps+1 { 80 | return nil 81 | } 82 | pos = b.maxSteps + pos + 1 83 | } 84 | off := pos * maxPktSize 85 | if off > len(b.buf) { 86 | return nil 87 | } 88 | if binary.BigEndian.Uint16(b.buf[off+4:off+6]) != sn { 89 | return nil 90 | } 91 | sz := int(binary.BigEndian.Uint16(b.buf[off : off+2])) 92 | return b.buf[off+2 : off+2+sz] 93 | } 94 | 95 | func (b *Bucket) set(sn uint16, pkt []byte) ([]byte, error) { 96 | if b.headSN-sn >= uint16(b.maxSteps+1) { 97 | return nil, errPacketTooOld 98 | } 99 | pos := b.step - int(b.headSN-sn+1) 100 | if pos < 0 { 101 | pos = b.maxSteps + pos + 1 102 | } 103 | off := pos * maxPktSize 104 | if off > len(b.buf) || off < 0 { 105 | return nil, errPacketTooOld 106 | } 107 | // Do not overwrite if packet exist 108 | if binary.BigEndian.Uint16(b.buf[off+4:off+6]) == sn { 109 | return nil, errRTXPacket 110 | } 111 | binary.BigEndian.PutUint16(b.buf[off:], uint16(len(pkt))) 112 | copy(b.buf[off+2:], pkt) 113 | return b.buf[off+2 : off+2+len(pkt)], nil 114 | } 115 | -------------------------------------------------------------------------------- /pkg/buffer/bucket_test.go: -------------------------------------------------------------------------------- 1 | package buffer 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/pion/rtp" 7 | "github.com/stretchr/testify/assert" 8 | ) 9 | 10 | var TestPackets = []*rtp.Packet{ 11 | { 12 | Header: rtp.Header{ 13 | SequenceNumber: 1, 14 | }, 15 | }, 16 | { 17 | Header: rtp.Header{ 18 | SequenceNumber: 3, 19 | }, 20 | }, 21 | { 22 | Header: rtp.Header{ 23 | SequenceNumber: 4, 24 | }, 25 | }, 26 | { 27 | Header: rtp.Header{ 28 | SequenceNumber: 6, 29 | }, 30 | }, 31 | { 32 | Header: rtp.Header{ 33 | SequenceNumber: 7, 34 | }, 35 | }, 36 | { 37 | Header: rtp.Header{ 38 | SequenceNumber: 10, 39 | }, 40 | }, 41 | } 42 | 43 | func Test_queue(t *testing.T) { 44 | b := make([]byte, 25000) 45 | q := NewBucket(&b) 46 | 47 | for _, p := range TestPackets { 48 | p := p 49 | buf, err := p.Marshal() 50 | assert.NoError(t, err) 51 | assert.NotPanics(t, func() { 52 | q.AddPacket(buf, p.SequenceNumber, true) 53 | }) 54 | } 55 | var expectedSN uint16 56 | expectedSN = 6 57 | np := rtp.Packet{} 58 | buff := make([]byte, maxPktSize) 59 | i, err := q.GetPacket(buff, 6) 60 | assert.NoError(t, err) 61 | err = np.Unmarshal(buff[:i]) 62 | assert.NoError(t, err) 63 | assert.Equal(t, expectedSN, np.SequenceNumber) 64 | 65 | np2 := &rtp.Packet{ 66 | Header: rtp.Header{ 67 | SequenceNumber: 8, 68 | }, 69 | } 70 | buf, err := np2.Marshal() 71 | assert.NoError(t, err) 72 | expectedSN = 8 73 | q.AddPacket(buf, 8, false) 74 | i, err = q.GetPacket(buff, expectedSN) 75 | assert.NoError(t, err) 76 | err = np.Unmarshal(buff[:i]) 77 | assert.NoError(t, err) 78 | assert.Equal(t, expectedSN, np.SequenceNumber) 79 | 80 | _, err = q.AddPacket(buf, 8, false) 81 | assert.ErrorIs(t, err, errRTXPacket) 82 | } 83 | 84 | func Test_queue_edges(t *testing.T) { 85 | var TestPackets = []*rtp.Packet{ 86 | { 87 | Header: rtp.Header{ 88 | SequenceNumber: 65533, 89 | }, 90 | }, 91 | { 92 | Header: rtp.Header{ 93 | SequenceNumber: 65534, 94 | }, 95 | }, 96 | { 97 | Header: rtp.Header{ 98 | SequenceNumber: 2, 99 | }, 100 | }, 101 | } 102 | b := make([]byte, 25000) 103 | q := NewBucket(&b) 104 | for _, p := range TestPackets { 105 | p := p 106 | assert.NotNil(t, p) 107 | assert.NotPanics(t, func() { 108 | p := p 109 | buf, err := p.Marshal() 110 | assert.NoError(t, err) 111 | assert.NotPanics(t, func() { 112 | q.AddPacket(buf, p.SequenceNumber, true) 113 | }) 114 | }) 115 | } 116 | var expectedSN uint16 117 | expectedSN = 65534 118 | np := rtp.Packet{} 119 | buff := make([]byte, maxPktSize) 120 | i, err := q.GetPacket(buff, expectedSN) 121 | assert.NoError(t, err) 122 | err = np.Unmarshal(buff[:i]) 123 | assert.NoError(t, err) 124 | assert.Equal(t, expectedSN, np.SequenceNumber) 125 | 126 | np2 := rtp.Packet{ 127 | Header: rtp.Header{ 128 | SequenceNumber: 65535, 129 | }, 130 | } 131 | buf, err := np2.Marshal() 132 | assert.NoError(t, err) 133 | q.AddPacket(buf, np2.SequenceNumber, false) 134 | i, err = q.GetPacket(buff, expectedSN+1) 135 | assert.NoError(t, err) 136 | err = np.Unmarshal(buff[:i]) 137 | assert.NoError(t, err) 138 | assert.Equal(t, expectedSN+1, np.SequenceNumber) 139 | } 140 | -------------------------------------------------------------------------------- /pkg/buffer/buffer_test.go: -------------------------------------------------------------------------------- 1 | package buffer 2 | 3 | import ( 4 | "sync" 5 | "testing" 6 | 7 | "github.com/pion/ion-sfu/pkg/logger" 8 | "github.com/pion/rtcp" 9 | 10 | "github.com/pion/rtp" 11 | "github.com/pion/webrtc/v3" 12 | "github.com/stretchr/testify/assert" 13 | ) 14 | 15 | func CreateTestPacket(pktStamp *SequenceNumberAndTimeStamp) *rtp.Packet { 16 | if pktStamp == nil { 17 | return &rtp.Packet{ 18 | Header: rtp.Header{}, 19 | Payload: []byte{1, 2, 3}, 20 | } 21 | } 22 | 23 | return &rtp.Packet{ 24 | Header: rtp.Header{ 25 | SequenceNumber: pktStamp.SequenceNumber, 26 | Timestamp: pktStamp.Timestamp, 27 | }, 28 | Payload: []byte{1, 2, 3}, 29 | } 30 | } 31 | 32 | type SequenceNumberAndTimeStamp struct { 33 | SequenceNumber uint16 34 | Timestamp uint32 35 | } 36 | 37 | func CreateTestListPackets(snsAndTSs []SequenceNumberAndTimeStamp) (packetList []*rtp.Packet) { 38 | for _, item := range snsAndTSs { 39 | item := item 40 | packetList = append(packetList, CreateTestPacket(&item)) 41 | } 42 | 43 | return packetList 44 | } 45 | 46 | func TestNack(t *testing.T) { 47 | pool := &sync.Pool{ 48 | New: func() interface{} { 49 | b := make([]byte, 1500) 50 | return &b 51 | }, 52 | } 53 | logger.SetGlobalOptions(logger.GlobalConfig{V: 1}) // 2 - TRACE 54 | logger := logger.New() 55 | buff := NewBuffer(123, pool, pool, logger) 56 | buff.codecType = webrtc.RTPCodecTypeVideo 57 | assert.NotNil(t, buff) 58 | var wg sync.WaitGroup 59 | // 3 nacks 1 Pli 60 | wg.Add(4) 61 | buff.OnFeedback(func(fb []rtcp.Packet) { 62 | for _, pkt := range fb { 63 | switch p := pkt.(type) { 64 | case *rtcp.TransportLayerNack: 65 | if p.Nacks[0].PacketList()[0] == 2 && p.MediaSSRC == 123 { 66 | wg.Done() 67 | } 68 | case *rtcp.PictureLossIndication: 69 | if p.MediaSSRC == 123 { 70 | wg.Done() 71 | } 72 | } 73 | } 74 | }) 75 | buff.Bind(webrtc.RTPParameters{ 76 | HeaderExtensions: nil, 77 | Codecs: []webrtc.RTPCodecParameters{ 78 | { 79 | RTPCodecCapability: webrtc.RTPCodecCapability{ 80 | MimeType: "video/vp8", 81 | ClockRate: 90000, 82 | RTCPFeedback: []webrtc.RTCPFeedback{{ 83 | Type: "nack", 84 | }}, 85 | }, 86 | PayloadType: 96, 87 | }, 88 | }, 89 | }, Options{}) 90 | for i := 0; i < 15; i++ { 91 | if i == 2 { 92 | continue 93 | } 94 | pkt := rtp.Packet{ 95 | Header: rtp.Header{SequenceNumber: uint16(i), Timestamp: uint32(i)}, 96 | Payload: []byte{0xff, 0xff, 0xff, 0xfd, 0xb4, 0x9f, 0x94, 0x1}, 97 | } 98 | b, err := pkt.Marshal() 99 | assert.NoError(t, err) 100 | _, err = buff.Write(b) 101 | assert.NoError(t, err) 102 | } 103 | wg.Wait() 104 | } 105 | 106 | func TestNewBuffer(t *testing.T) { 107 | type args struct { 108 | options Options 109 | ssrc uint32 110 | } 111 | tests := []struct { 112 | name string 113 | args args 114 | }{ 115 | { 116 | name: "Must not be nil and add packets in sequence", 117 | args: args{ 118 | options: Options{ 119 | MaxBitRate: 1e6, 120 | }, 121 | }, 122 | }, 123 | } 124 | for _, tt := range tests { 125 | tt := tt 126 | t.Run(tt.name, func(t *testing.T) { 127 | var TestPackets = []*rtp.Packet{ 128 | { 129 | Header: rtp.Header{ 130 | SequenceNumber: 65533, 131 | }, 132 | }, 133 | { 134 | Header: rtp.Header{ 135 | SequenceNumber: 65534, 136 | }, 137 | }, 138 | { 139 | Header: rtp.Header{ 140 | SequenceNumber: 2, 141 | }, 142 | }, 143 | { 144 | Header: rtp.Header{ 145 | SequenceNumber: 65535, 146 | }, 147 | }, 148 | } 149 | pool := &sync.Pool{ 150 | New: func() interface{} { 151 | b := make([]byte, 1500) 152 | return &b 153 | }, 154 | } 155 | logger.SetGlobalOptions(logger.GlobalConfig{V: 2}) // 2 - TRACE 156 | logger := logger.New() 157 | buff := NewBuffer(123, pool, pool, logger) 158 | buff.codecType = webrtc.RTPCodecTypeVideo 159 | assert.NotNil(t, buff) 160 | assert.NotNil(t, TestPackets) 161 | buff.OnFeedback(func(_ []rtcp.Packet) { 162 | }) 163 | buff.Bind(webrtc.RTPParameters{ 164 | HeaderExtensions: nil, 165 | Codecs: []webrtc.RTPCodecParameters{{ 166 | RTPCodecCapability: webrtc.RTPCodecCapability{ 167 | MimeType: "video/vp8", 168 | ClockRate: 9600, 169 | RTCPFeedback: nil, 170 | }, 171 | PayloadType: 0, 172 | }}, 173 | }, Options{}) 174 | 175 | for _, p := range TestPackets { 176 | buf, _ := p.Marshal() 177 | buff.Write(buf) 178 | } 179 | // assert.Equal(t, 6, buff.PacketQueue.size) 180 | assert.Equal(t, uint32(1<<16), buff.cycles) 181 | assert.Equal(t, uint16(2), buff.maxSeqNo) 182 | }) 183 | } 184 | } 185 | -------------------------------------------------------------------------------- /pkg/buffer/errors.go: -------------------------------------------------------------------------------- 1 | package buffer 2 | 3 | import "errors" 4 | 5 | var ( 6 | errPacketNotFound = errors.New("packet not found in cache") 7 | errBufferTooSmall = errors.New("buffer too small") 8 | errPacketTooOld = errors.New("received packet too old") 9 | errRTXPacket = errors.New("packet already received") 10 | ) 11 | -------------------------------------------------------------------------------- /pkg/buffer/factory.go: -------------------------------------------------------------------------------- 1 | package buffer 2 | 3 | import ( 4 | "io" 5 | "sync" 6 | 7 | "github.com/go-logr/logr" 8 | "github.com/pion/transport/packetio" 9 | ) 10 | 11 | type Factory struct { 12 | sync.RWMutex 13 | videoPool *sync.Pool 14 | audioPool *sync.Pool 15 | rtpBuffers map[uint32]*Buffer 16 | rtcpReaders map[uint32]*RTCPReader 17 | logger logr.Logger 18 | } 19 | 20 | func NewBufferFactory(trackingPackets int, logger logr.Logger) *Factory { 21 | // Enable package wide logging for non-method functions. 22 | // If logger is empty - use default Logger. 23 | // Logger is a public variable in buffer package. 24 | if logger == (logr.Logger{}) { 25 | logger = Logger 26 | } else { 27 | Logger = logger 28 | } 29 | 30 | return &Factory{ 31 | videoPool: &sync.Pool{ 32 | New: func() interface{} { 33 | b := make([]byte, trackingPackets*maxPktSize) 34 | return &b 35 | }, 36 | }, 37 | audioPool: &sync.Pool{ 38 | New: func() interface{} { 39 | b := make([]byte, maxPktSize*25) 40 | return &b 41 | }, 42 | }, 43 | rtpBuffers: make(map[uint32]*Buffer), 44 | rtcpReaders: make(map[uint32]*RTCPReader), 45 | logger: logger, 46 | } 47 | } 48 | 49 | func (f *Factory) GetOrNew(packetType packetio.BufferPacketType, ssrc uint32) io.ReadWriteCloser { 50 | f.Lock() 51 | defer f.Unlock() 52 | switch packetType { 53 | case packetio.RTCPBufferPacket: 54 | if reader, ok := f.rtcpReaders[ssrc]; ok { 55 | return reader 56 | } 57 | reader := NewRTCPReader(ssrc) 58 | f.rtcpReaders[ssrc] = reader 59 | reader.OnClose(func() { 60 | f.Lock() 61 | delete(f.rtcpReaders, ssrc) 62 | f.Unlock() 63 | }) 64 | return reader 65 | case packetio.RTPBufferPacket: 66 | if reader, ok := f.rtpBuffers[ssrc]; ok { 67 | return reader 68 | } 69 | buffer := NewBuffer(ssrc, f.videoPool, f.audioPool, f.logger) 70 | f.rtpBuffers[ssrc] = buffer 71 | buffer.OnClose(func() { 72 | f.Lock() 73 | delete(f.rtpBuffers, ssrc) 74 | f.Unlock() 75 | }) 76 | return buffer 77 | } 78 | return nil 79 | } 80 | 81 | func (f *Factory) GetBufferPair(ssrc uint32) (*Buffer, *RTCPReader) { 82 | f.RLock() 83 | defer f.RUnlock() 84 | return f.rtpBuffers[ssrc], f.rtcpReaders[ssrc] 85 | } 86 | 87 | func (f *Factory) GetBuffer(ssrc uint32) *Buffer { 88 | f.RLock() 89 | defer f.RUnlock() 90 | return f.rtpBuffers[ssrc] 91 | } 92 | 93 | func (f *Factory) GetRTCPReader(ssrc uint32) *RTCPReader { 94 | f.RLock() 95 | defer f.RUnlock() 96 | return f.rtcpReaders[ssrc] 97 | } 98 | -------------------------------------------------------------------------------- /pkg/buffer/helpers.go: -------------------------------------------------------------------------------- 1 | package buffer 2 | 3 | import ( 4 | "encoding/binary" 5 | "errors" 6 | "sync/atomic" 7 | ) 8 | 9 | var ( 10 | errShortPacket = errors.New("packet is not large enough") 11 | errNilPacket = errors.New("invalid nil packet") 12 | ) 13 | 14 | type atomicBool int32 15 | 16 | func (a *atomicBool) set(value bool) { 17 | var i int32 18 | if value { 19 | i = 1 20 | } 21 | atomic.StoreInt32((*int32)(a), i) 22 | } 23 | 24 | func (a *atomicBool) get() bool { 25 | return atomic.LoadInt32((*int32)(a)) != 0 26 | } 27 | 28 | // VP8 is a helper to get temporal data from VP8 packet header 29 | /* 30 | VP8 Payload Descriptor 31 | 0 1 2 3 4 5 6 7 0 1 2 3 4 5 6 7 32 | +-+-+-+-+-+-+-+-+ +-+-+-+-+-+-+-+-+ 33 | |X|R|N|S|R| PID | (REQUIRED) |X|R|N|S|R| PID | (REQUIRED) 34 | +-+-+-+-+-+-+-+-+ +-+-+-+-+-+-+-+-+ 35 | X: |I|L|T|K| RSV | (OPTIONAL) X: |I|L|T|K| RSV | (OPTIONAL) 36 | +-+-+-+-+-+-+-+-+ +-+-+-+-+-+-+-+-+ 37 | I: |M| PictureID | (OPTIONAL) I: |M| PictureID | (OPTIONAL) 38 | +-+-+-+-+-+-+-+-+ +-+-+-+-+-+-+-+-+ 39 | L: | TL0PICIDX | (OPTIONAL) | PictureID | 40 | +-+-+-+-+-+-+-+-+ +-+-+-+-+-+-+-+-+ 41 | T/K:|TID|Y| KEYIDX | (OPTIONAL) L: | TL0PICIDX | (OPTIONAL) 42 | +-+-+-+-+-+-+-+-+ +-+-+-+-+-+-+-+-+ 43 | T/K:|TID|Y| KEYIDX | (OPTIONAL) 44 | +-+-+-+-+-+-+-+-+ 45 | */ 46 | type VP8 struct { 47 | TemporalSupported bool 48 | // Optional Header 49 | PictureID uint16 /* 8 or 16 bits, picture ID */ 50 | PicIDIdx int 51 | MBit bool 52 | TL0PICIDX uint8 /* 8 bits temporal level zero index */ 53 | TlzIdx int 54 | 55 | // Optional Header If either of the T or K bits are set to 1, 56 | // the TID/Y/KEYIDX extension field MUST be present. 57 | TID uint8 /* 2 bits temporal layer idx*/ 58 | // IsKeyFrame is a helper to detect if current packet is a keyframe 59 | IsKeyFrame bool 60 | } 61 | 62 | // Unmarshal parses the passed byte slice and stores the result in the VP8 this method is called upon 63 | func (p *VP8) Unmarshal(payload []byte) error { 64 | if payload == nil { 65 | return errNilPacket 66 | } 67 | 68 | payloadLen := len(payload) 69 | 70 | if payloadLen < 1 { 71 | return errShortPacket 72 | } 73 | 74 | idx := 0 75 | S := payload[idx]&0x10 > 0 76 | // Check for extended bit control 77 | if payload[idx]&0x80 > 0 { 78 | idx++ 79 | if payloadLen < idx+1 { 80 | return errShortPacket 81 | } 82 | // Check if T is present, if not, no temporal layer is available 83 | p.TemporalSupported = payload[idx]&0x20 > 0 84 | K := payload[idx]&0x10 > 0 85 | L := payload[idx]&0x40 > 0 86 | // Check for PictureID 87 | if payload[idx]&0x80 > 0 { 88 | idx++ 89 | if payloadLen < idx+1 { 90 | return errShortPacket 91 | } 92 | p.PicIDIdx = idx 93 | pid := payload[idx] & 0x7f 94 | // Check if m is 1, then Picture ID is 15 bits 95 | if payload[idx]&0x80 > 0 { 96 | idx++ 97 | if payloadLen < idx+1 { 98 | return errShortPacket 99 | } 100 | p.MBit = true 101 | p.PictureID = binary.BigEndian.Uint16([]byte{pid, payload[idx]}) 102 | } else { 103 | p.PictureID = uint16(pid) 104 | } 105 | } 106 | // Check if TL0PICIDX is present 107 | if L { 108 | idx++ 109 | if payloadLen < idx+1 { 110 | return errShortPacket 111 | } 112 | p.TlzIdx = idx 113 | 114 | if int(idx) >= payloadLen { 115 | return errShortPacket 116 | } 117 | p.TL0PICIDX = payload[idx] 118 | } 119 | if p.TemporalSupported || K { 120 | idx++ 121 | if payloadLen < idx+1 { 122 | return errShortPacket 123 | } 124 | p.TID = (payload[idx] & 0xc0) >> 6 125 | } 126 | if idx >= payloadLen { 127 | return errShortPacket 128 | } 129 | idx++ 130 | if payloadLen < idx+1 { 131 | return errShortPacket 132 | } 133 | // Check is packet is a keyframe by looking at P bit in vp8 payload 134 | p.IsKeyFrame = payload[idx]&0x01 == 0 && S 135 | } else { 136 | idx++ 137 | if payloadLen < idx+1 { 138 | return errShortPacket 139 | } 140 | // Check is packet is a keyframe by looking at P bit in vp8 payload 141 | p.IsKeyFrame = payload[idx]&0x01 == 0 && S 142 | } 143 | return nil 144 | } 145 | 146 | // isH264Keyframe detects if h264 payload is a keyframe 147 | // this code was taken from https://github.com/jech/galene/blob/codecs/rtpconn/rtpreader.go#L45 148 | // all credits belongs to Juliusz Chroboczek @jech and the awesome Galene SFU 149 | func isH264Keyframe(payload []byte) bool { 150 | if len(payload) < 1 { 151 | return false 152 | } 153 | nalu := payload[0] & 0x1F 154 | if nalu == 0 { 155 | // reserved 156 | return false 157 | } else if nalu <= 23 { 158 | // simple NALU 159 | return nalu == 5 160 | } else if nalu == 24 || nalu == 25 || nalu == 26 || nalu == 27 { 161 | // STAP-A, STAP-B, MTAP16 or MTAP24 162 | i := 1 163 | if nalu == 25 || nalu == 26 || nalu == 27 { 164 | // skip DON 165 | i += 2 166 | } 167 | for i < len(payload) { 168 | if i+2 > len(payload) { 169 | return false 170 | } 171 | length := uint16(payload[i])<<8 | 172 | uint16(payload[i+1]) 173 | i += 2 174 | if i+int(length) > len(payload) { 175 | return false 176 | } 177 | offset := 0 178 | if nalu == 26 { 179 | offset = 3 180 | } else if nalu == 27 { 181 | offset = 4 182 | } 183 | if offset >= int(length) { 184 | return false 185 | } 186 | n := payload[i+offset] & 0x1F 187 | if n == 7 { 188 | return true 189 | } else if n >= 24 { 190 | // is this legal? 191 | Logger.V(0).Info("Non-simple NALU within a STAP") 192 | } 193 | i += int(length) 194 | } 195 | if i == len(payload) { 196 | return false 197 | } 198 | return false 199 | } else if nalu == 28 || nalu == 29 { 200 | // FU-A or FU-B 201 | if len(payload) < 2 { 202 | return false 203 | } 204 | if (payload[1] & 0x80) == 0 { 205 | // not a starting fragment 206 | return false 207 | } 208 | return payload[1]&0x1F == 7 209 | } 210 | return false 211 | } 212 | -------------------------------------------------------------------------------- /pkg/buffer/helpers_test.go: -------------------------------------------------------------------------------- 1 | package buffer 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | func TestVP8Helper_Unmarshal(t *testing.T) { 10 | type args struct { 11 | payload []byte 12 | } 13 | tests := []struct { 14 | name string 15 | args args 16 | wantErr bool 17 | checkTemporal bool 18 | temporalSupport bool 19 | checkKeyFrame bool 20 | keyFrame bool 21 | checkPictureID bool 22 | pictureID uint16 23 | checkTlzIdx bool 24 | tlzIdx uint8 25 | checkTempID bool 26 | temporalID uint8 27 | }{ 28 | { 29 | name: "Empty or nil payload must return error", 30 | args: args{payload: []byte{}}, 31 | wantErr: true, 32 | }, 33 | { 34 | name: "Temporal must be supported by setting T bit to 1", 35 | args: args{payload: []byte{0xff, 0x20, 0x1, 0x2, 0x3, 0x4}}, 36 | checkTemporal: true, 37 | temporalSupport: true, 38 | }, 39 | { 40 | name: "Picture must be ID 7 bits by setting M bit to 0 and present by I bit set to 1", 41 | args: args{payload: []byte{0xff, 0xff, 0x11, 0x2, 0x3, 0x4}}, 42 | checkPictureID: true, 43 | pictureID: 17, 44 | }, 45 | { 46 | name: "Picture ID must be 15 bits by setting M bit to 1 and present by I bit set to 1", 47 | args: args{payload: []byte{0xff, 0xff, 0x92, 0x67, 0x3, 0x4, 0x5}}, 48 | checkPictureID: true, 49 | pictureID: 4711, 50 | }, 51 | { 52 | name: "Temporal level zero index must be present if L set to 1", 53 | args: args{payload: []byte{0xff, 0xff, 0xff, 0xfd, 0xb4, 0x4, 0x5}}, 54 | checkTlzIdx: true, 55 | tlzIdx: 180, 56 | }, 57 | { 58 | name: "Temporal index must be present and used if T bit set to 1", 59 | args: args{payload: []byte{0xff, 0xff, 0xff, 0xfd, 0xb4, 0x9f, 0x5, 0x6}}, 60 | checkTempID: true, 61 | temporalID: 2, 62 | }, 63 | { 64 | name: "Check if packet is a keyframe by looking at P bit set to 0", 65 | args: args{payload: []byte{0xff, 0xff, 0xff, 0xfd, 0xb4, 0x9f, 0x94, 0x1}}, 66 | checkKeyFrame: true, 67 | keyFrame: true, 68 | }, 69 | } 70 | for _, tt := range tests { 71 | tt := tt 72 | t.Run(tt.name, func(t *testing.T) { 73 | p := &VP8{} 74 | if err := p.Unmarshal(tt.args.payload); (err != nil) != tt.wantErr { 75 | t.Errorf("Unmarshal() error = %v, wantErr %v", err, tt.wantErr) 76 | } 77 | if tt.checkTemporal { 78 | assert.Equal(t, tt.temporalSupport, p.TemporalSupported) 79 | } 80 | if tt.checkKeyFrame { 81 | assert.Equal(t, tt.keyFrame, p.IsKeyFrame) 82 | } 83 | if tt.checkPictureID { 84 | assert.Equal(t, tt.pictureID, p.PictureID) 85 | } 86 | if tt.checkTlzIdx { 87 | assert.Equal(t, tt.tlzIdx, p.TL0PICIDX) 88 | } 89 | if tt.checkTempID { 90 | assert.Equal(t, tt.temporalID, p.TID) 91 | } 92 | }) 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /pkg/buffer/nack.go: -------------------------------------------------------------------------------- 1 | package buffer 2 | 3 | import ( 4 | "sort" 5 | 6 | "github.com/pion/rtcp" 7 | ) 8 | 9 | const maxNackTimes = 3 // Max number of times a packet will be NACKed 10 | const maxNackCache = 100 // Max NACK sn the sfu will keep reference 11 | 12 | type nack struct { 13 | sn uint32 14 | nacked uint8 15 | } 16 | 17 | type nackQueue struct { 18 | nacks []nack 19 | kfSN uint32 20 | } 21 | 22 | func newNACKQueue() *nackQueue { 23 | return &nackQueue{ 24 | nacks: make([]nack, 0, maxNackCache+1), 25 | } 26 | } 27 | 28 | func (n *nackQueue) remove(extSN uint32) { 29 | i := sort.Search(len(n.nacks), func(i int) bool { return n.nacks[i].sn >= extSN }) 30 | if i >= len(n.nacks) || n.nacks[i].sn != extSN { 31 | return 32 | } 33 | copy(n.nacks[i:], n.nacks[i+1:]) 34 | n.nacks = n.nacks[:len(n.nacks)-1] 35 | } 36 | 37 | func (n *nackQueue) push(extSN uint32) { 38 | i := sort.Search(len(n.nacks), func(i int) bool { return n.nacks[i].sn >= extSN }) 39 | if i < len(n.nacks) && n.nacks[i].sn == extSN { 40 | return 41 | } 42 | 43 | nck := nack{ 44 | sn: extSN, 45 | nacked: 0, 46 | } 47 | if i == len(n.nacks) { 48 | n.nacks = append(n.nacks, nck) 49 | } else { 50 | n.nacks = append(n.nacks[:i+1], n.nacks[i:]...) 51 | n.nacks[i] = nck 52 | } 53 | 54 | if len(n.nacks) >= maxNackCache { 55 | copy(n.nacks, n.nacks[1:]) 56 | } 57 | } 58 | 59 | func (n *nackQueue) pairs(headSN uint32) ([]rtcp.NackPair, bool) { 60 | if len(n.nacks) == 0 { 61 | return nil, false 62 | } 63 | i := 0 64 | askKF := false 65 | var np rtcp.NackPair 66 | var nps []rtcp.NackPair 67 | for _, nck := range n.nacks { 68 | if nck.nacked >= maxNackTimes { 69 | if nck.sn > n.kfSN { 70 | n.kfSN = nck.sn 71 | askKF = true 72 | } 73 | continue 74 | } 75 | if nck.sn >= headSN-2 { 76 | n.nacks[i] = nck 77 | i++ 78 | continue 79 | } 80 | n.nacks[i] = nack{ 81 | sn: nck.sn, 82 | nacked: nck.nacked + 1, 83 | } 84 | i++ 85 | if np.PacketID == 0 || uint16(nck.sn) > np.PacketID+16 { 86 | if np.PacketID != 0 { 87 | nps = append(nps, np) 88 | } 89 | np.PacketID = uint16(nck.sn) 90 | np.LostPackets = 0 91 | continue 92 | } 93 | np.LostPackets |= 1 << (uint16(nck.sn) - np.PacketID - 1) 94 | } 95 | if np.PacketID != 0 { 96 | nps = append(nps, np) 97 | } 98 | n.nacks = n.nacks[:i] 99 | return nps, askKF 100 | } 101 | -------------------------------------------------------------------------------- /pkg/buffer/nack_test.go: -------------------------------------------------------------------------------- 1 | package buffer 2 | 3 | import ( 4 | "math/rand" 5 | "reflect" 6 | "testing" 7 | "time" 8 | 9 | "github.com/pion/rtcp" 10 | "github.com/stretchr/testify/assert" 11 | ) 12 | 13 | func Test_nackQueue_pairs(t *testing.T) { 14 | type fields struct { 15 | nacks []nack 16 | } 17 | tests := []struct { 18 | name string 19 | fields fields 20 | args []uint32 21 | want []rtcp.NackPair 22 | }{ 23 | { 24 | name: "Must return correct single pairs pair", 25 | fields: fields{ 26 | nacks: nil, 27 | }, 28 | args: []uint32{1, 2, 4, 5}, 29 | want: []rtcp.NackPair{{ 30 | PacketID: 1, 31 | LostPackets: 13, 32 | }}, 33 | }, 34 | { 35 | name: "Must return 2 pairs pair", 36 | fields: fields{ 37 | nacks: nil, 38 | }, 39 | args: []uint32{1, 2, 4, 5, 20, 22, 24, 27}, 40 | want: []rtcp.NackPair{ 41 | { 42 | PacketID: 1, 43 | LostPackets: 13, 44 | }, 45 | { 46 | PacketID: 20, 47 | LostPackets: 74, 48 | }, 49 | }, 50 | }, 51 | } 52 | for _, tt := range tests { 53 | tt := tt 54 | t.Run(tt.name, func(t *testing.T) { 55 | n := &nackQueue{ 56 | nacks: tt.fields.nacks, 57 | } 58 | for _, sn := range tt.args { 59 | n.push(sn) 60 | } 61 | got, _ := n.pairs(30) 62 | if !reflect.DeepEqual(got, tt.want) { 63 | t.Errorf("pairs() = %v, want %v", got, tt.want) 64 | } 65 | }) 66 | } 67 | } 68 | 69 | func Test_nackQueue_push(t *testing.T) { 70 | type fields struct { 71 | nacks []nack 72 | } 73 | type args struct { 74 | sn []uint32 75 | } 76 | tests := []struct { 77 | name string 78 | fields fields 79 | args args 80 | want []uint32 81 | }{ 82 | { 83 | name: "Must keep packet order", 84 | fields: fields{ 85 | nacks: make([]nack, 0, 10), 86 | }, 87 | args: args{ 88 | sn: []uint32{3, 4, 1, 5, 8, 7, 5}, 89 | }, 90 | want: []uint32{1, 3, 4, 5, 7, 8}, 91 | }, 92 | } 93 | for _, tt := range tests { 94 | tt := tt 95 | t.Run(tt.name, func(t *testing.T) { 96 | n := &nackQueue{ 97 | nacks: tt.fields.nacks, 98 | } 99 | for _, sn := range tt.args.sn { 100 | n.push(sn) 101 | } 102 | var newSN []uint32 103 | for _, sn := range n.nacks { 104 | newSN = append(newSN, sn.sn) 105 | } 106 | assert.Equal(t, tt.want, newSN) 107 | }) 108 | } 109 | } 110 | 111 | func Test_nackQueue(t *testing.T) { 112 | type fields struct { 113 | nacks []nack 114 | } 115 | type args struct { 116 | sn []uint32 117 | } 118 | tests := []struct { 119 | name string 120 | fields fields 121 | args args 122 | }{ 123 | { 124 | name: "Must keep packet order", 125 | fields: fields{ 126 | nacks: make([]nack, 0, 10), 127 | }, 128 | args: args{ 129 | sn: []uint32{3, 4, 1, 5, 8, 7, 5}, 130 | }, 131 | }, 132 | } 133 | for _, tt := range tests { 134 | tt := tt 135 | t.Run(tt.name, func(t *testing.T) { 136 | n := nackQueue{} 137 | r := rand.New(rand.NewSource(time.Now().UnixNano())) 138 | for i := 0; i < 100; i++ { 139 | assert.NotPanics(t, func() { 140 | n.push(uint32(r.Intn(60000))) 141 | n.remove(uint32(r.Intn(60000))) 142 | n.pairs(60001) 143 | }) 144 | } 145 | }) 146 | } 147 | } 148 | 149 | func Test_nackQueue_remove(t *testing.T) { 150 | type args struct { 151 | sn []uint32 152 | } 153 | tests := []struct { 154 | name string 155 | args args 156 | want []uint32 157 | }{ 158 | { 159 | name: "Must keep packet order", 160 | args: args{ 161 | sn: []uint32{3, 4, 1, 5, 8, 7, 5}, 162 | }, 163 | want: []uint32{1, 3, 4, 7, 8}, 164 | }, 165 | } 166 | for _, tt := range tests { 167 | tt := tt 168 | t.Run(tt.name, func(t *testing.T) { 169 | n := nackQueue{} 170 | for _, sn := range tt.args.sn { 171 | n.push(sn) 172 | } 173 | n.remove(5) 174 | var newSN []uint32 175 | for _, sn := range n.nacks { 176 | newSN = append(newSN, sn.sn) 177 | } 178 | assert.Equal(t, tt.want, newSN) 179 | }) 180 | } 181 | } 182 | -------------------------------------------------------------------------------- /pkg/buffer/rtcpreader.go: -------------------------------------------------------------------------------- 1 | package buffer 2 | 3 | import ( 4 | "io" 5 | "sync/atomic" 6 | ) 7 | 8 | type RTCPReader struct { 9 | ssrc uint32 10 | closed atomicBool 11 | onPacket atomic.Value //func([]byte) 12 | onClose func() 13 | } 14 | 15 | func NewRTCPReader(ssrc uint32) *RTCPReader { 16 | return &RTCPReader{ssrc: ssrc} 17 | } 18 | 19 | func (r *RTCPReader) Write(p []byte) (n int, err error) { 20 | if r.closed.get() { 21 | err = io.EOF 22 | return 23 | } 24 | if f, ok := r.onPacket.Load().(func([]byte)); ok { 25 | f(p) 26 | } 27 | return 28 | } 29 | 30 | func (r *RTCPReader) OnClose(fn func()) { 31 | r.onClose = fn 32 | } 33 | 34 | func (r *RTCPReader) Close() error { 35 | r.closed.set(true) 36 | r.onClose() 37 | return nil 38 | } 39 | 40 | func (r *RTCPReader) OnPacket(f func([]byte)) { 41 | r.onPacket.Store(f) 42 | } 43 | 44 | func (r *RTCPReader) Read(_ []byte) (n int, err error) { return } 45 | -------------------------------------------------------------------------------- /pkg/logger/zerologr.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Jorn Friedrich Dreyer 2 | // Modified 2021 Serhii Mikhno 3 | // 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // http://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | 16 | // Package logger defines a default implementation of the github.com/go-logr/logr 17 | // interfaces built on top of zerolog (github.com/rs/zerolog) and is the default 18 | // implementation for ion-sfu released binaries. 19 | 20 | // This package separates log level into two different concepts: 21 | // - V-Level - verbosity level, number, that every logger has. 22 | // A higher value that means more logs will be written. 23 | // - Log-level - usual log level (TRACE|DEBUG|INFO). 24 | // Every log row combines those two values. 25 | // You can set log level to TRACE and see all general traces. 26 | // To see more logs just add -v 27 | 28 | package logger 29 | 30 | import ( 31 | "fmt" 32 | "io" 33 | "os" 34 | "path/filepath" 35 | "runtime" 36 | "strings" 37 | 38 | "github.com/go-logr/logr" 39 | "github.com/go-logr/zerologr" 40 | "github.com/rs/zerolog" 41 | ) 42 | 43 | const ( 44 | timeFormat = "2006-01-02 15:04:05.000" 45 | ) 46 | 47 | // GlobalConfig config contains global options 48 | type GlobalConfig struct { 49 | V int `mapstructure:"v"` 50 | } 51 | 52 | // SetGlobalOptions sets the global options, like level against which all info logs will be 53 | // compared. If this is greater than or equal to the "V" of the logger, the 54 | // message will be logged. Concurrent-safe. 55 | func SetGlobalOptions(config GlobalConfig) { 56 | lvl := 1 - config.V 57 | if v := int(zerolog.TraceLevel); lvl < v { 58 | lvl = v 59 | } else if v := int(zerolog.InfoLevel); lvl > v { 60 | lvl = v 61 | } 62 | zerolog.SetGlobalLevel(zerolog.Level(lvl)) 63 | } 64 | 65 | // SetVLevelByStringGlobal does the same as SetGlobalOptions but 66 | // trying to expose verbosity level as more familiar "word-based" log levels 67 | func SetVLevelByStringGlobal(level string) { 68 | if v, err := zerolog.ParseLevel(level); err == nil { 69 | zerolog.SetGlobalLevel(v) 70 | } 71 | } 72 | 73 | // Options that can be passed to NewWithOptions 74 | type Options struct { 75 | // Name is an optional name of the logger 76 | Name string 77 | TimeFormat string 78 | Output io.Writer 79 | // Logger is an instance of zerolog, if nil a default logger is used 80 | Logger *zerolog.Logger 81 | } 82 | 83 | // New returns a logr.Logger, LogSink is implemented by zerolog. 84 | func New() logr.Logger { 85 | return NewWithOptions(Options{}) 86 | } 87 | 88 | // NewWithOptions returns a logr.Logger, LogSink is implemented by zerolog. 89 | func NewWithOptions(opts Options) logr.Logger { 90 | if opts.TimeFormat != "" { 91 | zerolog.TimeFieldFormat = opts.TimeFormat 92 | } else { 93 | zerolog.TimeFieldFormat = timeFormat 94 | } 95 | 96 | var out io.Writer 97 | if opts.Output != nil { 98 | out = opts.Output 99 | } else { 100 | out = getOutputFormat() 101 | } 102 | 103 | if opts.Logger == nil { 104 | l := zerolog.New(out).With().Timestamp().Logger() 105 | opts.Logger = &l 106 | } 107 | 108 | ls := zerologr.NewLogSink(opts.Logger) 109 | if zerolog.LevelFieldName == "" { 110 | // Restore field removed by Zerologr 111 | zerolog.LevelFieldName = "level" 112 | } 113 | l := logr.New(ls) 114 | if opts.Name != "" { 115 | l = l.WithName(opts.Name) 116 | } 117 | return l 118 | } 119 | 120 | func getOutputFormat() zerolog.ConsoleWriter { 121 | output := zerolog.ConsoleWriter{Out: os.Stdout, NoColor: false} 122 | output.FormatTimestamp = func(i interface{}) string { 123 | return "[" + i.(string) + "]" 124 | } 125 | output.FormatLevel = func(i interface{}) string { 126 | return strings.ToUpper(fmt.Sprintf("[%-3s]", i)) 127 | } 128 | output.FormatMessage = func(i interface{}) string { 129 | _, file, line, _ := runtime.Caller(10) 130 | return fmt.Sprintf("[%s:%d] => %s", filepath.Base(file), line, i) 131 | } 132 | return output 133 | } 134 | -------------------------------------------------------------------------------- /pkg/logger/zerologr_test.go: -------------------------------------------------------------------------------- 1 | package logger 2 | 3 | import ( 4 | "bytes" 5 | "sync/atomic" 6 | "testing" 7 | 8 | "github.com/stretchr/testify/assert" 9 | ) 10 | 11 | func TestDefaultVerbosityLevel(t *testing.T) { 12 | 13 | t.Run("info", func(t *testing.T) { 14 | SetGlobalOptions(GlobalConfig{V: 0}) 15 | out := &bytes.Buffer{} 16 | log := NewWithOptions(Options{TimeFormat: "NOTIME", Output: out}) 17 | log.Info("info") 18 | assert.Equal(t, `{"level":"info","v":0,"time":"NOTIME","message":"info"}`+"\n", out.String()) 19 | }) 20 | 21 | t.Run("info-add-fields", func(t *testing.T) { 22 | out := &bytes.Buffer{} 23 | log := NewWithOptions(Options{TimeFormat: "NOTIME", Output: out}) 24 | log.Info("", "test_field", 123) 25 | assert.Equal(t, `{"level":"info","v":0,"test_field":123,"time":"NOTIME"}`+"\n", out.String()) 26 | }) 27 | 28 | t.Run("empty", func(t *testing.T) { 29 | out := &bytes.Buffer{} 30 | log := NewWithOptions(Options{TimeFormat: "NOTIME", Output: out}) 31 | log.Info("") 32 | assert.Equal(t, `{"level":"info","v":0,"time":"NOTIME"}`+"\n", out.String()) 33 | }) 34 | 35 | t.Run("disabled", func(t *testing.T) { 36 | out := &bytes.Buffer{} 37 | log := NewWithOptions(Options{TimeFormat: "NOTIME", Output: out}) 38 | log.V(1).Info("You should not see this") 39 | assert.Equal(t, ``, out.String()) 40 | }) 41 | } 42 | 43 | func TestVerbosityLevel2(t *testing.T) { 44 | 45 | t.Run("info", func(t *testing.T) { 46 | SetGlobalOptions(GlobalConfig{V: 2}) 47 | out := &bytes.Buffer{} 48 | log := NewWithOptions(Options{TimeFormat: "NOTIME", Output: out}) 49 | log.Info("info") 50 | assert.Equal(t, `{"level":"info","v":0,"time":"NOTIME","message":"info"}`+"\n", out.String()) 51 | }) 52 | 53 | t.Run("info-add-fields", func(t *testing.T) { 54 | out := &bytes.Buffer{} 55 | log := NewWithOptions(Options{TimeFormat: "NOTIME", Output: out}) 56 | log.Info("", "test_field", 123) 57 | assert.Equal(t, `{"level":"info","v":0,"test_field":123,"time":"NOTIME"}`+"\n", out.String()) 58 | }) 59 | 60 | t.Run("empty", func(t *testing.T) { 61 | out := &bytes.Buffer{} 62 | log := NewWithOptions(Options{TimeFormat: "NOTIME", Output: out}) 63 | log.Info("") 64 | assert.Equal(t, `{"level":"info","v":0,"time":"NOTIME"}`+"\n", out.String()) 65 | }) 66 | 67 | t.Run("disabled", func(t *testing.T) { 68 | out := &bytes.Buffer{} 69 | log := NewWithOptions(Options{TimeFormat: "NOTIME", Output: out}) 70 | log.V(1).Info("") 71 | assert.Equal(t, `{"level":"debug","v":1,"time":"NOTIME"}`+"\n", out.String()) 72 | }) 73 | } 74 | 75 | type blackholeStream struct { 76 | writeCount uint64 77 | } 78 | 79 | func (s *blackholeStream) WriteCount() uint64 { 80 | return atomic.LoadUint64(&s.writeCount) 81 | } 82 | 83 | func (s *blackholeStream) Write(p []byte) (int, error) { 84 | atomic.AddUint64(&s.writeCount, 1) 85 | return len(p), nil 86 | } 87 | 88 | func BenchmarkLoggerLogs(b *testing.B) { 89 | stream := &blackholeStream{} 90 | log := NewWithOptions(Options{TimeFormat: "NOTIME", Output: stream}) 91 | b.ResetTimer() 92 | 93 | b.RunParallel(func(pb *testing.PB) { 94 | for pb.Next() { 95 | log.Info("The quick brown fox jumps over the lazy dog") 96 | } 97 | }) 98 | 99 | if stream.WriteCount() != uint64(b.N) { 100 | b.Fatalf("Log write count") 101 | } 102 | } 103 | 104 | func BenchmarLoggerLog(b *testing.B) { 105 | stream := &blackholeStream{} 106 | log := NewWithOptions(Options{TimeFormat: "NOTIME", Output: stream}) 107 | b.ResetTimer() 108 | SetGlobalOptions(GlobalConfig{V: 1}) 109 | b.RunParallel(func(pb *testing.PB) { 110 | for pb.Next() { 111 | log.V(1).Info("The quick brown fox jumps over the lazy dog") 112 | } 113 | }) 114 | 115 | if stream.WriteCount() != uint64(b.N) { 116 | b.Fatalf("Log write count") 117 | } 118 | } 119 | 120 | func BenchmarkLoggerLogWith10Fields(b *testing.B) { 121 | stream := &blackholeStream{} 122 | log := NewWithOptions(Options{TimeFormat: "NOTIME", Output: stream}) 123 | b.ResetTimer() 124 | SetGlobalOptions(GlobalConfig{V: 1}) 125 | b.RunParallel(func(pb *testing.PB) { 126 | for pb.Next() { 127 | log.V(1).Info("The quick brown fox jumps over the lazy dog", 128 | "test1", 1, 129 | "test2", 2, 130 | "test3", 3, 131 | "test4", 4, 132 | "test5", 5, 133 | "test6", 6, 134 | "test7", 7, 135 | "test8", 8, 136 | "test9", 9, 137 | "test10", 10) 138 | } 139 | }) 140 | 141 | if stream.WriteCount() != uint64(b.N) { 142 | b.Fatalf("Log write count") 143 | } 144 | } 145 | -------------------------------------------------------------------------------- /pkg/middlewares/datachannel/README.md: -------------------------------------------------------------------------------- 1 | # Datachannels middlewares 2 | 3 | `ion-sfu` supports datachannels middlewares similar to the `net/http` standard library handlers. 4 | 5 | ## API 6 | 7 | ### Middleware 8 | 9 | To create a datachannel middleware, just follow below pattern: 10 | 11 | ```go 12 | func SubscriberAPI(next sfu.MessageProcessor) sfu.MessageProcessor { 13 | return sfu.ProcessFunc(func(ctx context.Context, args sfu.ProcessArgs) { 14 | next.Process(ctx,args) 15 | } 16 | } 17 | ``` 18 | 19 | ### Init middlewares 20 | 21 | To initialize the middlewares you need to declare them after sfu initialization: 22 | 23 | ```go 24 | s := sfu.NewSFU(conf) 25 | dc := s.NewDatachannel(sfu.APIChannelLabel) 26 | dc.Use(datachannel.KeepAlive(5*time.Second), datachannel.SubscriberAPI) 27 | // This callback is optional 28 | dc.OnMessage(func(ctx context.Context, msg webrtc.DataChannelMessage, in *webrtc.DataChannel, out []*webrtc.DataChannel) { 29 | }) 30 | ``` 31 | 32 | Datachannels created in this way will be negotiated on peer join in the `Subscriber` peer connection. Clients can then get a reference to the channel by using the `ondatachannel` event handler. 33 | -------------------------------------------------------------------------------- /pkg/middlewares/datachannel/keepalive.go: -------------------------------------------------------------------------------- 1 | package datachannel 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "time" 7 | 8 | "github.com/pion/ion-sfu/pkg/sfu" 9 | ) 10 | 11 | func KeepAlive(timeout time.Duration) func(next sfu.MessageProcessor) sfu.MessageProcessor { 12 | var timer *time.Timer 13 | return func(next sfu.MessageProcessor) sfu.MessageProcessor { 14 | return sfu.ProcessFunc(func(ctx context.Context, args sfu.ProcessArgs) { 15 | if timer == nil { 16 | timer = time.AfterFunc(timeout, func() { 17 | _ = args.Peer.Close() 18 | }) 19 | } 20 | if args.Message.IsString && bytes.Equal(args.Message.Data, []byte("ping")) { 21 | if !timer.Stop() { 22 | <-timer.C 23 | } 24 | timer.Reset(timeout) 25 | _ = args.DataChannel.SendText("pong") 26 | return 27 | } 28 | next.Process(ctx, args) 29 | }) 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /pkg/middlewares/datachannel/subscriberapi.go: -------------------------------------------------------------------------------- 1 | package datachannel 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "fmt" 7 | 8 | "github.com/pion/ion-sfu/pkg/sfu" 9 | "github.com/pion/webrtc/v3" 10 | ) 11 | 12 | const ( 13 | highValue = "high" 14 | mediumValue = "medium" 15 | lowValue = "low" 16 | mutedValue = "none" 17 | ActiveLayerMethod = "activeLayer" 18 | ) 19 | 20 | type setRemoteMedia struct { 21 | StreamID string `json:"streamId"` 22 | Video string `json:"video"` 23 | Framerate string `json:"framerate"` 24 | Audio bool `json:"audio"` 25 | Layers []string `json:"layers"` 26 | } 27 | 28 | type activeLayerMessage struct { 29 | StreamID string `json:"streamId"` 30 | ActiveLayer string `json:"activeLayer"` 31 | AvailableLayers []string `json:"availableLayers"` 32 | } 33 | 34 | func layerStrToInt(layer string) (int, error) { 35 | switch layer { 36 | case highValue: 37 | return 2, nil 38 | case mediumValue: 39 | return 1, nil 40 | case lowValue: 41 | return 0, nil 42 | default: 43 | // unknown value 44 | return -1, fmt.Errorf("Unknown value") 45 | } 46 | } 47 | 48 | func layerIntToStr(layer int) (string, error) { 49 | switch layer { 50 | case 0: 51 | return lowValue, nil 52 | case 1: 53 | return mediumValue, nil 54 | case 2: 55 | return highValue, nil 56 | default: 57 | return "", fmt.Errorf("Unknown value: %d", layer) 58 | } 59 | } 60 | 61 | func transformLayers(layers []string) ([]uint16, error) { 62 | res := make([]uint16, len(layers)) 63 | for _, layer := range layers { 64 | if l, err := layerStrToInt(layer); err == nil { 65 | res = append(res, uint16(l)) 66 | } else { 67 | return nil, fmt.Errorf("Unknown layer value: %v", layer) 68 | } 69 | } 70 | return res, nil 71 | } 72 | 73 | func sendMessage(streamID string, peer sfu.Peer, layers []string, activeLayer int) { 74 | al, _ := layerIntToStr(activeLayer) 75 | payload := activeLayerMessage{ 76 | StreamID: streamID, 77 | ActiveLayer: al, 78 | AvailableLayers: layers, 79 | } 80 | msg := sfu.ChannelAPIMessage{ 81 | Method: ActiveLayerMethod, 82 | Params: payload, 83 | } 84 | bytes, err := json.Marshal(msg) 85 | if err != nil { 86 | sfu.Logger.Error(err, "unable to marshal active layer message") 87 | } 88 | 89 | if err := peer.SendDCMessage(sfu.APIChannelLabel, bytes); err != nil { 90 | sfu.Logger.Error(err, "unable to send ActiveLayerMessage to peer", "peer_id", peer.ID()) 91 | } 92 | } 93 | 94 | func SubscriberAPI(next sfu.MessageProcessor) sfu.MessageProcessor { 95 | return sfu.ProcessFunc(func(ctx context.Context, args sfu.ProcessArgs) { 96 | srm := &setRemoteMedia{} 97 | if err := json.Unmarshal(args.Message.Data, srm); err != nil { 98 | return 99 | } 100 | // Publisher changing active layers 101 | if srm.Layers != nil && len(srm.Layers) > 0 { 102 | layers, err := transformLayers(srm.Layers) 103 | if err != nil { 104 | sfu.Logger.Error(err, "error reading layers") 105 | next.Process(ctx, args) 106 | return 107 | } 108 | 109 | session := args.Peer.Session() 110 | peers := session.Peers() 111 | for _, peer := range peers { 112 | if peer.ID() != args.Peer.ID() { 113 | downTracks := peer.Subscriber().GetDownTracks(srm.StreamID) 114 | for _, dt := range downTracks { 115 | if dt.Kind() == webrtc.RTPCodecTypeVideo { 116 | newLayer, _ := dt.UptrackLayersChange(layers) 117 | sendMessage(srm.StreamID, peer, srm.Layers, int(newLayer)) 118 | } 119 | } 120 | } 121 | } 122 | } else { 123 | downTracks := args.Peer.Subscriber().GetDownTracks(srm.StreamID) 124 | for _, dt := range downTracks { 125 | switch dt.Kind() { 126 | case webrtc.RTPCodecTypeAudio: 127 | dt.Mute(!srm.Audio) 128 | case webrtc.RTPCodecTypeVideo: 129 | switch srm.Video { 130 | case highValue: 131 | dt.Mute(false) 132 | dt.SwitchSpatialLayer(2, true) 133 | case mediumValue: 134 | dt.Mute(false) 135 | dt.SwitchSpatialLayer(1, true) 136 | case lowValue: 137 | dt.Mute(false) 138 | dt.SwitchSpatialLayer(0, true) 139 | case mutedValue: 140 | dt.Mute(true) 141 | } 142 | switch srm.Framerate { 143 | case highValue: 144 | dt.SwitchTemporalLayer(2, true) 145 | case mediumValue: 146 | dt.SwitchTemporalLayer(1, true) 147 | case lowValue: 148 | dt.SwitchTemporalLayer(0, true) 149 | } 150 | } 151 | 152 | } 153 | } 154 | next.Process(ctx, args) 155 | }) 156 | } 157 | -------------------------------------------------------------------------------- /pkg/relay/README.md: -------------------------------------------------------------------------------- 1 | # Relay 2 | 3 | `ion-sfu` supports relaying tracks to other ion-SFUs or other services using the ORTC API. 4 | 5 | Using this api allows to quickly send the stream to other services by signaling a single request, after that all the following negotiations are handled internally. 6 | 7 | ## API 8 | 9 | ### Relay Peer 10 | 11 | The relay peer shares common methods with the Webrtc PeerConnection, so it should be straight forward to use. To create a new relay peer follow below example 12 | : 13 | ```go 14 | // Meta holds all the related information of the peer you want to relay. 15 | meta := PeerMeta{ 16 | PeerID : "super-villain-1", 17 | SessionID : "world-domination", 18 | } 19 | // config will hold pion/webrtc related structs required for the connection. 20 | // you should fill according your requirements or leave the defaults. 21 | config := &PeerConfig{} 22 | peer, err := NewPeer(meta, config) 23 | handleErr(err) 24 | 25 | // Now before working with the peer you need to signal the peer to 26 | // your remote sever, the signaling can be whatever method you want (gRPC, RESt, pubsub, etc..) 27 | signalFunc= func (meta PeerMeta, signal []byte) ([]byte, error){ 28 | if meta.session== "world-domination"{ 29 | return RelayToLegionOfDoom(meta, signal) 30 | } 31 | return nil, errors.New("not supported") 32 | } 33 | 34 | // The remote peer should create a new Relay Peer with the metadata and call Answer. 35 | if err:= peer.Offer(signalFunc); err!=nil{ 36 | handleErr(err) 37 | } 38 | 39 | // If there are no errors, relay peer offer some convenience methods to communicate with 40 | // Relayed peer. 41 | 42 | // Emit will fire and forget to the request event 43 | peer.Emit("evil-plan-1", data) 44 | // Request will wait for a remote answer, use a time cancelled 45 | // context to not block forever if peer does not answer 46 | ans,err:= peer.Request(ctx, "evil-plan-2", data) 47 | // To listen to remote event just attach the callback to peer 48 | peer.OnRequest( func (event string, msg Message){ 49 | // to access to request data 50 | msg.Paylod() 51 | // to reply the request 52 | msg.Reply(...) 53 | }) 54 | 55 | // The Relay Peer also has some convenience callbacks to manage the peer lifespan. 56 | 57 | // Peer OnClose is called when the remote peer connection is closed, or the Close method is called 58 | peer.OnClose(func()) 59 | // Peer OnReady is called when the relay peer is ready to start negotiating tracks, data channels and request 60 | // is highly recommended to attach all the initialization logic to this callback 61 | peer.OnReady(func()) 62 | 63 | // To add or receive tracks or data channels the API is similar to webrtc Peer Connection, just listen 64 | // to the required callbacks 65 | peer.OnDataChannel(f func(channel *webrtc.DataChannel)) 66 | peer.OnTrack(f func(track *webrtc.TrackRemote, receiver *webrtc.RTPReceiver)) 67 | // Make sure to call below methods after the OnReady callback fired. 68 | peer.CreateDataChannel(label string) 69 | peer.AddTrack(receiver *webrtc.RTPReceiver, remoteTrack *webrtc.TrackRemote, 70 | localTrack webrtc.TrackLocal) (*webrtc.RTPSender, error) 71 | ``` 72 | 73 | ### ION-SFU integration 74 | 75 | ION-SFU offers some convenience methods for relaying peers in a very simple way. 76 | 77 | To relay a peer just call `Peer.Publisher().Relay(...)` then signal the data to the remote SFU and ingest the data using: 78 | 79 | `session.AddRelayPeer(peerID string, signalData []byte) ([]byte, error)` 80 | 81 | set the []byte response from the method as the response of the signaling. And is ready, everytime a peer joins to the new SFU will negotiate the relayed stream. 82 | -------------------------------------------------------------------------------- /pkg/sfu/audioobserver.go: -------------------------------------------------------------------------------- 1 | package sfu 2 | 3 | import ( 4 | "sort" 5 | "sync" 6 | ) 7 | 8 | type audioStream struct { 9 | id string 10 | sum int 11 | total int 12 | } 13 | 14 | type AudioObserver struct { 15 | sync.RWMutex 16 | streams []*audioStream 17 | expected int 18 | threshold uint8 19 | previous []string 20 | } 21 | 22 | func NewAudioObserver(threshold uint8, interval, filter int) *AudioObserver { 23 | if threshold > 127 { 24 | threshold = 127 25 | } 26 | if filter < 0 { 27 | filter = 0 28 | } 29 | if filter > 100 { 30 | filter = 100 31 | } 32 | 33 | return &AudioObserver{ 34 | threshold: threshold, 35 | expected: interval * filter / 2000, 36 | } 37 | } 38 | 39 | func (a *AudioObserver) addStream(streamID string) { 40 | a.Lock() 41 | a.streams = append(a.streams, &audioStream{id: streamID}) 42 | a.Unlock() 43 | } 44 | 45 | func (a *AudioObserver) removeStream(streamID string) { 46 | a.Lock() 47 | defer a.Unlock() 48 | idx := -1 49 | for i, s := range a.streams { 50 | if s.id == streamID { 51 | idx = i 52 | break 53 | } 54 | } 55 | if idx == -1 { 56 | return 57 | } 58 | a.streams[idx] = a.streams[len(a.streams)-1] 59 | a.streams[len(a.streams)-1] = nil 60 | a.streams = a.streams[:len(a.streams)-1] 61 | } 62 | 63 | func (a *AudioObserver) observe(streamID string, dBov uint8) { 64 | a.RLock() 65 | defer a.RUnlock() 66 | for _, as := range a.streams { 67 | if as.id == streamID { 68 | if dBov <= a.threshold { 69 | as.sum += int(dBov) 70 | as.total++ 71 | } 72 | return 73 | } 74 | } 75 | } 76 | 77 | func (a *AudioObserver) Calc() []string { 78 | a.Lock() 79 | defer a.Unlock() 80 | 81 | sort.Slice(a.streams, func(i, j int) bool { 82 | si, sj := a.streams[i], a.streams[j] 83 | switch { 84 | case si.total != sj.total: 85 | return si.total > sj.total 86 | default: 87 | return si.sum < sj.sum 88 | } 89 | }) 90 | 91 | streamIDs := make([]string, 0, len(a.streams)) 92 | for _, s := range a.streams { 93 | if s.total >= a.expected { 94 | streamIDs = append(streamIDs, s.id) 95 | } 96 | s.total = 0 97 | s.sum = 0 98 | } 99 | 100 | if len(a.previous) == len(streamIDs) { 101 | for i, s := range a.previous { 102 | if s != streamIDs[i] { 103 | a.previous = streamIDs 104 | return streamIDs 105 | } 106 | } 107 | return nil 108 | } 109 | 110 | a.previous = streamIDs 111 | return streamIDs 112 | } 113 | -------------------------------------------------------------------------------- /pkg/sfu/audioobserver_test.go: -------------------------------------------------------------------------------- 1 | package sfu 2 | 3 | import ( 4 | "reflect" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/assert" 8 | ) 9 | 10 | func Test_audioLevel_addStream(t *testing.T) { 11 | type args struct { 12 | streamID string 13 | } 14 | tests := []struct { 15 | name string 16 | args args 17 | want audioStream 18 | }{ 19 | { 20 | name: "Must add stream to audio level monitor", 21 | args: args{ 22 | streamID: "a", 23 | }, 24 | want: audioStream{ 25 | id: "a", 26 | }, 27 | }, 28 | } 29 | for _, tt := range tests { 30 | tt := tt 31 | t.Run(tt.name, func(t *testing.T) { 32 | a := &AudioObserver{} 33 | a.addStream(tt.args.streamID) 34 | assert.Equal(t, tt.want, *a.streams[0]) 35 | 36 | }) 37 | } 38 | } 39 | 40 | func Test_audioLevel_calc(t *testing.T) { 41 | type fields struct { 42 | streams []*audioStream 43 | expected int 44 | previous []string 45 | } 46 | tests := []struct { 47 | name string 48 | fields fields 49 | want []string 50 | }{ 51 | { 52 | name: "Must return streams that are above filter", 53 | fields: fields{ 54 | streams: []*audioStream{ 55 | { 56 | id: "a", 57 | sum: 1, 58 | total: 5, 59 | }, 60 | { 61 | id: "b", 62 | sum: 2, 63 | total: 5, 64 | }, 65 | { 66 | id: "c", 67 | sum: 2, 68 | total: 2, 69 | }, 70 | }, 71 | expected: 3, 72 | }, 73 | want: []string{"a", "b"}, 74 | }, 75 | { 76 | name: "Must return nil if result is same as previous", 77 | fields: fields{ 78 | streams: []*audioStream{ 79 | { 80 | id: "a", 81 | sum: 1, 82 | total: 5, 83 | }, 84 | { 85 | id: "b", 86 | sum: 2, 87 | total: 5, 88 | }, 89 | { 90 | id: "c", 91 | sum: 2, 92 | total: 2, 93 | }, 94 | }, 95 | expected: 3, 96 | previous: []string{"a", "b"}, 97 | }, 98 | want: nil, 99 | }, 100 | } 101 | for _, tt := range tests { 102 | tt := tt 103 | t.Run(tt.name, func(t *testing.T) { 104 | a := &AudioObserver{ 105 | streams: tt.fields.streams, 106 | expected: tt.fields.expected, 107 | previous: tt.fields.previous, 108 | } 109 | if got := a.Calc(); !reflect.DeepEqual(got, tt.want) { 110 | t.Errorf("Calc() = %v, want %v", got, tt.want) 111 | } 112 | }) 113 | } 114 | } 115 | 116 | func Test_audioLevel_observe(t *testing.T) { 117 | type fields struct { 118 | streams []*audioStream 119 | threshold uint8 120 | } 121 | type args struct { 122 | streamID string 123 | dBov uint8 124 | } 125 | tests := []struct { 126 | name string 127 | fields fields 128 | args args 129 | want audioStream 130 | }{ 131 | { 132 | name: "Must increase sum and total when dBov is above threshold", 133 | fields: fields{ 134 | streams: []*audioStream{ 135 | { 136 | id: "a", 137 | sum: 0, 138 | total: 0, 139 | }, 140 | }, 141 | threshold: 40, 142 | }, 143 | args: args{ 144 | streamID: "a", 145 | dBov: 20, 146 | }, 147 | want: audioStream{ 148 | id: "a", 149 | sum: 20, 150 | total: 1, 151 | }, 152 | }, 153 | { 154 | name: "Must not increase sum and total when dBov is below threshold", 155 | fields: fields{ 156 | streams: []*audioStream{ 157 | { 158 | id: "a", 159 | sum: 0, 160 | total: 0, 161 | }, 162 | }, 163 | threshold: 40, 164 | }, 165 | args: args{ 166 | streamID: "a", 167 | dBov: 60, 168 | }, 169 | want: audioStream{ 170 | id: "a", 171 | sum: 0, 172 | total: 0, 173 | }, 174 | }, 175 | } 176 | for _, tt := range tests { 177 | tt := tt 178 | t.Run(tt.name, func(t *testing.T) { 179 | a := &AudioObserver{ 180 | streams: tt.fields.streams, 181 | threshold: tt.fields.threshold, 182 | } 183 | a.observe(tt.args.streamID, tt.args.dBov) 184 | assert.Equal(t, *a.streams[0], tt.want) 185 | }) 186 | } 187 | } 188 | 189 | func Test_audioLevel_removeStream(t *testing.T) { 190 | type fields struct { 191 | streams []*audioStream 192 | } 193 | type args struct { 194 | streamID string 195 | } 196 | tests := []struct { 197 | name string 198 | fields fields 199 | args args 200 | }{ 201 | { 202 | name: "Must remove correct ID", 203 | fields: fields{ 204 | streams: []*audioStream{ 205 | { 206 | id: "a", 207 | }, 208 | { 209 | id: "b", 210 | }, 211 | { 212 | id: "c", 213 | }, 214 | { 215 | id: "d", 216 | }, 217 | }, 218 | }, 219 | args: args{ 220 | streamID: "b", 221 | }, 222 | }, 223 | } 224 | for _, tt := range tests { 225 | tt := tt 226 | t.Run(tt.name, func(t *testing.T) { 227 | a := &AudioObserver{ 228 | streams: tt.fields.streams, 229 | } 230 | a.removeStream(tt.args.streamID) 231 | assert.Equal(t, len(a.streams), len(tt.fields.streams)-1) 232 | for _, s := range a.streams { 233 | assert.NotEqual(t, s.id, tt.args.streamID) 234 | } 235 | }) 236 | } 237 | } 238 | 239 | func Test_newAudioLevel(t *testing.T) { 240 | type args struct { 241 | threshold uint8 242 | interval int 243 | filter int 244 | } 245 | tests := []struct { 246 | name string 247 | args args 248 | want *AudioObserver 249 | }{ 250 | { 251 | name: "Must return a new audio level", 252 | args: args{ 253 | threshold: 40, 254 | interval: 1000, 255 | filter: 20, 256 | }, 257 | want: &AudioObserver{ 258 | expected: 1000 * 20 / 2000, 259 | threshold: 40, 260 | }, 261 | }, 262 | } 263 | for _, tt := range tests { 264 | tt := tt 265 | t.Run(tt.name, func(t *testing.T) { 266 | if got := NewAudioObserver(tt.args.threshold, tt.args.interval, tt.args.filter); !reflect.DeepEqual(got, tt.want) { 267 | t.Errorf("NewAudioLevel() = %v, want %v", got, tt.want) 268 | } 269 | }) 270 | } 271 | } 272 | -------------------------------------------------------------------------------- /pkg/sfu/datachannel.go: -------------------------------------------------------------------------------- 1 | package sfu 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/pion/webrtc/v3" 7 | ) 8 | 9 | type ( 10 | // Datachannel is a wrapper to define middlewares executed on defined label. 11 | // The datachannels created will be negotiated on join to all peers that joins 12 | // the SFU. 13 | Datachannel struct { 14 | Label string 15 | middlewares []func(MessageProcessor) MessageProcessor 16 | onMessage func(ctx context.Context, args ProcessArgs) 17 | } 18 | 19 | ProcessArgs struct { 20 | Peer Peer 21 | Message webrtc.DataChannelMessage 22 | DataChannel *webrtc.DataChannel 23 | } 24 | 25 | Middlewares []func(MessageProcessor) MessageProcessor 26 | 27 | MessageProcessor interface { 28 | Process(ctx context.Context, args ProcessArgs) 29 | } 30 | 31 | ProcessFunc func(ctx context.Context, args ProcessArgs) 32 | 33 | chainHandler struct { 34 | middlewares Middlewares 35 | Last MessageProcessor 36 | current MessageProcessor 37 | } 38 | ) 39 | 40 | // Use adds the middlewares to the current Datachannel. 41 | // The middlewares are going to be executed before the OnMessage event fires. 42 | func (dc *Datachannel) Use(middlewares ...func(MessageProcessor) MessageProcessor) { 43 | dc.middlewares = append(dc.middlewares, middlewares...) 44 | } 45 | 46 | // OnMessage sets the message callback for the datachannel, the event is fired 47 | // after all the middlewares have processed the message. 48 | func (dc *Datachannel) OnMessage(fn func(ctx context.Context, args ProcessArgs)) { 49 | dc.onMessage = fn 50 | } 51 | 52 | func (p ProcessFunc) Process(ctx context.Context, args ProcessArgs) { 53 | p(ctx, args) 54 | } 55 | 56 | func (mws Middlewares) Process(h MessageProcessor) MessageProcessor { 57 | return &chainHandler{mws, h, chain(mws, h)} 58 | } 59 | 60 | func (mws Middlewares) ProcessFunc(h MessageProcessor) MessageProcessor { 61 | return &chainHandler{mws, h, chain(mws, h)} 62 | } 63 | 64 | func newDCChain(m []func(p MessageProcessor) MessageProcessor) Middlewares { 65 | return Middlewares(m) 66 | } 67 | 68 | func (c *chainHandler) Process(ctx context.Context, args ProcessArgs) { 69 | c.current.Process(ctx, args) 70 | } 71 | 72 | func chain(mws []func(processor MessageProcessor) MessageProcessor, last MessageProcessor) MessageProcessor { 73 | if len(mws) == 0 { 74 | return last 75 | } 76 | h := mws[len(mws)-1](last) 77 | for i := len(mws) - 2; i >= 0; i-- { 78 | h = mws[i](h) 79 | } 80 | return h 81 | } 82 | -------------------------------------------------------------------------------- /pkg/sfu/errors.go: -------------------------------------------------------------------------------- 1 | package sfu 2 | 3 | import "errors" 4 | 5 | var ( 6 | // PeerLocal erors 7 | errPeerConnectionInitFailed = errors.New("pc init failed") 8 | errCreatingDataChannel = errors.New("failed to create data channel") 9 | // router errors 10 | errNoReceiverFound = errors.New("no receiver found") 11 | // Helpers errors 12 | errShortPacket = errors.New("packet is not large enough") 13 | errNilPacket = errors.New("invalid nil packet") 14 | 15 | ErrSpatialNotSupported = errors.New("current track does not support simulcast/SVC") 16 | ErrSpatialLayerBusy = errors.New("a spatial layer change is in progress, try latter") 17 | ) 18 | -------------------------------------------------------------------------------- /pkg/sfu/helpers.go: -------------------------------------------------------------------------------- 1 | package sfu 2 | 3 | import ( 4 | "encoding/binary" 5 | "strings" 6 | "sync/atomic" 7 | "time" 8 | 9 | "github.com/pion/ion-sfu/pkg/buffer" 10 | "github.com/pion/webrtc/v3" 11 | ) 12 | 13 | var ( 14 | ntpEpoch = time.Date(1900, 1, 1, 0, 0, 0, 0, time.UTC) 15 | ) 16 | 17 | type atomicBool int32 18 | type ntpTime uint64 19 | 20 | func (a *atomicBool) set(value bool) (swapped bool) { 21 | if value { 22 | return atomic.SwapInt32((*int32)(a), 1) == 0 23 | } 24 | return atomic.SwapInt32((*int32)(a), 0) == 1 25 | } 26 | 27 | func (a *atomicBool) get() bool { 28 | return atomic.LoadInt32((*int32)(a)) != 0 29 | } 30 | 31 | // setVp8TemporalLayer is a helper to detect and modify accordingly the vp8 payload to reflect 32 | // temporal changes in the SFU. 33 | // VP8 temporal layers implemented according https://tools.ietf.org/html/rfc7741 34 | func setVP8TemporalLayer(p *buffer.ExtPacket, d *DownTrack) (buf []byte, picID uint16, tlz0Idx uint8, drop bool) { 35 | pkt, ok := p.Payload.(buffer.VP8) 36 | if !ok { 37 | return 38 | } 39 | 40 | layer := atomic.LoadInt32(&d.temporalLayer) 41 | currentLayer := uint16(layer) 42 | currentTargetLayer := uint16(layer >> 16) 43 | // Check if temporal getLayer is requested 44 | if currentTargetLayer != currentLayer { 45 | if pkt.TID <= uint8(currentTargetLayer) { 46 | atomic.StoreInt32(&d.temporalLayer, int32(currentTargetLayer)<<16|int32(currentTargetLayer)) 47 | } 48 | } else if pkt.TID > uint8(currentLayer) { 49 | drop = true 50 | return 51 | } 52 | 53 | buf = *d.payload 54 | buf = buf[:len(p.Packet.Payload)] 55 | copy(buf, p.Packet.Payload) 56 | 57 | picID = pkt.PictureID - d.simulcast.refPicID + d.simulcast.pRefPicID + 1 58 | tlz0Idx = pkt.TL0PICIDX - d.simulcast.refTlZIdx + d.simulcast.pRefTlZIdx + 1 59 | 60 | if p.Head { 61 | d.simulcast.lPicID = picID 62 | d.simulcast.lTlZIdx = tlz0Idx 63 | } 64 | 65 | modifyVP8TemporalPayload(buf, pkt.PicIDIdx, pkt.TlzIdx, picID, tlz0Idx, pkt.MBit) 66 | 67 | return 68 | } 69 | 70 | func modifyVP8TemporalPayload(payload []byte, picIDIdx, tlz0Idx int, picID uint16, tlz0ID uint8, mBit bool) { 71 | pid := make([]byte, 2) 72 | binary.BigEndian.PutUint16(pid, picID) 73 | payload[picIDIdx] = pid[0] 74 | if mBit { 75 | payload[picIDIdx] |= 0x80 76 | payload[picIDIdx+1] = pid[1] 77 | } 78 | payload[tlz0Idx] = tlz0ID 79 | } 80 | 81 | // Do a fuzzy find for a codec in the list of codecs 82 | // Used for lookup up a codec in an existing list to find a match 83 | func codecParametersFuzzySearch(needle webrtc.RTPCodecParameters, haystack []webrtc.RTPCodecParameters) (webrtc.RTPCodecParameters, error) { 84 | // First attempt to match on MimeType + SDPFmtpLine 85 | for _, c := range haystack { 86 | if strings.EqualFold(c.RTPCodecCapability.MimeType, needle.RTPCodecCapability.MimeType) && 87 | c.RTPCodecCapability.SDPFmtpLine == needle.RTPCodecCapability.SDPFmtpLine { 88 | return c, nil 89 | } 90 | } 91 | 92 | // Fallback to just MimeType 93 | for _, c := range haystack { 94 | if strings.EqualFold(c.RTPCodecCapability.MimeType, needle.RTPCodecCapability.MimeType) { 95 | return c, nil 96 | } 97 | } 98 | 99 | return webrtc.RTPCodecParameters{}, webrtc.ErrCodecNotFound 100 | } 101 | 102 | func ntpToMillisSinceEpoch(ntp uint64) uint64 { 103 | // ntp time since epoch calculate fractional ntp as milliseconds 104 | // (lower 32 bits stored as 1/2^32 seconds) and add 105 | // ntp seconds (stored in higher 32 bits) as milliseconds 106 | return (((ntp & 0xFFFFFFFF) * 1000) >> 32) + ((ntp >> 32) * 1000) 107 | } 108 | 109 | func fastForwardTimestampAmount(newestTimestamp uint32, referenceTimestamp uint32) uint32 { 110 | if buffer.IsTimestampWrapAround(newestTimestamp, referenceTimestamp) { 111 | return uint32(uint64(newestTimestamp) + 0x100000000 - uint64(referenceTimestamp)) 112 | } 113 | if newestTimestamp < referenceTimestamp { 114 | return 0 115 | } 116 | return newestTimestamp - referenceTimestamp 117 | } 118 | 119 | func (t ntpTime) Duration() time.Duration { 120 | sec := (t >> 32) * 1e9 121 | frac := (t & 0xffffffff) * 1e9 122 | nsec := frac >> 32 123 | if uint32(frac) >= 0x80000000 { 124 | nsec++ 125 | } 126 | return time.Duration(sec + nsec) 127 | } 128 | 129 | func (t ntpTime) Time() time.Time { 130 | return ntpEpoch.Add(t.Duration()) 131 | } 132 | 133 | func toNtpTime(t time.Time) ntpTime { 134 | nsec := uint64(t.Sub(ntpEpoch)) 135 | sec := nsec / 1e9 136 | nsec = (nsec - sec*1e9) << 32 137 | frac := nsec / 1e9 138 | if nsec%1e9 >= 1e9/2 { 139 | frac++ 140 | } 141 | return ntpTime(sec<<32 | frac) 142 | } 143 | -------------------------------------------------------------------------------- /pkg/sfu/helpers_test.go: -------------------------------------------------------------------------------- 1 | package sfu 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | ) 7 | 8 | func Test_timeToNtp(t *testing.T) { 9 | type args struct { 10 | ns time.Time 11 | } 12 | tests := []struct { 13 | name string 14 | args args 15 | wantNTP uint64 16 | }{ 17 | { 18 | name: "Must return correct NTP time", 19 | args: args{ 20 | ns: time.Unix(1602391458, 1234), 21 | }, 22 | wantNTP: 16369753560730047668, 23 | }, 24 | } 25 | for _, tt := range tests { 26 | tt := tt 27 | t.Run(tt.name, func(t *testing.T) { 28 | gotNTP := uint64(toNtpTime(tt.args.ns)) 29 | if gotNTP != tt.wantNTP { 30 | t.Errorf("timeToNtp() gotFraction = %v, want %v", gotNTP, tt.wantNTP) 31 | } 32 | }) 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /pkg/sfu/mediaengine.go: -------------------------------------------------------------------------------- 1 | package sfu 2 | 3 | import ( 4 | "github.com/pion/sdp/v3" 5 | "github.com/pion/webrtc/v3" 6 | ) 7 | 8 | const frameMarking = "urn:ietf:params:rtp-hdrext:framemarking" 9 | 10 | func getPublisherMediaEngine() (*webrtc.MediaEngine, error) { 11 | me := &webrtc.MediaEngine{} 12 | if err := me.RegisterCodec(webrtc.RTPCodecParameters{ 13 | RTPCodecCapability: webrtc.RTPCodecCapability{MimeType: webrtc.MimeTypeOpus, ClockRate: 48000, Channels: 2, SDPFmtpLine: "minptime=10;useinbandfec=1", RTCPFeedback: nil}, 14 | PayloadType: 111, 15 | }, webrtc.RTPCodecTypeAudio); err != nil { 16 | return nil, err 17 | } 18 | 19 | videoRTCPFeedback := []webrtc.RTCPFeedback{ 20 | {Type: webrtc.TypeRTCPFBGoogREMB, Parameter: ""}, 21 | {Type: webrtc.TypeRTCPFBCCM, Parameter: "fir"}, 22 | {Type: webrtc.TypeRTCPFBNACK, Parameter: ""}, 23 | {Type: webrtc.TypeRTCPFBNACK, Parameter: "pli"}, 24 | } 25 | for _, codec := range []webrtc.RTPCodecParameters{ 26 | { 27 | RTPCodecCapability: webrtc.RTPCodecCapability{MimeType: webrtc.MimeTypeVP8, ClockRate: 90000, RTCPFeedback: videoRTCPFeedback}, 28 | PayloadType: 96, 29 | }, 30 | { 31 | RTPCodecCapability: webrtc.RTPCodecCapability{MimeType: webrtc.MimeTypeVP9, ClockRate: 90000, SDPFmtpLine: "profile-id=0", RTCPFeedback: videoRTCPFeedback}, 32 | PayloadType: 98, 33 | }, 34 | { 35 | RTPCodecCapability: webrtc.RTPCodecCapability{MimeType: webrtc.MimeTypeVP9, ClockRate: 90000, SDPFmtpLine: "profile-id=1", RTCPFeedback: videoRTCPFeedback}, 36 | PayloadType: 100, 37 | }, 38 | { 39 | RTPCodecCapability: webrtc.RTPCodecCapability{MimeType: webrtc.MimeTypeH264, ClockRate: 90000, SDPFmtpLine: "level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42001f", RTCPFeedback: videoRTCPFeedback}, 40 | PayloadType: 102, 41 | }, 42 | { 43 | RTPCodecCapability: webrtc.RTPCodecCapability{MimeType: webrtc.MimeTypeH264, ClockRate: 90000, SDPFmtpLine: "level-asymmetry-allowed=1;packetization-mode=0;profile-level-id=42001f", RTCPFeedback: videoRTCPFeedback}, 44 | PayloadType: 127, 45 | }, 46 | { 47 | RTPCodecCapability: webrtc.RTPCodecCapability{MimeType: webrtc.MimeTypeH264, ClockRate: 90000, SDPFmtpLine: "level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42e01f", RTCPFeedback: videoRTCPFeedback}, 48 | PayloadType: 125, 49 | }, 50 | { 51 | RTPCodecCapability: webrtc.RTPCodecCapability{MimeType: webrtc.MimeTypeH264, ClockRate: 90000, SDPFmtpLine: "level-asymmetry-allowed=1;packetization-mode=0;profile-level-id=42e01f", RTCPFeedback: videoRTCPFeedback}, 52 | PayloadType: 108, 53 | }, 54 | { 55 | RTPCodecCapability: webrtc.RTPCodecCapability{MimeType: webrtc.MimeTypeH264, ClockRate: 90000, SDPFmtpLine: "level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=640032", RTCPFeedback: videoRTCPFeedback}, 56 | PayloadType: 123, 57 | }, 58 | } { 59 | if err := me.RegisterCodec(codec, webrtc.RTPCodecTypeVideo); err != nil { 60 | return nil, err 61 | } 62 | } 63 | 64 | for _, extension := range []string{ 65 | sdp.SDESMidURI, 66 | sdp.SDESRTPStreamIDURI, 67 | sdp.TransportCCURI, 68 | frameMarking, 69 | } { 70 | if err := me.RegisterHeaderExtension(webrtc.RTPHeaderExtensionCapability{URI: extension}, webrtc.RTPCodecTypeVideo); err != nil { 71 | return nil, err 72 | } 73 | } 74 | for _, extension := range []string{ 75 | sdp.SDESMidURI, 76 | sdp.SDESRTPStreamIDURI, 77 | sdp.AudioLevelURI, 78 | } { 79 | if err := me.RegisterHeaderExtension(webrtc.RTPHeaderExtensionCapability{URI: extension}, webrtc.RTPCodecTypeAudio); err != nil { 80 | return nil, err 81 | } 82 | } 83 | 84 | return me, nil 85 | } 86 | 87 | func getSubscriberMediaEngine() (*webrtc.MediaEngine, error) { 88 | me := &webrtc.MediaEngine{} 89 | return me, nil 90 | } 91 | -------------------------------------------------------------------------------- /pkg/sfu/peer.go: -------------------------------------------------------------------------------- 1 | package sfu 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "sync" 7 | 8 | "github.com/lucsky/cuid" 9 | 10 | "github.com/pion/webrtc/v3" 11 | ) 12 | 13 | const ( 14 | publisher = 0 15 | subscriber = 1 16 | ) 17 | 18 | var ( 19 | // ErrTransportExists join is called after a peerconnection is established 20 | ErrTransportExists = errors.New("rtc transport already exists for this connection") 21 | // ErrNoTransportEstablished cannot signal before join 22 | ErrNoTransportEstablished = errors.New("no rtc transport exists for this Peer") 23 | // ErrOfferIgnored if offer received in unstable state 24 | ErrOfferIgnored = errors.New("offered ignored") 25 | ) 26 | 27 | type Peer interface { 28 | ID() string 29 | Session() Session 30 | Publisher() *Publisher 31 | Subscriber() *Subscriber 32 | Close() error 33 | SendDCMessage(label string, msg []byte) error 34 | } 35 | 36 | // JoinConfig allow adding more control to the peers joining a SessionLocal. 37 | type JoinConfig struct { 38 | // If true the peer will not be allowed to publish tracks to SessionLocal. 39 | NoPublish bool 40 | // If true the peer will not be allowed to subscribe to other peers in SessionLocal. 41 | NoSubscribe bool 42 | // If true the peer will not automatically subscribe all tracks, 43 | // and then the peer can use peer.Subscriber().AddDownTrack/RemoveDownTrack 44 | // to customize the subscrbe stream combination as needed. 45 | // this parameter depends on NoSubscribe=false. 46 | NoAutoSubscribe bool 47 | } 48 | 49 | // SessionProvider provides the SessionLocal to the sfu.Peer 50 | // This allows the sfu.SFU{} implementation to be customized / wrapped by another package 51 | type SessionProvider interface { 52 | GetSession(sid string) (Session, WebRTCTransportConfig) 53 | } 54 | 55 | type ChannelAPIMessage struct { 56 | Method string `json:"method"` 57 | Params interface{} `json:"params,omitempty"` 58 | } 59 | 60 | // PeerLocal represents a pair peer connection 61 | type PeerLocal struct { 62 | sync.Mutex 63 | id string 64 | closed atomicBool 65 | session Session 66 | provider SessionProvider 67 | 68 | publisher *Publisher 69 | subscriber *Subscriber 70 | 71 | OnOffer func(*webrtc.SessionDescription) 72 | OnIceCandidate func(*webrtc.ICECandidateInit, int) 73 | OnICEConnectionStateChange func(webrtc.ICEConnectionState) 74 | 75 | remoteAnswerPending bool 76 | negotiationPending bool 77 | } 78 | 79 | // NewPeer creates a new PeerLocal for signaling with the given SFU 80 | func NewPeer(provider SessionProvider) *PeerLocal { 81 | return &PeerLocal{ 82 | provider: provider, 83 | } 84 | } 85 | 86 | // Join initializes this peer for a given sessionID 87 | func (p *PeerLocal) Join(sid, uid string, config ...JoinConfig) error { 88 | var conf JoinConfig 89 | if len(config) > 0 { 90 | conf = config[0] 91 | } 92 | 93 | if p.session != nil { 94 | Logger.V(1).Info("peer already exists", "session_id", sid, "peer_id", p.id, "publisher_id", p.publisher.id) 95 | return ErrTransportExists 96 | } 97 | 98 | if uid == "" { 99 | uid = cuid.New() 100 | } 101 | p.id = uid 102 | var err error 103 | 104 | s, cfg := p.provider.GetSession(sid) 105 | p.session = s 106 | 107 | if !conf.NoSubscribe { 108 | p.subscriber, err = NewSubscriber(uid, cfg) 109 | if err != nil { 110 | return fmt.Errorf("error creating transport: %v", err) 111 | } 112 | 113 | p.subscriber.noAutoSubscribe = conf.NoAutoSubscribe 114 | 115 | p.subscriber.OnNegotiationNeeded(func() { 116 | p.Lock() 117 | defer p.Unlock() 118 | 119 | if p.remoteAnswerPending { 120 | p.negotiationPending = true 121 | return 122 | } 123 | 124 | Logger.V(1).Info("Negotiation needed", "peer_id", p.id) 125 | offer, err := p.subscriber.CreateOffer() 126 | if err != nil { 127 | Logger.Error(err, "CreateOffer error") 128 | return 129 | } 130 | 131 | p.remoteAnswerPending = true 132 | if p.OnOffer != nil && !p.closed.get() { 133 | Logger.V(0).Info("Send offer", "peer_id", p.id) 134 | p.OnOffer(&offer) 135 | } 136 | }) 137 | 138 | p.subscriber.OnICECandidate(func(c *webrtc.ICECandidate) { 139 | Logger.V(1).Info("On subscriber ice candidate called for peer", "peer_id", p.id) 140 | if c == nil { 141 | return 142 | } 143 | 144 | if p.OnIceCandidate != nil && !p.closed.get() { 145 | json := c.ToJSON() 146 | p.OnIceCandidate(&json, subscriber) 147 | } 148 | }) 149 | } 150 | 151 | if !conf.NoPublish { 152 | p.publisher, err = NewPublisher(uid, p.session, &cfg) 153 | if err != nil { 154 | return fmt.Errorf("error creating transport: %v", err) 155 | } 156 | if !conf.NoSubscribe { 157 | for _, dc := range p.session.GetDCMiddlewares() { 158 | if err := p.subscriber.AddDatachannel(p, dc); err != nil { 159 | return fmt.Errorf("setting subscriber default dc datachannel: %w", err) 160 | } 161 | } 162 | } 163 | 164 | p.publisher.OnICECandidate(func(c *webrtc.ICECandidate) { 165 | Logger.V(1).Info("on publisher ice candidate called for peer", "peer_id", p.id) 166 | if c == nil { 167 | return 168 | } 169 | 170 | if p.OnIceCandidate != nil && !p.closed.get() { 171 | json := c.ToJSON() 172 | p.OnIceCandidate(&json, publisher) 173 | } 174 | }) 175 | 176 | p.publisher.OnICEConnectionStateChange(func(s webrtc.ICEConnectionState) { 177 | if p.OnICEConnectionStateChange != nil && !p.closed.get() { 178 | p.OnICEConnectionStateChange(s) 179 | } 180 | }) 181 | } 182 | 183 | p.session.AddPeer(p) 184 | 185 | Logger.V(0).Info("PeerLocal join SessionLocal", "peer_id", p.id, "session_id", sid) 186 | 187 | if !conf.NoSubscribe { 188 | p.session.Subscribe(p) 189 | } 190 | return nil 191 | } 192 | 193 | // Answer an offer from remote 194 | func (p *PeerLocal) Answer(sdp webrtc.SessionDescription) (*webrtc.SessionDescription, error) { 195 | if p.publisher == nil { 196 | return nil, ErrNoTransportEstablished 197 | } 198 | 199 | Logger.V(0).Info("PeerLocal got offer", "peer_id", p.id) 200 | 201 | if p.publisher.SignalingState() != webrtc.SignalingStateStable { 202 | return nil, ErrOfferIgnored 203 | } 204 | 205 | answer, err := p.publisher.Answer(sdp) 206 | if err != nil { 207 | return nil, fmt.Errorf("error creating answer: %v", err) 208 | } 209 | 210 | Logger.V(0).Info("PeerLocal send answer", "peer_id", p.id) 211 | 212 | return &answer, nil 213 | } 214 | 215 | // SetRemoteDescription when receiving an answer from remote 216 | func (p *PeerLocal) SetRemoteDescription(sdp webrtc.SessionDescription) error { 217 | if p.subscriber == nil { 218 | return ErrNoTransportEstablished 219 | } 220 | p.Lock() 221 | defer p.Unlock() 222 | 223 | Logger.V(0).Info("PeerLocal got answer", "peer_id", p.id) 224 | if err := p.subscriber.SetRemoteDescription(sdp); err != nil { 225 | return fmt.Errorf("setting remote description: %w", err) 226 | } 227 | 228 | p.remoteAnswerPending = false 229 | 230 | if p.negotiationPending { 231 | p.negotiationPending = false 232 | p.subscriber.negotiate() 233 | } 234 | 235 | return nil 236 | } 237 | 238 | // Trickle candidates available for this peer 239 | func (p *PeerLocal) Trickle(candidate webrtc.ICECandidateInit, target int) error { 240 | if p.subscriber == nil || p.publisher == nil { 241 | return ErrNoTransportEstablished 242 | } 243 | Logger.V(0).Info("PeerLocal trickle", "peer_id", p.id) 244 | switch target { 245 | case publisher: 246 | if err := p.publisher.AddICECandidate(candidate); err != nil { 247 | return fmt.Errorf("setting ice candidate: %w", err) 248 | } 249 | case subscriber: 250 | if err := p.subscriber.AddICECandidate(candidate); err != nil { 251 | return fmt.Errorf("setting ice candidate: %w", err) 252 | } 253 | } 254 | return nil 255 | } 256 | 257 | func (p *PeerLocal) SendDCMessage(label string, msg []byte) error { 258 | if p.subscriber == nil { 259 | return fmt.Errorf("no subscriber for this peer") 260 | } 261 | dc := p.subscriber.DataChannel(label) 262 | 263 | if dc == nil { 264 | return fmt.Errorf("data channel %s doesn't exist", label) 265 | } 266 | 267 | if err := dc.SendText(string(msg)); err != nil { 268 | return fmt.Errorf("failed to send message: %v", err) 269 | } 270 | return nil 271 | } 272 | 273 | // Close shuts down the peer connection and sends true to the done channel 274 | func (p *PeerLocal) Close() error { 275 | p.Lock() 276 | defer p.Unlock() 277 | 278 | if !p.closed.set(true) { 279 | return nil 280 | } 281 | 282 | if p.session != nil { 283 | p.session.RemovePeer(p) 284 | } 285 | if p.publisher != nil { 286 | p.publisher.Close() 287 | } 288 | if p.subscriber != nil { 289 | if err := p.subscriber.Close(); err != nil { 290 | return err 291 | } 292 | } 293 | return nil 294 | } 295 | 296 | func (p *PeerLocal) Subscriber() *Subscriber { 297 | return p.subscriber 298 | } 299 | 300 | func (p *PeerLocal) Publisher() *Publisher { 301 | return p.publisher 302 | } 303 | 304 | func (p *PeerLocal) Session() Session { 305 | return p.session 306 | } 307 | 308 | // ID return the peer id 309 | func (p *PeerLocal) ID() string { 310 | return p.id 311 | } 312 | -------------------------------------------------------------------------------- /pkg/sfu/receiver_test.go: -------------------------------------------------------------------------------- 1 | package sfu 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | func TestWebRTCReceiver_OnCloseHandler(t *testing.T) { 10 | type args struct { 11 | fn func() 12 | } 13 | tests := []struct { 14 | name string 15 | args args 16 | }{ 17 | { 18 | name: "Must set on close handler function", 19 | args: args{ 20 | fn: func() {}, 21 | }, 22 | }, 23 | } 24 | for _, tt := range tests { 25 | tt := tt 26 | t.Run(tt.name, func(t *testing.T) { 27 | w := &WebRTCReceiver{} 28 | w.OnCloseHandler(tt.args.fn) 29 | assert.NotNil(t, w.onCloseHandler) 30 | }) 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /pkg/sfu/relay.go: -------------------------------------------------------------------------------- 1 | package sfu 2 | 3 | func RelayWithFanOutDataChannels() func(r *relayPeer) { 4 | return func(r *relayPeer) { 5 | r.relayFanOutDataChannels = true 6 | } 7 | } 8 | 9 | func RelayWithSenderReports() func(r *relayPeer) { 10 | return func(r *relayPeer) { 11 | r.withSRReports = true 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /pkg/sfu/relaypeer.go: -------------------------------------------------------------------------------- 1 | package sfu 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "sync" 7 | "time" 8 | 9 | "github.com/pion/ion-sfu/pkg/buffer" 10 | "github.com/pion/ion-sfu/pkg/relay" 11 | "github.com/pion/rtcp" 12 | "github.com/pion/transport/packetio" 13 | "github.com/pion/webrtc/v3" 14 | ) 15 | 16 | type RelayPeer struct { 17 | mu sync.RWMutex 18 | 19 | peer *relay.Peer 20 | session Session 21 | router Router 22 | config *WebRTCTransportConfig 23 | tracks []PublisherTrack 24 | relayPeers []*relay.Peer 25 | dataChannels []*webrtc.DataChannel 26 | } 27 | 28 | func NewRelayPeer(peer *relay.Peer, session Session, config *WebRTCTransportConfig) *RelayPeer { 29 | r := newRouter(peer.ID(), session, config) 30 | r.SetRTCPWriter(peer.WriteRTCP) 31 | 32 | rp := &RelayPeer{ 33 | peer: peer, 34 | router: r, 35 | config: config, 36 | session: session, 37 | } 38 | 39 | peer.OnTrack(func(track *webrtc.TrackRemote, receiver *webrtc.RTPReceiver, meta *relay.TrackMeta) { 40 | if recv, pub := r.AddReceiver(receiver, track, meta.TrackID, meta.StreamID); pub { 41 | recv.SetTrackMeta(meta.TrackID, meta.StreamID) 42 | session.Publish(r, recv) 43 | rp.mu.Lock() 44 | rp.tracks = append(rp.tracks, PublisherTrack{track, recv, true}) 45 | for _, lrp := range rp.relayPeers { 46 | if err := rp.createRelayTrack(track, recv, lrp); err != nil { 47 | Logger.V(1).Error(err, "Creating relay track.", "peer_id", peer.ID()) 48 | } 49 | } 50 | rp.mu.Unlock() 51 | } else { 52 | rp.mu.Lock() 53 | rp.tracks = append(rp.tracks, PublisherTrack{track, recv, false}) 54 | rp.mu.Unlock() 55 | } 56 | }) 57 | 58 | return rp 59 | } 60 | 61 | func (r *RelayPeer) GetRouter() Router { 62 | return r.router 63 | } 64 | 65 | func (r *RelayPeer) ID() string { 66 | return r.peer.ID() 67 | } 68 | 69 | func (r *RelayPeer) Relay(signalFn func(meta relay.PeerMeta, signal []byte) ([]byte, error)) (*relay.Peer, error) { 70 | rp, err := relay.NewPeer(relay.PeerMeta{ 71 | PeerID: r.peer.ID(), 72 | SessionID: r.session.ID(), 73 | }, &relay.PeerConfig{ 74 | SettingEngine: r.config.Setting, 75 | ICEServers: r.config.Configuration.ICEServers, 76 | Logger: Logger, 77 | }) 78 | if err != nil { 79 | return nil, fmt.Errorf("relay: %w", err) 80 | } 81 | 82 | rp.OnReady(func() { 83 | r.mu.Lock() 84 | for _, tp := range r.tracks { 85 | if !tp.clientRelay { 86 | // simulcast will just relay client track for now 87 | continue 88 | } 89 | if err = r.createRelayTrack(tp.Track, tp.Receiver, rp); err != nil { 90 | Logger.V(1).Error(err, "Creating relay track.", "peer_id", r.ID()) 91 | } 92 | } 93 | r.relayPeers = append(r.relayPeers, rp) 94 | r.mu.Unlock() 95 | go r.relayReports(rp) 96 | }) 97 | 98 | rp.OnDataChannel(func(channel *webrtc.DataChannel) { 99 | r.mu.Lock() 100 | r.dataChannels = append(r.dataChannels, channel) 101 | r.mu.Unlock() 102 | r.session.AddDatachannel("", channel) 103 | }) 104 | 105 | if err = rp.Offer(signalFn); err != nil { 106 | return nil, fmt.Errorf("relay: %w", err) 107 | } 108 | 109 | return rp, nil 110 | } 111 | 112 | func (r *RelayPeer) DataChannel(label string) *webrtc.DataChannel { 113 | r.mu.RLock() 114 | defer r.mu.RUnlock() 115 | for _, dc := range r.dataChannels { 116 | if dc.Label() == label { 117 | return dc 118 | } 119 | } 120 | return nil 121 | } 122 | 123 | func (r *RelayPeer) createRelayTrack(track *webrtc.TrackRemote, receiver Receiver, rp *relay.Peer) error { 124 | codec := track.Codec() 125 | downTrack, err := NewDownTrack(webrtc.RTPCodecCapability{ 126 | MimeType: codec.MimeType, 127 | ClockRate: codec.ClockRate, 128 | Channels: codec.Channels, 129 | SDPFmtpLine: codec.SDPFmtpLine, 130 | RTCPFeedback: []webrtc.RTCPFeedback{{"nack", ""}, {"nack", "pli"}}, 131 | }, receiver, r.config.BufferFactory, r.ID(), r.config.Router.MaxPacketTrack) 132 | if err != nil { 133 | Logger.V(1).Error(err, "Create Relay downtrack err", "peer_id", r.ID()) 134 | return err 135 | } 136 | 137 | sdr, err := rp.AddTrack(receiver.(*WebRTCReceiver).receiver, track, downTrack) 138 | if err != nil { 139 | Logger.V(1).Error(err, "Relaying track.", "peer_id", r.ID()) 140 | return fmt.Errorf("relay: %w", err) 141 | } 142 | 143 | r.config.BufferFactory.GetOrNew(packetio.RTCPBufferPacket, 144 | uint32(sdr.GetParameters().Encodings[0].SSRC)).(*buffer.RTCPReader).OnPacket(func(bytes []byte) { 145 | pkts, err := rtcp.Unmarshal(bytes) 146 | if err != nil { 147 | Logger.V(1).Error(err, "Unmarshal rtcp reports", "peer_id", r.ID()) 148 | return 149 | } 150 | var rpkts []rtcp.Packet 151 | for _, pkt := range pkts { 152 | switch pk := pkt.(type) { 153 | case *rtcp.PictureLossIndication: 154 | rpkts = append(rpkts, &rtcp.PictureLossIndication{ 155 | SenderSSRC: pk.MediaSSRC, 156 | MediaSSRC: uint32(track.SSRC()), 157 | }) 158 | } 159 | } 160 | 161 | if len(rpkts) > 0 { 162 | if err := r.peer.WriteRTCP(rpkts); err != nil { 163 | Logger.V(1).Error(err, "Sending rtcp relay reports", "peer_id", r.ID()) 164 | } 165 | } 166 | }) 167 | 168 | downTrack.OnCloseHandler(func() { 169 | if err = sdr.Stop(); err != nil { 170 | Logger.V(1).Error(err, "Stopping relay sender.", "peer_id", r.ID()) 171 | } 172 | }) 173 | 174 | receiver.AddDownTrack(downTrack, true) 175 | return nil 176 | } 177 | 178 | func (r *RelayPeer) relayReports(rp *relay.Peer) { 179 | for { 180 | time.Sleep(5 * time.Second) 181 | 182 | var packets []rtcp.Packet 183 | for _, t := range rp.LocalTracks() { 184 | if dt, ok := t.(*DownTrack); ok { 185 | if !dt.bound.get() { 186 | continue 187 | } 188 | if sr := dt.CreateSenderReport(); sr != nil { 189 | packets = append(packets, sr) 190 | } 191 | } 192 | } 193 | 194 | if len(packets) == 0 { 195 | continue 196 | } 197 | 198 | if err := rp.WriteRTCP(packets); err != nil { 199 | if err == io.EOF || err == io.ErrClosedPipe { 200 | return 201 | } 202 | Logger.Error(err, "Sending downtrack reports err") 203 | } 204 | } 205 | } 206 | -------------------------------------------------------------------------------- /pkg/sfu/sequencer.go: -------------------------------------------------------------------------------- 1 | package sfu 2 | 3 | import ( 4 | "sync" 5 | "time" 6 | ) 7 | 8 | const ( 9 | ignoreRetransmission = 100 // Ignore packet retransmission after ignoreRetransmission milliseconds 10 | ) 11 | 12 | type packetMeta struct { 13 | // Original sequence number from stream. 14 | // The original sequence number is used to find the original 15 | // packet from publisher 16 | sourceSeqNo uint16 17 | // Modified sequence number after offset. 18 | // This sequence number is used for the associated 19 | // down track, is modified according the offsets, and 20 | // must not be shared 21 | targetSeqNo uint16 22 | // Modified timestamp for current associated 23 | // down track. 24 | timestamp uint32 25 | // The last time this packet was nack requested. 26 | // Sometimes clients request the same packet more than once, so keep 27 | // track of the requested packets helps to avoid writing multiple times 28 | // the same packet. 29 | // The resolution is 1 ms counting after the sequencer start time. 30 | lastNack uint32 31 | // Spatial layer of packet 32 | layer uint8 33 | // Information that differs depending the codec 34 | misc uint32 35 | } 36 | 37 | func (p *packetMeta) setVP8PayloadMeta(tlz0Idx uint8, picID uint16) { 38 | p.misc = uint32(tlz0Idx)<<16 | uint32(picID) 39 | } 40 | 41 | func (p *packetMeta) getVP8PayloadMeta() (uint8, uint16) { 42 | return uint8(p.misc >> 16), uint16(p.misc) 43 | } 44 | 45 | // Sequencer stores the packet sequence received by the down track 46 | type sequencer struct { 47 | sync.Mutex 48 | init bool 49 | max int 50 | seq []packetMeta 51 | step int 52 | headSN uint16 53 | startTime int64 54 | } 55 | 56 | func newSequencer(maxTrack int) *sequencer { 57 | return &sequencer{ 58 | startTime: time.Now().UnixNano() / 1e6, 59 | max: maxTrack, 60 | seq: make([]packetMeta, maxTrack), 61 | } 62 | } 63 | 64 | func (n *sequencer) push(sn, offSn uint16, timeStamp uint32, layer uint8, head bool) *packetMeta { 65 | n.Lock() 66 | defer n.Unlock() 67 | if !n.init { 68 | n.headSN = offSn 69 | n.init = true 70 | } 71 | 72 | step := 0 73 | if head { 74 | inc := offSn - n.headSN 75 | for i := uint16(1); i < inc; i++ { 76 | n.step++ 77 | if n.step >= n.max { 78 | n.step = 0 79 | } 80 | } 81 | step = n.step 82 | n.headSN = offSn 83 | } else { 84 | step = n.step - int(n.headSN-offSn) 85 | if step < 0 { 86 | if step*-1 >= n.max { 87 | Logger.V(0).Info("Old packet received, can not be sequenced", "head", sn, "received", offSn) 88 | return nil 89 | } 90 | step = n.max + step 91 | } 92 | } 93 | n.seq[n.step] = packetMeta{ 94 | sourceSeqNo: sn, 95 | targetSeqNo: offSn, 96 | timestamp: timeStamp, 97 | layer: layer, 98 | } 99 | pm := &n.seq[n.step] 100 | n.step++ 101 | if n.step >= n.max { 102 | n.step = 0 103 | } 104 | return pm 105 | } 106 | 107 | func (n *sequencer) getSeqNoPairs(seqNo []uint16) []packetMeta { 108 | n.Lock() 109 | meta := make([]packetMeta, 0, 17) 110 | refTime := uint32(time.Now().UnixNano()/1e6 - n.startTime) 111 | for _, sn := range seqNo { 112 | step := n.step - int(n.headSN-sn) - 1 113 | if step < 0 { 114 | if step*-1 >= n.max { 115 | continue 116 | } 117 | step = n.max + step 118 | } 119 | seq := &n.seq[step] 120 | if seq.targetSeqNo == sn { 121 | if seq.lastNack == 0 || refTime-seq.lastNack > ignoreRetransmission { 122 | seq.lastNack = refTime 123 | meta = append(meta, *seq) 124 | } 125 | } 126 | } 127 | n.Unlock() 128 | return meta 129 | } 130 | -------------------------------------------------------------------------------- /pkg/sfu/sequencer_test.go: -------------------------------------------------------------------------------- 1 | package sfu 2 | 3 | import ( 4 | "reflect" 5 | "testing" 6 | "time" 7 | 8 | "github.com/stretchr/testify/assert" 9 | ) 10 | 11 | func Test_sequencer(t *testing.T) { 12 | seq := newSequencer(500) 13 | off := uint16(15) 14 | 15 | for i := uint16(1); i < 520; i++ { 16 | seq.push(i, i+off, 123, 2, true) 17 | } 18 | 19 | time.Sleep(60 * time.Millisecond) 20 | req := []uint16{57, 58, 62, 63, 513, 514, 515, 516, 517} 21 | res := seq.getSeqNoPairs(req) 22 | assert.Equal(t, len(req), len(res)) 23 | for i, val := range res { 24 | assert.Equal(t, val.targetSeqNo, req[i]) 25 | assert.Equal(t, val.sourceSeqNo, req[i]-off) 26 | assert.Equal(t, val.layer, uint8(2)) 27 | } 28 | res = seq.getSeqNoPairs(req) 29 | assert.Equal(t, 0, len(res)) 30 | time.Sleep(150 * time.Millisecond) 31 | res = seq.getSeqNoPairs(req) 32 | assert.Equal(t, len(req), len(res)) 33 | for i, val := range res { 34 | assert.Equal(t, val.targetSeqNo, req[i]) 35 | assert.Equal(t, val.sourceSeqNo, req[i]-off) 36 | assert.Equal(t, val.layer, uint8(2)) 37 | } 38 | 39 | s := seq.push(521, 521+off, 123, 1, true) 40 | var ( 41 | tlzIdx = uint8(15) 42 | picID = uint16(16) 43 | ) 44 | s.setVP8PayloadMeta(tlzIdx, picID) 45 | s.sourceSeqNo = 12 46 | m := seq.getSeqNoPairs([]uint16{521 + off}) 47 | assert.Equal(t, 1, len(m)) 48 | tlz0, pID := m[0].getVP8PayloadMeta() 49 | assert.Equal(t, tlzIdx, tlz0) 50 | assert.Equal(t, picID, pID) 51 | } 52 | 53 | func Test_sequencer_getNACKSeqNo(t *testing.T) { 54 | type args struct { 55 | seqNo []uint16 56 | } 57 | type fields struct { 58 | input []uint16 59 | offset uint16 60 | } 61 | 62 | tests := []struct { 63 | name string 64 | fields fields 65 | args args 66 | want []uint16 67 | }{ 68 | { 69 | name: "Should get correct seq numbers", 70 | fields: fields{ 71 | input: []uint16{2, 3, 4, 7, 8}, 72 | offset: 5, 73 | }, 74 | args: args{ 75 | seqNo: []uint16{4 + 5, 5 + 5, 8 + 5}, 76 | }, 77 | want: []uint16{4, 8}, 78 | }, 79 | } 80 | for _, tt := range tests { 81 | tt := tt 82 | t.Run(tt.name, func(t *testing.T) { 83 | n := newSequencer(500) 84 | 85 | for _, i := range tt.fields.input { 86 | n.push(i, i+tt.fields.offset, 123, 3, true) 87 | } 88 | 89 | g := n.getSeqNoPairs(tt.args.seqNo) 90 | var got []uint16 91 | for _, sn := range g { 92 | got = append(got, sn.sourceSeqNo) 93 | } 94 | if !reflect.DeepEqual(got, tt.want) { 95 | t.Errorf("getSeqNoPairs() = %v, want %v", got, tt.want) 96 | } 97 | }) 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /pkg/sfu/sfu.go: -------------------------------------------------------------------------------- 1 | package sfu 2 | 3 | import ( 4 | "math/rand" 5 | "net" 6 | "os" 7 | "runtime" 8 | "sync" 9 | "time" 10 | 11 | "github.com/go-logr/logr" 12 | "github.com/pion/ice/v2" 13 | "github.com/pion/ion-sfu/pkg/buffer" 14 | "github.com/pion/ion-sfu/pkg/stats" 15 | "github.com/pion/turn/v2" 16 | "github.com/pion/webrtc/v3" 17 | ) 18 | 19 | // Logger is an implementation of logr.Logger. If is not provided - will be turned off. 20 | var Logger logr.Logger = logr.Discard() 21 | 22 | // ICEServerConfig defines parameters for ice servers 23 | type ICEServerConfig struct { 24 | URLs []string `mapstructure:"urls"` 25 | Username string `mapstructure:"username"` 26 | Credential string `mapstructure:"credential"` 27 | } 28 | 29 | type Candidates struct { 30 | IceLite bool `mapstructure:"icelite"` 31 | NAT1To1IPs []string `mapstructure:"nat1to1"` 32 | } 33 | 34 | // WebRTCTransportConfig represents Configuration options 35 | type WebRTCTransportConfig struct { 36 | Configuration webrtc.Configuration 37 | Setting webrtc.SettingEngine 38 | Router RouterConfig 39 | BufferFactory *buffer.Factory 40 | } 41 | 42 | type WebRTCTimeoutsConfig struct { 43 | ICEDisconnectedTimeout int `mapstructure:"disconnected"` 44 | ICEFailedTimeout int `mapstructure:"failed"` 45 | ICEKeepaliveInterval int `mapstructure:"keepalive"` 46 | } 47 | 48 | // WebRTCConfig defines parameters for ice 49 | type WebRTCConfig struct { 50 | ICESinglePort int `mapstructure:"singleport"` 51 | ICEPortRange []uint16 `mapstructure:"portrange"` 52 | ICEServers []ICEServerConfig `mapstructure:"iceserver"` 53 | Candidates Candidates `mapstructure:"candidates"` 54 | SDPSemantics string `mapstructure:"sdpsemantics"` 55 | MDNS bool `mapstructure:"mdns"` 56 | Timeouts WebRTCTimeoutsConfig `mapstructure:"timeouts"` 57 | } 58 | 59 | // Config for base SFU 60 | type Config struct { 61 | SFU struct { 62 | Ballast int64 `mapstructure:"ballast"` 63 | WithStats bool `mapstructure:"withstats"` 64 | } `mapstructure:"sfu"` 65 | WebRTC WebRTCConfig `mapstructure:"webrtc"` 66 | Router RouterConfig `mapstructure:"Router"` 67 | Turn TurnConfig `mapstructure:"turn"` 68 | BufferFactory *buffer.Factory 69 | TurnAuth func(username string, realm string, srcAddr net.Addr) ([]byte, bool) 70 | } 71 | 72 | var ( 73 | packetFactory *sync.Pool 74 | ) 75 | 76 | // SFU represents an sfu instance 77 | type SFU struct { 78 | sync.RWMutex 79 | webrtc WebRTCTransportConfig 80 | turn *turn.Server 81 | sessions map[string]Session 82 | datachannels []*Datachannel 83 | withStats bool 84 | } 85 | 86 | // NewWebRTCTransportConfig parses our settings and returns a usable WebRTCTransportConfig for creating PeerConnections 87 | func NewWebRTCTransportConfig(c Config) WebRTCTransportConfig { 88 | se := webrtc.SettingEngine{} 89 | se.DisableMediaEngineCopy(true) 90 | 91 | if c.WebRTC.ICESinglePort != 0 { 92 | Logger.Info("Listen on ", "single-port", c.WebRTC.ICESinglePort) 93 | udpListener, err := net.ListenUDP("udp", &net.UDPAddr{ 94 | IP: net.IP{0, 0, 0, 0}, 95 | Port: c.WebRTC.ICESinglePort, 96 | }) 97 | if err != nil { 98 | panic(err) 99 | } 100 | se.SetICEUDPMux(webrtc.NewICEUDPMux(nil, udpListener)) 101 | } else { 102 | var icePortStart, icePortEnd uint16 103 | 104 | if c.Turn.Enabled && len(c.Turn.PortRange) == 0 { 105 | icePortStart = sfuMinPort 106 | icePortEnd = sfuMaxPort 107 | } else if len(c.WebRTC.ICEPortRange) == 2 { 108 | icePortStart = c.WebRTC.ICEPortRange[0] 109 | icePortEnd = c.WebRTC.ICEPortRange[1] 110 | } 111 | if icePortStart != 0 || icePortEnd != 0 { 112 | if err := se.SetEphemeralUDPPortRange(icePortStart, icePortEnd); err != nil { 113 | panic(err) 114 | } 115 | } 116 | } 117 | 118 | var iceServers []webrtc.ICEServer 119 | if c.WebRTC.Candidates.IceLite { 120 | se.SetLite(c.WebRTC.Candidates.IceLite) 121 | } else { 122 | for _, iceServer := range c.WebRTC.ICEServers { 123 | s := webrtc.ICEServer{ 124 | URLs: iceServer.URLs, 125 | Username: iceServer.Username, 126 | Credential: iceServer.Credential, 127 | } 128 | iceServers = append(iceServers, s) 129 | } 130 | } 131 | 132 | se.BufferFactory = c.BufferFactory.GetOrNew 133 | 134 | sdpSemantics := webrtc.SDPSemanticsUnifiedPlan 135 | switch c.WebRTC.SDPSemantics { 136 | case "unified-plan-with-fallback": 137 | sdpSemantics = webrtc.SDPSemanticsUnifiedPlanWithFallback 138 | case "plan-b": 139 | sdpSemantics = webrtc.SDPSemanticsPlanB 140 | } 141 | 142 | if c.WebRTC.Timeouts.ICEDisconnectedTimeout == 0 && 143 | c.WebRTC.Timeouts.ICEFailedTimeout == 0 && 144 | c.WebRTC.Timeouts.ICEKeepaliveInterval == 0 { 145 | Logger.Info("No webrtc timeouts found in config, using default ones") 146 | } else { 147 | se.SetICETimeouts( 148 | time.Duration(c.WebRTC.Timeouts.ICEDisconnectedTimeout)*time.Second, 149 | time.Duration(c.WebRTC.Timeouts.ICEFailedTimeout)*time.Second, 150 | time.Duration(c.WebRTC.Timeouts.ICEKeepaliveInterval)*time.Second, 151 | ) 152 | } 153 | 154 | w := WebRTCTransportConfig{ 155 | Configuration: webrtc.Configuration{ 156 | ICEServers: iceServers, 157 | SDPSemantics: sdpSemantics, 158 | }, 159 | Setting: se, 160 | Router: c.Router, 161 | BufferFactory: c.BufferFactory, 162 | } 163 | 164 | if len(c.WebRTC.Candidates.NAT1To1IPs) > 0 { 165 | w.Setting.SetNAT1To1IPs(c.WebRTC.Candidates.NAT1To1IPs, webrtc.ICECandidateTypeHost) 166 | } 167 | 168 | if !c.WebRTC.MDNS { 169 | w.Setting.SetICEMulticastDNSMode(ice.MulticastDNSModeDisabled) 170 | } 171 | 172 | if c.SFU.WithStats { 173 | w.Router.WithStats = true 174 | stats.InitStats() 175 | } 176 | 177 | return w 178 | } 179 | 180 | func init() { 181 | // Init packet factory 182 | packetFactory = &sync.Pool{ 183 | New: func() interface{} { 184 | b := make([]byte, 1460) 185 | return &b 186 | }, 187 | } 188 | } 189 | 190 | // NewSFU creates a new sfu instance 191 | func NewSFU(c Config) *SFU { 192 | // Init random seed 193 | rand.Seed(time.Now().UnixNano()) 194 | // Init ballast 195 | ballast := make([]byte, c.SFU.Ballast*1024*1024) 196 | 197 | if c.BufferFactory == nil { 198 | c.BufferFactory = buffer.NewBufferFactory(c.Router.MaxPacketTrack, Logger) 199 | } 200 | 201 | w := NewWebRTCTransportConfig(c) 202 | 203 | sfu := &SFU{ 204 | webrtc: w, 205 | sessions: make(map[string]Session), 206 | withStats: w.Router.WithStats, 207 | } 208 | 209 | if c.Turn.Enabled { 210 | ts, err := InitTurnServer(c.Turn, c.TurnAuth) 211 | if err != nil { 212 | Logger.Error(err, "Could not init turn server err") 213 | os.Exit(1) 214 | } 215 | sfu.turn = ts 216 | } 217 | 218 | runtime.KeepAlive(ballast) 219 | return sfu 220 | } 221 | 222 | // NewSession creates a new SessionLocal instance 223 | func (s *SFU) newSession(id string) Session { 224 | session := NewSession(id, s.datachannels, s.webrtc).(*SessionLocal) 225 | 226 | session.OnClose(func() { 227 | s.Lock() 228 | delete(s.sessions, id) 229 | s.Unlock() 230 | 231 | if s.withStats { 232 | stats.Sessions.Dec() 233 | } 234 | }) 235 | 236 | s.Lock() 237 | s.sessions[id] = session 238 | s.Unlock() 239 | 240 | if s.withStats { 241 | stats.Sessions.Inc() 242 | } 243 | 244 | return session 245 | } 246 | 247 | // GetSession by id 248 | func (s *SFU) getSession(id string) Session { 249 | s.RLock() 250 | defer s.RUnlock() 251 | return s.sessions[id] 252 | } 253 | 254 | func (s *SFU) GetSession(sid string) (Session, WebRTCTransportConfig) { 255 | session := s.getSession(sid) 256 | if session == nil { 257 | session = s.newSession(sid) 258 | } 259 | return session, s.webrtc 260 | } 261 | 262 | func (s *SFU) NewDatachannel(label string) *Datachannel { 263 | dc := &Datachannel{Label: label} 264 | s.datachannels = append(s.datachannels, dc) 265 | return dc 266 | } 267 | 268 | // GetSessions return all sessions 269 | func (s *SFU) GetSessions() []Session { 270 | s.RLock() 271 | defer s.RUnlock() 272 | sessions := make([]Session, 0, len(s.sessions)) 273 | for _, session := range s.sessions { 274 | sessions = append(sessions, session) 275 | } 276 | return sessions 277 | } 278 | -------------------------------------------------------------------------------- /pkg/sfu/simulcast.go: -------------------------------------------------------------------------------- 1 | package sfu 2 | 3 | import "time" 4 | 5 | const ( 6 | quarterResolution = "q" 7 | halfResolution = "h" 8 | fullResolution = "f" 9 | ) 10 | 11 | type SimulcastConfig struct { 12 | BestQualityFirst bool `mapstructure:"bestqualityfirst"` 13 | EnableTemporalLayer bool `mapstructure:"enabletemporallayer"` 14 | } 15 | 16 | type simulcastTrackHelpers struct { 17 | switchDelay time.Time 18 | temporalSupported bool 19 | temporalEnabled bool 20 | lTSCalc int64 21 | 22 | // VP8Helper temporal helpers 23 | pRefPicID uint16 24 | refPicID uint16 25 | lPicID uint16 26 | pRefTlZIdx uint8 27 | refTlZIdx uint8 28 | lTlZIdx uint8 29 | refSN uint16 30 | } 31 | -------------------------------------------------------------------------------- /pkg/sfu/subscriber.go: -------------------------------------------------------------------------------- 1 | package sfu 2 | 3 | import ( 4 | "context" 5 | "io" 6 | "sync" 7 | "time" 8 | 9 | "github.com/bep/debounce" 10 | "github.com/pion/rtcp" 11 | "github.com/pion/webrtc/v3" 12 | ) 13 | 14 | const APIChannelLabel = "ion-sfu" 15 | 16 | type Subscriber struct { 17 | sync.RWMutex 18 | 19 | id string 20 | pc *webrtc.PeerConnection 21 | me *webrtc.MediaEngine 22 | 23 | tracks map[string][]*DownTrack 24 | channels map[string]*webrtc.DataChannel 25 | candidates []webrtc.ICECandidateInit 26 | 27 | negotiate func() 28 | closeOnce sync.Once 29 | 30 | noAutoSubscribe bool 31 | } 32 | 33 | // NewSubscriber creates a new Subscriber 34 | func NewSubscriber(id string, cfg WebRTCTransportConfig) (*Subscriber, error) { 35 | me, err := getSubscriberMediaEngine() 36 | if err != nil { 37 | Logger.Error(err, "NewPeer error") 38 | return nil, errPeerConnectionInitFailed 39 | } 40 | api := webrtc.NewAPI(webrtc.WithMediaEngine(me), webrtc.WithSettingEngine(cfg.Setting)) 41 | pc, err := api.NewPeerConnection(cfg.Configuration) 42 | 43 | if err != nil { 44 | Logger.Error(err, "NewPeer error") 45 | return nil, errPeerConnectionInitFailed 46 | } 47 | 48 | s := &Subscriber{ 49 | id: id, 50 | me: me, 51 | pc: pc, 52 | tracks: make(map[string][]*DownTrack), 53 | channels: make(map[string]*webrtc.DataChannel), 54 | noAutoSubscribe: false, 55 | } 56 | 57 | pc.OnICEConnectionStateChange(func(connectionState webrtc.ICEConnectionState) { 58 | Logger.V(1).Info("ice connection status", "state", connectionState) 59 | switch connectionState { 60 | case webrtc.ICEConnectionStateFailed: 61 | fallthrough 62 | case webrtc.ICEConnectionStateClosed: 63 | s.closeOnce.Do(func() { 64 | Logger.V(1).Info("webrtc ice closed", "peer_id", s.id) 65 | if err := s.Close(); err != nil { 66 | Logger.Error(err, "webrtc transport close err") 67 | } 68 | }) 69 | } 70 | }) 71 | 72 | go s.downTracksReports() 73 | 74 | return s, nil 75 | } 76 | 77 | func (s *Subscriber) AddDatachannel(peer Peer, dc *Datachannel) error { 78 | ndc, err := s.pc.CreateDataChannel(dc.Label, &webrtc.DataChannelInit{}) 79 | if err != nil { 80 | return err 81 | } 82 | 83 | mws := newDCChain(dc.middlewares) 84 | p := mws.Process(ProcessFunc(func(ctx context.Context, args ProcessArgs) { 85 | if dc.onMessage != nil { 86 | dc.onMessage(ctx, args) 87 | } 88 | })) 89 | ndc.OnMessage(func(msg webrtc.DataChannelMessage) { 90 | p.Process(context.Background(), ProcessArgs{ 91 | Peer: peer, 92 | Message: msg, 93 | DataChannel: ndc, 94 | }) 95 | }) 96 | 97 | s.channels[dc.Label] = ndc 98 | 99 | return nil 100 | } 101 | 102 | // DataChannel returns the channel for a label 103 | func (s *Subscriber) DataChannel(label string) *webrtc.DataChannel { 104 | s.RLock() 105 | defer s.RUnlock() 106 | return s.channels[label] 107 | } 108 | 109 | func (s *Subscriber) OnNegotiationNeeded(f func()) { 110 | debounced := debounce.New(250 * time.Millisecond) 111 | s.negotiate = func() { 112 | debounced(f) 113 | } 114 | } 115 | 116 | func (s *Subscriber) CreateOffer() (webrtc.SessionDescription, error) { 117 | offer, err := s.pc.CreateOffer(nil) 118 | if err != nil { 119 | return webrtc.SessionDescription{}, err 120 | } 121 | 122 | err = s.pc.SetLocalDescription(offer) 123 | if err != nil { 124 | return webrtc.SessionDescription{}, err 125 | } 126 | 127 | return offer, nil 128 | } 129 | 130 | // OnICECandidate handler 131 | func (s *Subscriber) OnICECandidate(f func(c *webrtc.ICECandidate)) { 132 | s.pc.OnICECandidate(f) 133 | } 134 | 135 | // AddICECandidate to peer connection 136 | func (s *Subscriber) AddICECandidate(candidate webrtc.ICECandidateInit) error { 137 | if s.pc.RemoteDescription() != nil { 138 | return s.pc.AddICECandidate(candidate) 139 | } 140 | s.candidates = append(s.candidates, candidate) 141 | return nil 142 | } 143 | 144 | func (s *Subscriber) AddDownTrack(streamID string, downTrack *DownTrack) { 145 | s.Lock() 146 | defer s.Unlock() 147 | if dt, ok := s.tracks[streamID]; ok { 148 | dt = append(dt, downTrack) 149 | s.tracks[streamID] = dt 150 | } else { 151 | s.tracks[streamID] = []*DownTrack{downTrack} 152 | } 153 | } 154 | 155 | func (s *Subscriber) RemoveDownTrack(streamID string, downTrack *DownTrack) { 156 | s.Lock() 157 | defer s.Unlock() 158 | if dts, ok := s.tracks[streamID]; ok { 159 | idx := -1 160 | for i, dt := range dts { 161 | if dt == downTrack { 162 | idx = i 163 | break 164 | } 165 | } 166 | if idx >= 0 { 167 | dts[idx] = dts[len(dts)-1] 168 | dts[len(dts)-1] = nil 169 | dts = dts[:len(dts)-1] 170 | s.tracks[streamID] = dts 171 | } 172 | } 173 | } 174 | 175 | func (s *Subscriber) AddDataChannel(label string) (*webrtc.DataChannel, error) { 176 | s.Lock() 177 | defer s.Unlock() 178 | 179 | if s.channels[label] != nil { 180 | return s.channels[label], nil 181 | } 182 | 183 | dc, err := s.pc.CreateDataChannel(label, &webrtc.DataChannelInit{}) 184 | if err != nil { 185 | Logger.Error(err, "dc creation error") 186 | return nil, errCreatingDataChannel 187 | } 188 | 189 | s.channels[label] = dc 190 | 191 | return dc, nil 192 | } 193 | 194 | // SetRemoteDescription sets the SessionDescription of the remote peer 195 | func (s *Subscriber) SetRemoteDescription(desc webrtc.SessionDescription) error { 196 | if err := s.pc.SetRemoteDescription(desc); err != nil { 197 | Logger.Error(err, "SetRemoteDescription error") 198 | return err 199 | } 200 | 201 | for _, c := range s.candidates { 202 | if err := s.pc.AddICECandidate(c); err != nil { 203 | Logger.Error(err, "Add subscriber ice candidate to peer err", "peer_id", s.id) 204 | } 205 | } 206 | s.candidates = nil 207 | 208 | return nil 209 | } 210 | 211 | func (s *Subscriber) RegisterDatachannel(label string, dc *webrtc.DataChannel) { 212 | s.Lock() 213 | s.channels[label] = dc 214 | s.Unlock() 215 | } 216 | 217 | func (s *Subscriber) GetDatachannel(label string) *webrtc.DataChannel { 218 | return s.DataChannel(label) 219 | } 220 | 221 | func (s *Subscriber) DownTracks() []*DownTrack { 222 | s.RLock() 223 | defer s.RUnlock() 224 | var downTracks []*DownTrack 225 | for _, tracks := range s.tracks { 226 | downTracks = append(downTracks, tracks...) 227 | } 228 | return downTracks 229 | } 230 | 231 | func (s *Subscriber) GetDownTracks(streamID string) []*DownTrack { 232 | s.RLock() 233 | defer s.RUnlock() 234 | return s.tracks[streamID] 235 | } 236 | 237 | // Negotiate fires a debounced negotiation request 238 | func (s *Subscriber) Negotiate() { 239 | s.negotiate() 240 | } 241 | 242 | // Close peer 243 | func (s *Subscriber) Close() error { 244 | return s.pc.Close() 245 | } 246 | 247 | func (s *Subscriber) downTracksReports() { 248 | for { 249 | time.Sleep(5 * time.Second) 250 | 251 | if s.pc.ConnectionState() == webrtc.PeerConnectionStateClosed { 252 | return 253 | } 254 | 255 | var r []rtcp.Packet 256 | var sd []rtcp.SourceDescriptionChunk 257 | s.RLock() 258 | for _, dts := range s.tracks { 259 | for _, dt := range dts { 260 | if !dt.bound.get() { 261 | continue 262 | } 263 | if sr := dt.CreateSenderReport(); sr != nil { 264 | r = append(r, sr) 265 | } 266 | sd = append(sd, dt.CreateSourceDescriptionChunks()...) 267 | } 268 | } 269 | s.RUnlock() 270 | i := 0 271 | j := 0 272 | for i < len(sd) { 273 | i = (j + 1) * 15 274 | if i >= len(sd) { 275 | i = len(sd) 276 | } 277 | nsd := sd[j*15 : i] 278 | r = append(r, &rtcp.SourceDescription{Chunks: nsd}) 279 | j++ 280 | if err := s.pc.WriteRTCP(r); err != nil { 281 | if err == io.EOF || err == io.ErrClosedPipe { 282 | return 283 | } 284 | Logger.Error(err, "Sending downtrack reports err") 285 | } 286 | r = r[:0] 287 | } 288 | } 289 | } 290 | 291 | func (s *Subscriber) sendStreamDownTracksReports(streamID string) { 292 | var r []rtcp.Packet 293 | var sd []rtcp.SourceDescriptionChunk 294 | 295 | s.RLock() 296 | dts := s.tracks[streamID] 297 | for _, dt := range dts { 298 | if !dt.bound.get() { 299 | continue 300 | } 301 | sd = append(sd, dt.CreateSourceDescriptionChunks()...) 302 | } 303 | s.RUnlock() 304 | if len(sd) == 0 { 305 | return 306 | } 307 | r = append(r, &rtcp.SourceDescription{Chunks: sd}) 308 | go func() { 309 | r := r 310 | i := 0 311 | for { 312 | if err := s.pc.WriteRTCP(r); err != nil { 313 | Logger.Error(err, "Sending track binding reports err") 314 | } 315 | if i > 5 { 316 | return 317 | } 318 | i++ 319 | time.Sleep(20 * time.Millisecond) 320 | } 321 | }() 322 | } 323 | -------------------------------------------------------------------------------- /pkg/sfu/turn.go: -------------------------------------------------------------------------------- 1 | package sfu 2 | 3 | import ( 4 | "context" 5 | "crypto/rand" 6 | "crypto/tls" 7 | "fmt" 8 | "net" 9 | "os" 10 | "regexp" 11 | "strconv" 12 | "strings" 13 | "time" 14 | 15 | "github.com/pion/dtls/v2" 16 | "github.com/pion/logging" 17 | "github.com/pion/turn/v2" 18 | ) 19 | 20 | const ( 21 | turnMinPort = 32768 22 | turnMaxPort = 46883 23 | sfuMinPort = 46884 24 | sfuMaxPort = 60999 25 | ) 26 | 27 | type TurnAuth struct { 28 | Credentials string `mapstructure:"credentials"` 29 | Secret string `mapstructure:"secret"` 30 | } 31 | 32 | // WebRTCConfig defines parameters for ice 33 | type TurnConfig struct { 34 | Enabled bool `mapstructure:"enabled"` 35 | Realm string `mapstructure:"realm"` 36 | Address string `mapstructure:"address"` 37 | Cert string `mapstructure:"cert"` 38 | Key string `mapstructure:"key"` 39 | Auth TurnAuth `mapstructure:"auth"` 40 | PortRange []uint16 `mapstructure:"portrange"` 41 | } 42 | 43 | func InitTurnServer(conf TurnConfig, auth func(username, realm string, srcAddr net.Addr) ([]byte, bool)) (*turn.Server, error) { 44 | var listeners []turn.ListenerConfig 45 | 46 | // Create a UDP listener to pass into pion/turn 47 | // pion/turn itself doesn't allocate any UDP sockets, but lets the user pass them in 48 | // this allows us to add logging, storage or modify inbound/outbound traffic 49 | udpListener, err := net.ListenPacket("udp4", conf.Address) 50 | if err != nil { 51 | return nil, err 52 | } 53 | // Create a TCP listener to pass into pion/turn 54 | // pion/turn itself doesn't allocate any TCP listeners, but lets the user pass them in 55 | // this allows us to add logging, storage or modify inbound/outbound traffic 56 | tcpListener, err := net.Listen("tcp4", conf.Address) 57 | if err != nil { 58 | return nil, err 59 | } 60 | 61 | addr := strings.Split(conf.Address, ":") 62 | 63 | var minPort uint16 = turnMinPort 64 | var maxPort uint16 = turnMaxPort 65 | 66 | if len(conf.PortRange) == 2 { 67 | minPort = conf.PortRange[0] 68 | maxPort = conf.PortRange[1] 69 | } 70 | 71 | if len(conf.Cert) > 0 && len(conf.Key) > 0 { 72 | // Create a TLS listener to pass into pion/turn 73 | cert, err := tls.LoadX509KeyPair(conf.Cert, conf.Key) 74 | if err != nil { 75 | return nil, err 76 | } 77 | config := tls.Config{Certificates: []tls.Certificate{cert}} 78 | config.Rand = rand.Reader 79 | tlsListener := tls.NewListener(tcpListener, &config) 80 | listeners = append(listeners, turn.ListenerConfig{ 81 | Listener: tlsListener, 82 | RelayAddressGenerator: &turn.RelayAddressGeneratorPortRange{ 83 | RelayAddress: net.ParseIP(addr[0]), 84 | Address: "0.0.0.0", 85 | MinPort: minPort, 86 | MaxPort: maxPort, 87 | }, 88 | }) 89 | // Create a DTLS listener to pass into pion/turn 90 | ctx := context.Background() 91 | dtlsConf := &dtls.Config{ 92 | Certificates: []tls.Certificate{cert}, 93 | ExtendedMasterSecret: dtls.RequireExtendedMasterSecret, 94 | // Create timeout context for accepted connection. 95 | ConnectContextMaker: func() (context.Context, func()) { 96 | return context.WithTimeout(ctx, 30*time.Second) 97 | }, 98 | } 99 | port, err := strconv.ParseInt(addr[1], 10, 64) 100 | if err != nil { 101 | return nil, err 102 | } 103 | a := &net.UDPAddr{IP: net.ParseIP(addr[0]), Port: int(port)} 104 | dtlsListener, err := dtls.Listen("udp4", a, dtlsConf) 105 | if err != nil { 106 | return nil, err 107 | } 108 | listeners = append(listeners, turn.ListenerConfig{ 109 | Listener: dtlsListener, 110 | RelayAddressGenerator: &turn.RelayAddressGeneratorPortRange{ 111 | RelayAddress: net.ParseIP(addr[0]), 112 | Address: "0.0.0.0", 113 | MinPort: minPort, 114 | MaxPort: maxPort, 115 | }, 116 | }) 117 | } 118 | 119 | if auth == nil { 120 | if conf.Auth.Secret != "" { 121 | logger := logging.NewDefaultLeveledLoggerForScope("lt-creds", logging.LogLevelTrace, os.Stdout) 122 | auth = turn.NewLongTermAuthHandler(conf.Auth.Secret, logger) 123 | } else { 124 | usersMap := map[string][]byte{} 125 | for _, kv := range regexp.MustCompile(`(\w+)=(\w+)`).FindAllStringSubmatch(conf.Auth.Credentials, -1) { 126 | usersMap[kv[1]] = turn.GenerateAuthKey(kv[1], conf.Realm, kv[2]) 127 | } 128 | if len(usersMap) == 0 { 129 | Logger.Error(fmt.Errorf("No turn auth provided"), "Got err") 130 | } 131 | auth = func(username string, realm string, srcAddr net.Addr) ([]byte, bool) { 132 | if key, ok := usersMap[username]; ok { 133 | return key, true 134 | } 135 | return nil, false 136 | } 137 | } 138 | } 139 | 140 | return turn.NewServer(turn.ServerConfig{ 141 | Realm: conf.Realm, 142 | // Set AuthHandler callback 143 | // This is called everytime a user tries to authenticate with the TURN server 144 | // Return the key for that user, or false when no user is found 145 | AuthHandler: auth, 146 | // ListenerConfig is a list of Listeners and the Configuration around them 147 | ListenerConfigs: append(listeners, turn.ListenerConfig{ 148 | Listener: tcpListener, 149 | RelayAddressGenerator: &turn.RelayAddressGeneratorPortRange{ 150 | RelayAddress: net.ParseIP(addr[0]), 151 | Address: "0.0.0.0", 152 | MinPort: minPort, 153 | MaxPort: maxPort, 154 | }, 155 | }, 156 | ), 157 | // PacketConnConfigs is a list of UDP Listeners and the Configuration around them 158 | PacketConnConfigs: []turn.PacketConnConfig{ 159 | { 160 | PacketConn: udpListener, 161 | RelayAddressGenerator: &turn.RelayAddressGeneratorPortRange{ 162 | RelayAddress: net.ParseIP(addr[0]), 163 | Address: "0.0.0.0", 164 | MinPort: minPort, 165 | MaxPort: maxPort, 166 | }, 167 | }, 168 | }, 169 | }) 170 | } 171 | -------------------------------------------------------------------------------- /pkg/stats/stream.go: -------------------------------------------------------------------------------- 1 | package stats 2 | 3 | import ( 4 | "math" 5 | "sync" 6 | "sync/atomic" 7 | 8 | "github.com/pion/ion-sfu/pkg/buffer" 9 | "github.com/prometheus/client_golang/prometheus" 10 | ) 11 | 12 | var ( 13 | driftBuckets = []float64{5, 10, 20, 40, 80, 160, math.Inf(+1)} 14 | 15 | drift = prometheus.NewHistogram(prometheus.HistogramOpts{ 16 | Subsystem: "rtp", 17 | Name: "drift_millis", 18 | Buckets: driftBuckets, 19 | }) 20 | 21 | expectedCount = prometheus.NewCounter(prometheus.CounterOpts{ 22 | Subsystem: "rtp", 23 | Name: "expected", 24 | }) 25 | 26 | receivedCount = prometheus.NewCounter(prometheus.CounterOpts{ 27 | Subsystem: "rtp", 28 | Name: "received", 29 | }) 30 | 31 | packetCount = prometheus.NewCounter(prometheus.CounterOpts{ 32 | Subsystem: "rtp", 33 | Name: "packets", 34 | }) 35 | 36 | totalBytes = prometheus.NewCounter(prometheus.CounterOpts{ 37 | Subsystem: "rtp", 38 | Name: "bytes", 39 | }) 40 | 41 | expectedMinusReceived = prometheus.NewSummary(prometheus.SummaryOpts{ 42 | Subsystem: "rtp", 43 | Name: "expected_minus_received", 44 | }) 45 | 46 | lostRate = prometheus.NewSummary(prometheus.SummaryOpts{ 47 | Subsystem: "rtp", 48 | Name: "lost_rate", 49 | }) 50 | 51 | jitter = prometheus.NewSummary(prometheus.SummaryOpts{ 52 | Subsystem: "rtp", 53 | Name: "jitter", 54 | }) 55 | 56 | Sessions = prometheus.NewGauge(prometheus.GaugeOpts{ 57 | Subsystem: "sfu", 58 | Name: "sessions", 59 | Help: "Current number of sessions", 60 | }) 61 | 62 | Peers = prometheus.NewGauge(prometheus.GaugeOpts{ 63 | Subsystem: "sfu", 64 | Name: "peers", 65 | Help: "Current number of peers connected", 66 | }) 67 | 68 | AudioTracks = prometheus.NewGauge(prometheus.GaugeOpts{ 69 | Subsystem: "sfu", 70 | Name: "audio_tracks", 71 | Help: "Current number of audio tracks", 72 | }) 73 | 74 | VideoTracks = prometheus.NewGauge(prometheus.GaugeOpts{ 75 | Subsystem: "sfu", 76 | Name: "video_tracks", 77 | Help: "Current number of video tracks", 78 | }) 79 | ) 80 | 81 | func InitStats() { 82 | prometheus.MustRegister(drift) 83 | prometheus.MustRegister(expectedCount) 84 | prometheus.MustRegister(receivedCount) 85 | prometheus.MustRegister(packetCount) 86 | prometheus.MustRegister(totalBytes) 87 | prometheus.MustRegister(expectedMinusReceived) 88 | prometheus.MustRegister(lostRate) 89 | prometheus.MustRegister(jitter) 90 | prometheus.MustRegister(Sessions) 91 | prometheus.MustRegister(Peers) 92 | prometheus.MustRegister(AudioTracks) 93 | prometheus.MustRegister(VideoTracks) 94 | } 95 | 96 | // Stream contains buffer statistics 97 | type Stream struct { 98 | sync.RWMutex 99 | Buffer *buffer.Buffer 100 | cname string 101 | driftInMillis uint64 102 | hasStats bool 103 | lastStats buffer.Stats 104 | diffStats buffer.Stats 105 | } 106 | 107 | // NewStream constructs a new Stream 108 | func NewStream(buffer *buffer.Buffer) *Stream { 109 | s := &Stream{ 110 | Buffer: buffer, 111 | } 112 | return s 113 | } 114 | 115 | // GetCName returns the cname for a given stream 116 | func (s *Stream) GetCName() string { 117 | s.RLock() 118 | defer s.RUnlock() 119 | 120 | return s.cname 121 | } 122 | 123 | func (s *Stream) SetCName(cname string) { 124 | s.Lock() 125 | defer s.Unlock() 126 | 127 | s.cname = cname 128 | } 129 | 130 | func (s *Stream) SetDriftInMillis(driftInMillis uint64) { 131 | atomic.StoreUint64(&s.driftInMillis, driftInMillis) 132 | } 133 | 134 | func (s *Stream) GetDriftInMillis() uint64 { 135 | return atomic.LoadUint64(&s.driftInMillis) 136 | } 137 | 138 | func (s *Stream) UpdateStats(stats buffer.Stats) (hasDiff bool, diffStats buffer.Stats) { 139 | s.Lock() 140 | defer s.Unlock() 141 | 142 | hadStats := false 143 | 144 | if s.hasStats { 145 | s.diffStats.LastExpected = stats.LastExpected - s.lastStats.LastExpected 146 | s.diffStats.LastReceived = stats.LastReceived - s.lastStats.LastReceived 147 | s.diffStats.PacketCount = stats.PacketCount - s.lastStats.PacketCount 148 | s.diffStats.TotalByte = stats.TotalByte - s.lastStats.TotalByte 149 | hadStats = true 150 | } 151 | 152 | s.lastStats = stats 153 | s.hasStats = true 154 | 155 | return hadStats, s.diffStats 156 | } 157 | 158 | func (s *Stream) CalcStats() { 159 | bufferStats := s.Buffer.GetStats() 160 | driftInMillis := s.GetDriftInMillis() 161 | 162 | hadStats, diffStats := s.UpdateStats(bufferStats) 163 | 164 | drift.Observe(float64(driftInMillis)) 165 | if hadStats { 166 | expectedCount.Add(float64(diffStats.LastExpected)) 167 | receivedCount.Add(float64(diffStats.LastReceived)) 168 | packetCount.Add(float64(diffStats.PacketCount)) 169 | totalBytes.Add(float64(diffStats.TotalByte)) 170 | } 171 | 172 | expectedMinusReceived.Observe(float64(bufferStats.LastExpected - bufferStats.LastReceived)) 173 | lostRate.Observe(float64(bufferStats.LostRate)) 174 | jitter.Observe(bufferStats.Jitter) 175 | } 176 | -------------------------------------------------------------------------------- /pkg/twcc/twcc_test.go: -------------------------------------------------------------------------------- 1 | package twcc 2 | 3 | import ( 4 | "math/rand" 5 | "testing" 6 | "time" 7 | 8 | "github.com/pion/rtcp" 9 | "github.com/stretchr/testify/assert" 10 | ) 11 | 12 | func TestTransportWideCC_writeRunLengthChunk(t1 *testing.T) { 13 | type fields struct { 14 | len uint16 15 | } 16 | type args struct { 17 | symbol uint16 18 | runLength uint16 19 | } 20 | tests := []struct { 21 | name string 22 | fields fields 23 | args args 24 | wantErr bool 25 | wantBytes []byte 26 | }{ 27 | { 28 | name: "Must not return error", 29 | 30 | args: args{ 31 | symbol: rtcp.TypeTCCPacketNotReceived, 32 | runLength: 221, 33 | }, 34 | wantErr: false, 35 | wantBytes: []byte{0, 0xdd}, 36 | }, { 37 | name: "Must set run length after padding", 38 | fields: fields{ 39 | len: 1, 40 | }, 41 | args: args{ 42 | symbol: rtcp.TypeTCCPacketReceivedWithoutDelta, 43 | runLength: 24, 44 | }, 45 | wantBytes: []byte{0, 0x60, 0x18}, 46 | }, 47 | } 48 | for _, tt := range tests { 49 | tt := tt 50 | t1.Run(tt.name, func(t1 *testing.T) { 51 | t := &Responder{ 52 | len: tt.fields.len, 53 | } 54 | t.writeRunLengthChunk(tt.args.symbol, tt.args.runLength) 55 | assert.Equal(t1, tt.wantBytes, t.payload[:t.len]) 56 | }) 57 | } 58 | } 59 | 60 | func TestTransportWideCC_writeStatusSymbolChunk(t1 *testing.T) { 61 | type fields struct { 62 | len uint16 63 | } 64 | type args struct { 65 | symbolSize uint16 66 | symbolList []uint16 67 | } 68 | tests := []struct { 69 | name string 70 | fields fields 71 | args args 72 | wantBytes []byte 73 | }{ 74 | { 75 | name: "Must not return error", 76 | args: args{ 77 | symbolSize: rtcp.TypeTCCSymbolSizeOneBit, 78 | symbolList: []uint16{rtcp.TypeTCCPacketNotReceived, 79 | rtcp.TypeTCCPacketReceivedSmallDelta, 80 | rtcp.TypeTCCPacketReceivedSmallDelta, 81 | rtcp.TypeTCCPacketReceivedSmallDelta, 82 | rtcp.TypeTCCPacketReceivedSmallDelta, 83 | rtcp.TypeTCCPacketReceivedSmallDelta, 84 | rtcp.TypeTCCPacketNotReceived, 85 | rtcp.TypeTCCPacketNotReceived, 86 | rtcp.TypeTCCPacketNotReceived, 87 | rtcp.TypeTCCPacketReceivedSmallDelta, 88 | rtcp.TypeTCCPacketReceivedSmallDelta, 89 | rtcp.TypeTCCPacketReceivedSmallDelta, 90 | rtcp.TypeTCCPacketNotReceived, 91 | rtcp.TypeTCCPacketNotReceived}, 92 | }, 93 | wantBytes: []byte{0x9F, 0x1C}, 94 | }, 95 | { 96 | name: "Must set symbol chunk after padding", 97 | fields: fields{ 98 | len: 1, 99 | }, 100 | args: args{ 101 | symbolSize: rtcp.TypeTCCSymbolSizeTwoBit, 102 | symbolList: []uint16{ 103 | rtcp.TypeTCCPacketNotReceived, 104 | rtcp.TypeTCCPacketReceivedWithoutDelta, 105 | rtcp.TypeTCCPacketReceivedSmallDelta, 106 | rtcp.TypeTCCPacketReceivedSmallDelta, 107 | rtcp.TypeTCCPacketReceivedSmallDelta, 108 | rtcp.TypeTCCPacketNotReceived, 109 | rtcp.TypeTCCPacketNotReceived}, 110 | }, 111 | wantBytes: []byte{0x0, 0xcd, 0x50}, 112 | }, 113 | } 114 | for _, tt := range tests { 115 | tt := tt 116 | t1.Run(tt.name, func(t1 *testing.T) { 117 | t := &Responder{ 118 | len: tt.fields.len, 119 | } 120 | for i, v := range tt.args.symbolList { 121 | t.createStatusSymbolChunk(tt.args.symbolSize, v, i) 122 | } 123 | t.writeStatusSymbolChunk(tt.args.symbolSize) 124 | assert.Equal(t1, tt.wantBytes, t.payload[:t.len]) 125 | }) 126 | } 127 | } 128 | 129 | func TestTransportWideCC_writeDelta(t1 *testing.T) { 130 | a := -32768 131 | type fields struct { 132 | deltaLen uint16 133 | } 134 | type args struct { 135 | deltaType uint16 136 | delta uint16 137 | } 138 | tests := []struct { 139 | name string 140 | fields fields 141 | args args 142 | want []byte 143 | }{ 144 | { 145 | name: "Must set correct small delta", 146 | args: args{ 147 | deltaType: rtcp.TypeTCCPacketReceivedSmallDelta, 148 | delta: 255, 149 | }, 150 | want: []byte{0xff}, 151 | }, 152 | { 153 | name: "Must set correct small delta with padding", 154 | fields: fields{ 155 | deltaLen: 1, 156 | }, 157 | args: args{ 158 | deltaType: rtcp.TypeTCCPacketReceivedSmallDelta, 159 | delta: 255, 160 | }, 161 | want: []byte{0, 0xff}, 162 | }, 163 | { 164 | name: "Must set correct large delta", 165 | args: args{ 166 | deltaType: rtcp.TypeTCCPacketReceivedLargeDelta, 167 | delta: 32767, 168 | }, 169 | want: []byte{0x7F, 0xFF}, 170 | }, 171 | { 172 | name: "Must set correct large delta with padding", 173 | fields: fields{ 174 | deltaLen: 1, 175 | }, 176 | args: args{ 177 | deltaType: rtcp.TypeTCCPacketReceivedLargeDelta, 178 | delta: uint16(a), 179 | }, 180 | want: []byte{0, 0x80, 0x00}, 181 | }, 182 | } 183 | for _, tt := range tests { 184 | tt := tt 185 | t1.Run(tt.name, func(t1 *testing.T) { 186 | t := &Responder{ 187 | deltaLen: tt.fields.deltaLen, 188 | } 189 | t.writeDelta(tt.args.deltaType, tt.args.delta) 190 | assert.Equal(t1, tt.want, t.deltas[:t.deltaLen]) 191 | assert.Equal(t1, tt.fields.deltaLen+tt.args.deltaType, t.deltaLen) 192 | }) 193 | } 194 | } 195 | 196 | func TestTransportWideCC_writeHeader(t1 *testing.T) { 197 | type fields struct { 198 | tccPktCtn uint8 199 | sSSRC uint32 200 | mSSRC uint32 201 | } 202 | type args struct { 203 | bSN uint16 204 | packetCount uint16 205 | refTime uint32 206 | } 207 | tests := []struct { 208 | name string 209 | fields fields 210 | args args 211 | want []byte 212 | }{ 213 | { 214 | name: "Must construct correct header", 215 | fields: fields{ 216 | tccPktCtn: 23, 217 | sSSRC: 4195875351, 218 | mSSRC: 1124282272, 219 | }, 220 | args: args{ 221 | bSN: 153, 222 | packetCount: 1, 223 | refTime: 4057090, 224 | }, 225 | want: []byte{ 226 | 0xfa, 0x17, 0xfa, 0x17, 227 | 0x43, 0x3, 0x2f, 0xa0, 228 | 0x0, 0x99, 0x0, 0x1, 229 | 0x3d, 0xe8, 0x2, 0x17}, 230 | }, 231 | } 232 | for _, tt := range tests { 233 | tt := tt 234 | t1.Run(tt.name, func(t1 *testing.T) { 235 | t := &Responder{ 236 | pktCtn: tt.fields.tccPktCtn, 237 | sSSRC: tt.fields.sSSRC, 238 | mSSRC: tt.fields.mSSRC, 239 | } 240 | t.writeHeader(tt.args.bSN, tt.args.packetCount, tt.args.refTime) 241 | assert.Equal(t1, tt.want, t.payload[0:16]) 242 | }) 243 | } 244 | } 245 | 246 | func TestTccPacket(t1 *testing.T) { 247 | want := []byte{ 248 | 0xfa, 0x17, 0xfa, 0x17, 249 | 0x43, 0x3, 0x2f, 0xa0, 250 | 0x0, 0x99, 0x0, 0x1, 251 | 0x3d, 0xe8, 0x2, 0x17, 252 | 0x60, 0x18, 0x0, 0xdd, 253 | 0x9F, 0x1C, 0xcd, 0x50, 254 | } 255 | 256 | delta := []byte{ 257 | 0xff, 0x80, 0xaa, 258 | } 259 | 260 | symbol1 := []uint16{rtcp.TypeTCCPacketNotReceived, 261 | rtcp.TypeTCCPacketReceivedSmallDelta, 262 | rtcp.TypeTCCPacketReceivedSmallDelta, 263 | rtcp.TypeTCCPacketReceivedSmallDelta, 264 | rtcp.TypeTCCPacketReceivedSmallDelta, 265 | rtcp.TypeTCCPacketReceivedSmallDelta, 266 | rtcp.TypeTCCPacketNotReceived, 267 | rtcp.TypeTCCPacketNotReceived, 268 | rtcp.TypeTCCPacketNotReceived, 269 | rtcp.TypeTCCPacketReceivedSmallDelta, 270 | rtcp.TypeTCCPacketReceivedSmallDelta, 271 | rtcp.TypeTCCPacketReceivedSmallDelta, 272 | rtcp.TypeTCCPacketNotReceived, 273 | rtcp.TypeTCCPacketNotReceived} 274 | symbol2 := []uint16{ 275 | rtcp.TypeTCCPacketNotReceived, 276 | rtcp.TypeTCCPacketReceivedWithoutDelta, 277 | rtcp.TypeTCCPacketReceivedSmallDelta, 278 | rtcp.TypeTCCPacketReceivedSmallDelta, 279 | rtcp.TypeTCCPacketReceivedSmallDelta, 280 | rtcp.TypeTCCPacketNotReceived, 281 | rtcp.TypeTCCPacketNotReceived} 282 | 283 | t := &Responder{ 284 | pktCtn: 23, 285 | sSSRC: 4195875351, 286 | mSSRC: 1124282272, 287 | } 288 | t.writeHeader(153, 1, 4057090) 289 | t.writeRunLengthChunk(rtcp.TypeTCCPacketReceivedWithoutDelta, 24) 290 | t.writeRunLengthChunk(rtcp.TypeTCCPacketNotReceived, 221) 291 | for i, v := range symbol1 { 292 | t.createStatusSymbolChunk(rtcp.TypeTCCSymbolSizeOneBit, v, i) 293 | } 294 | t.writeStatusSymbolChunk(rtcp.TypeTCCSymbolSizeOneBit) 295 | for i, v := range symbol2 { 296 | t.createStatusSymbolChunk(rtcp.TypeTCCSymbolSizeTwoBit, v, i) 297 | } 298 | t.writeStatusSymbolChunk(rtcp.TypeTCCSymbolSizeTwoBit) 299 | t.deltaLen = uint16(len(delta)) 300 | assert.Equal(t1, want, t.payload[:24]) 301 | 302 | pLen := t.len + t.deltaLen + 4 303 | pad := pLen%4 != 0 304 | for pLen%4 != 0 { 305 | pLen++ 306 | } 307 | hdr := rtcp.Header{ 308 | Padding: pad, 309 | Length: (pLen / 4) - 1, 310 | Count: rtcp.FormatTCC, 311 | Type: rtcp.TypeTransportSpecificFeedback, 312 | } 313 | assert.Equal(t1, int(pLen), len(want)+3+4+1) 314 | hb, _ := hdr.Marshal() 315 | pkt := make([]byte, pLen) 316 | copy(pkt, hb) 317 | assert.Equal(t1, hb, pkt[:len(hb)]) 318 | copy(pkt[4:], t.payload[:t.len]) 319 | assert.Equal(t1, append(hb, t.payload[:t.len]...), pkt[:len(hb)+int(t.len)]) 320 | copy(pkt[4+t.len:], delta[:t.deltaLen]) 321 | assert.Equal(t1, delta, pkt[len(hb)+int(t.len):len(pkt)-1]) 322 | var ss rtcp.TransportLayerCC 323 | err := ss.Unmarshal(pkt) 324 | assert.NoError(t1, err) 325 | 326 | assert.Equal(t1, hdr, ss.Header) 327 | 328 | } 329 | 330 | func BenchmarkBuildPacket(b *testing.B) { 331 | rand.Seed(time.Now().UnixNano()) 332 | b.ReportAllocs() 333 | n := 1 + rand.Intn(4-1+1) 334 | var twcc Responder 335 | tm := time.Now() 336 | for i := 1; i < 100; i++ { 337 | tm := tm.Add(time.Duration(60*n) * time.Millisecond) 338 | twcc.extInfo = append(twcc.extInfo, rtpExtInfo{ 339 | ExtTSN: uint32(i), 340 | Timestamp: tm.UnixNano(), 341 | }) 342 | } 343 | for i := 0; i < b.N; i++ { 344 | _ = twcc.buildTransportCCPacket() 345 | } 346 | } 347 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "config:base" 4 | ], 5 | "postUpdateOptions": [ 6 | "gomodTidy" 7 | ], 8 | "commitBody": "Generated by renovateBot", 9 | "packageRules": [ 10 | { 11 | "packagePatterns": ["^golang.org/x/"], 12 | "schedule": ["on the first day of the month"] 13 | } 14 | ] 15 | } 16 | --------------------------------------------------------------------------------