├── .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 |
9 |
10 |
11 |
12 |
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 |
40 |
43 |
46 |
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 |
75 |
76 |
77 |
Video
78 |
79 |
82 |
83 |
84 |
86 |
87 |
88 |
89 |
Audio
90 |
91 |
94 |
95 |
96 |
98 |
99 |
100 |
101 |
102 |
103 |
104 |
Remotes
105 |
106 |
111 |
112 |
117 |
122 |
123 |
124 |
125 |
126 |
129 |
146 |
147 |
DataChannel Message
148 |
149 |
150 |
151 |
163 |
166 |
167 |
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 |
55 |
56 |
57 |
Video
58 |
59 |
69 |
70 |
71 |
80 |
81 |
82 |
83 |
Audio
84 |
85 |
95 |
96 |
97 |
106 |
107 |
108 |
109 |
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 |
--------------------------------------------------------------------------------