├── proto ├── auth │ └── auth.proto ├── islb │ ├── islb.proto │ ├── islb_grpc.pb.go │ └── islb.pb.go ├── ion │ ├── ion.proto │ └── ion.pb.go ├── debug │ ├── debug.proto │ └── debug.pb.go ├── Dockerfile └── rtc │ ├── rtc.proto │ └── rtc_grpc.pb.go ├── apps ├── auth │ ├── server │ │ └── service.go │ ├── main.go │ └── proto │ │ └── auth.proto ├── conference │ ├── README.md │ └── main.go └── room │ ├── main.go │ ├── server │ ├── peer.go │ ├── room_signal.go │ └── room.go │ └── proto │ └── room.proto ├── .github ├── hooks │ ├── pre-commit.sh │ ├── pre-push.sh │ └── commit-msg.sh ├── FUNDING.yml ├── install-hooks.sh ├── workflows │ ├── build.yaml │ ├── lint.yaml │ ├── renovate-go-mod-fix.yaml │ ├── docker-sfu.yaml │ ├── docker-islb.yaml │ ├── docker-signal.yaml │ └── docker-app-room.yaml └── lint-filename.sh ├── pkg ├── proto │ └── proto.go ├── auth │ ├── claims.go │ ├── wrappers.go │ ├── jwt.go │ └── auth.go ├── util │ ├── atomic.go │ ├── nats.go │ ├── service.go │ ├── util.go │ ├── grpc.go │ └── wrapped.go ├── error │ ├── code.go │ └── error.go ├── node │ ├── islb │ │ ├── islb_test.go │ │ ├── server.go │ │ ├── islb.go │ │ └── registry.go │ ├── sfu │ │ ├── sfu_test.go │ │ └── sfu.go │ └── signal │ │ └── signal.go ├── ion │ ├── node_test.go │ └── node.go ├── runner │ └── runner.go └── db │ ├── redis_test.go │ └── redis.go ├── codecov.yml ├── configs ├── docker │ ├── islb.toml │ ├── app-room.toml │ ├── signal.toml │ └── sfu.toml ├── islb.toml ├── app-room.toml ├── signal.toml └── sfu.toml ├── examples ├── sfu-v2 │ ├── README.md │ ├── main.go │ └── index.html ├── webserver │ ├── README.md │ └── main.go └── ion-pubsub │ ├── index.html │ └── main.js ├── .dockerignore ├── .gitignore ├── renovate.json ├── docker ├── sfu.Dockerfile ├── islb.Dockerfile ├── signal.Dockerfile └── app-room.Dockerfile ├── scripts ├── key ├── deps_inst ├── README.md ├── deps ├── all ├── service └── common ├── go.mod ├── LICENSE ├── cmd ├── sfu │ └── main.go ├── islb │ └── main.go └── signal │ └── main.go ├── README.md ├── docker-compose.yml └── Makefile /proto/auth/auth.proto: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /apps/auth/server/service.go: -------------------------------------------------------------------------------- 1 | package server 2 | -------------------------------------------------------------------------------- /.github/hooks/pre-commit.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | exec 1>&2 -------------------------------------------------------------------------------- /.github/hooks/pre-push.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | exit 0 4 | -------------------------------------------------------------------------------- /apps/auth/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | func main() { 4 | } 5 | -------------------------------------------------------------------------------- /.github/hooks/commit-msg.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -e 4 | 5 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | open_collective: pion-ion 3 | 4 | -------------------------------------------------------------------------------- /pkg/proto/proto.go: -------------------------------------------------------------------------------- 1 | package proto 2 | 3 | const ( 4 | ServiceALL = "*" 5 | ServiceISLB = "islb" 6 | ServiceROOM = "room" 7 | ServiceRTC = "rtc" 8 | ServiceAVP = "avp" 9 | ServiceSIG = "signal" 10 | ) 11 | -------------------------------------------------------------------------------- /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/*" -------------------------------------------------------------------------------- /configs/docker/islb.toml: -------------------------------------------------------------------------------- 1 | [global] 2 | # data center id 3 | dc = "dc1" 4 | 5 | [log] 6 | level = "info" 7 | 8 | [nats] 9 | url = "nats://nats:4222" 10 | 11 | 12 | [redis] 13 | addrs = ["redis:6379"] 14 | password = "" 15 | db = 0 -------------------------------------------------------------------------------- /configs/islb.toml: -------------------------------------------------------------------------------- 1 | [global] 2 | # data center id 3 | dc = "dc1" 4 | 5 | [log] 6 | level = "info" 7 | 8 | [nats] 9 | url = "nats://127.0.0.1:4222" 10 | 11 | 12 | [redis] 13 | addrs = [":6379"] 14 | password = "" 15 | db = 0 16 | -------------------------------------------------------------------------------- /examples/sfu-v2/README.md: -------------------------------------------------------------------------------- 1 | # SFU v2 example 2 | 3 | ## Run example 4 | 5 | ```sh 6 | go run examples/sfu-v2/main.go -c configs/sfu.toml -addr :5551 7 | ``` 8 | 9 | ## Open echotest example 10 | 11 | Open index.html in the browser -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | .git 2 | .github 3 | .gitignore 4 | .idea 5 | Dockerfile 6 | islb.Dockerfile 7 | web.Dockerfile 8 | docker-compose.yml 9 | docker 10 | docs 11 | screenshots 12 | */*/node_modules 13 | sdk/*/*/dist/*.js 14 | npm-debug.log 15 | -------------------------------------------------------------------------------- /configs/app-room.toml: -------------------------------------------------------------------------------- 1 | [global] 2 | # data center id 3 | dc = "dc1" 4 | 5 | [log] 6 | level = "info" 7 | # level = "debug" 8 | 9 | [nats] 10 | url = "nats://127.0.0.1:4222" 11 | 12 | [redis] 13 | addrs = [":6379"] 14 | password = "" 15 | db = 0 16 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .*.sw[op] 2 | logs 3 | bin 4 | *.pem 5 | *.pid 6 | *.log 7 | cmd/biz/biz 8 | cmd/sfu/sfu 9 | cmd/islb/islb 10 | cmd/avp/avp 11 | apps/room/room 12 | .idea 13 | .vscode 14 | node_modules/ 15 | cover.out 16 | out 17 | .DS_Store 18 | main 19 | 20 | -------------------------------------------------------------------------------- /configs/docker/app-room.toml: -------------------------------------------------------------------------------- 1 | [global] 2 | # data center id 3 | dc = "dc1" 4 | 5 | [log] 6 | level = "info" 7 | # level = "debug" 8 | 9 | [nats] 10 | url = "nats://nats:4222" 11 | 12 | [redis] 13 | addrs = ["redis:6379"] 14 | password = "" 15 | db = 0 16 | -------------------------------------------------------------------------------- /.github/install-hooks.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | SCRIPT_PATH=$( cd "$(dirname "${BASH_SOURCE[0]}")" ; pwd -P ) 4 | 5 | cp "$SCRIPT_PATH/hooks/pre-commit.sh" "$SCRIPT_PATH/../.git/hooks/pre-commit" 6 | cp "$SCRIPT_PATH/hooks/pre-push.sh" "$SCRIPT_PATH/../.git/hooks/pre-push" 7 | -------------------------------------------------------------------------------- /apps/auth/proto/auth.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | option go_package = "github.com/pion/ion/apps/auth/proto"; 4 | 5 | package auth; 6 | 7 | message Permission { 8 | bool publish = 1; 9 | bool subscribe = 2; 10 | bool addPeers = 3; 11 | bool removePeers = 4; 12 | bool endSession = 5; 13 | bool lockRoom = 6; 14 | } -------------------------------------------------------------------------------- /.github/workflows/build.yaml: -------------------------------------------------------------------------------- 1 | name: build 2 | on: 3 | push: 4 | branches: 5 | - master 6 | pull_request: 7 | branches: 8 | - master 9 | jobs: 10 | build: 11 | name: build 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v2 15 | 16 | - name: build 17 | run: make all 18 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /pkg/auth/claims.go: -------------------------------------------------------------------------------- 1 | package auth 2 | 3 | import "github.com/golang-jwt/jwt/v4" 4 | 5 | // claims custom claims type for jwt 6 | type Claims struct { 7 | UID string `json:"uid"` 8 | SID string `json:"sid"` 9 | Publish bool `json:"publish"` 10 | Subcribe bool `json:"subscribe"` 11 | Services []string `json:"services"` 12 | jwt.StandardClaims 13 | } 14 | -------------------------------------------------------------------------------- /proto/islb/islb.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | import "proto/ion/ion.proto"; 4 | 5 | option go_package = "github.com/pion/ion/proto/islb"; 6 | 7 | package islb; 8 | 9 | service ISLB { 10 | } 11 | 12 | message FindNodeRequest { 13 | string sid = 1; 14 | string nid = 2; 15 | string service = 3; 16 | } 17 | 18 | message FindNodeReply { 19 | repeated ion.Node nodes = 1; 20 | } -------------------------------------------------------------------------------- /proto/ion/ion.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | option go_package = "github.com/pion/ion/proto/ion"; 4 | 5 | package ion; 6 | 7 | message Empty {} 8 | 9 | message RPC { 10 | string protocol = 1; 11 | string addr = 2; 12 | map params = 3; 13 | } 14 | 15 | message Node { 16 | string dc = 1; 17 | string nid = 2; 18 | string service = 3; 19 | RPC rpc = 4; 20 | } 21 | -------------------------------------------------------------------------------- /proto/debug/debug.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | option go_package = "github.com/pion/ion/proto/debug"; 4 | 5 | package debug; 6 | 7 | message Debugging { 8 | string nid = 1; 9 | string service = 2; 10 | string file = 3; 11 | int32 line = 4; 12 | string function = 5; 13 | } 14 | 15 | message IonError { 16 | int32 errorCode = 1; 17 | string description = 2; 18 | optional Debugging debugging = 3; 19 | } 20 | -------------------------------------------------------------------------------- /.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$" 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 | -------------------------------------------------------------------------------- /docker/sfu.Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:1.16-alpine as builder 2 | 3 | WORKDIR /ion 4 | 5 | COPY go.mod go.mod 6 | COPY go.sum go.sum 7 | 8 | COPY pkg/ pkg/ 9 | COPY proto/ proto/ 10 | COPY cmd/ cmd/ 11 | 12 | RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 GO111MODULE=on go build -a -o /ion/sfu ./cmd/sfu 13 | 14 | FROM alpine 15 | 16 | COPY --from=builder /ion/sfu /sfu 17 | 18 | COPY configs/docker/sfu.toml /configs/sfu.toml 19 | 20 | ENTRYPOINT ["/sfu"] 21 | CMD ["-c", "/configs/sfu.toml"] 22 | -------------------------------------------------------------------------------- /proto/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:1.14.13-stretch as builder 2 | 3 | RUN go get github.com/golang/protobuf/protoc-gen-go 4 | RUN export GO111MODULE=on && go get google.golang.org/grpc/cmd/protoc-gen-go-grpc@v1.1.0 5 | 6 | RUN echo $GOPATH 7 | 8 | FROM alpine:3.12.1 9 | 10 | RUN apk --no-cache add protobuf make 11 | 12 | COPY --from=builder /go/bin/protoc-gen-go /usr/bin/ 13 | COPY --from=builder /go/bin/protoc-gen-go-grpc /usr/bin/ 14 | 15 | WORKDIR /workspace 16 | 17 | ENTRYPOINT ["make"] -------------------------------------------------------------------------------- /pkg/util/atomic.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import "sync/atomic" 4 | 5 | // AtomicBool represents a atomic bool 6 | type AtomicBool struct { 7 | val int32 8 | } 9 | 10 | // Set atomic bool 11 | func (b *AtomicBool) Set(value bool) (swapped bool) { 12 | if value { 13 | return atomic.SwapInt32(&(b.val), 1) == 0 14 | } 15 | return atomic.SwapInt32(&(b.val), 0) == 1 16 | } 17 | 18 | // Get atomic bool 19 | func (b *AtomicBool) Get() bool { 20 | return atomic.LoadInt32(&(b.val)) != 0 21 | } 22 | -------------------------------------------------------------------------------- /docker/islb.Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:1.16-alpine as builder 2 | 3 | WORKDIR /ion 4 | 5 | COPY go.mod go.mod 6 | COPY go.sum go.sum 7 | RUN go mod download 8 | 9 | COPY pkg/ pkg/ 10 | COPY proto/ proto/ 11 | COPY cmd/ cmd/ 12 | 13 | RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 GO111MODULE=on go build -a -o /ion/islb ./cmd/islb 14 | 15 | FROM alpine 16 | 17 | COPY --from=builder /ion/islb /islb 18 | 19 | COPY configs/docker/islb.toml /configs/islb.toml 20 | 21 | ENTRYPOINT ["/islb"] 22 | CMD ["-c", "/configs/islb.toml"] 23 | -------------------------------------------------------------------------------- /docker/signal.Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:1.16-alpine as builder 2 | 3 | WORKDIR /ion 4 | 5 | COPY go.mod go.mod 6 | COPY go.sum go.sum 7 | RUN go mod download 8 | 9 | COPY pkg/ pkg/ 10 | COPY proto/ proto/ 11 | COPY cmd/ cmd/ 12 | 13 | RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 GO111MODULE=on go build -a -o /ion/signal ./cmd/signal 14 | 15 | FROM alpine 16 | 17 | COPY --from=builder /ion/signal /signal 18 | 19 | COPY configs/docker/signal.toml /configs/signal.toml 20 | 21 | ENTRYPOINT ["/signal"] 22 | CMD ["-c", "/configs/signal.toml"] 23 | -------------------------------------------------------------------------------- /apps/conference/README.md: -------------------------------------------------------------------------------- 1 | #Conference 2 | 3 | This is a locally all-in-one app which not depend on nats. 4 | 5 | Conference support both sfu logic and basic room logic. 6 | 7 | `Conference = sfu + room` 8 | 9 | It show how to DIY an app than you want. 10 | 11 | And you can add a auth app 12 | 13 | `ConferenceX = sfu + room + auth` 14 | 15 | 16 | 17 | And you can build your-app by DIY a custom room app 18 | 19 | `your-app = sfu + custom-room-app` 20 | 21 | 22 | 23 | You can make custom-room-app to join ion cluster with nats like room node. 24 | 25 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /configs/signal.toml: -------------------------------------------------------------------------------- 1 | [global] 2 | # data center id 3 | dc = "dc1" 4 | 5 | [log] 6 | level = "info" 7 | # level = "debug" 8 | 9 | [nats] 10 | url = "nats://127.0.0.1:4222" 11 | 12 | 13 | [signal.grpc] 14 | #listen ip port 15 | host = "0.0.0.0" 16 | port = "5551" 17 | allow_all_origins = true 18 | # cert= "configs/certs/cert.pem" 19 | # key= "configs/certs/key.pem" 20 | 21 | [signal.jwt] 22 | enabled = false 23 | key_type = "HMAC" 24 | key = "1q2dGu5pzikcrECJgW3ADfXX3EsmoD99SYvSVCpDsJrAqxou5tUNbHPvkEFI4bTS" 25 | 26 | [signal.svc] 27 | services = ["rtc", "room"] 28 | -------------------------------------------------------------------------------- /docker/app-room.Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:1.16-alpine as builder 2 | 3 | WORKDIR /ion 4 | 5 | COPY go.mod go.mod 6 | COPY go.sum go.sum 7 | RUN go mod download 8 | 9 | COPY pkg/ pkg/ 10 | COPY proto/ proto/ 11 | COPY apps/ apps/ 12 | 13 | RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 GO111MODULE=on go build -a -installsuffix cgo -o /ion/app-room ./apps/room 14 | 15 | FROM alpine 16 | 17 | COPY --from=builder /ion/app-room /app-room 18 | 19 | COPY configs/docker/app-room.toml /configs/app-room.toml 20 | 21 | ENTRYPOINT ["/app-room"] 22 | CMD ["-c", "/configs/app-room.toml"] 23 | -------------------------------------------------------------------------------- /scripts/key: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -eux 3 | 4 | APP_DIR=$(cd `dirname $0`/../; pwd) 5 | cd $APP_DIR 6 | PATH=$APP_DIR/configs/certs 7 | 8 | echo "****************************************" 9 | echo "building key.pem and cert.pem to $PATH" 10 | echo "****************************************" 11 | echo -e "\n" 12 | mv $PATH/key.pem $PATH/key.pem.bak.`/bin/date +%Y%m%d%H%M%S` 2>/dev/null 13 | mv $PATH/cert.pem $PATH/cert.pem.bak.`/bin/date +%Y%m%d%H%M%S` 2>/dev/null 14 | /usr/bin/openssl req -newkey rsa:2048 -new -nodes -x509 -days 3650 -keyout $PATH/key.pem -out $PATH/cert.pem 15 | echo 'build ok' 16 | 17 | -------------------------------------------------------------------------------- /.github/workflows/renovate-go-mod-fix.yaml: -------------------------------------------------------------------------------- 1 | name: go-mod-fix 2 | on: 3 | push: 4 | branches: 5 | - renovate/* 6 | 7 | jobs: 8 | go-mod-fix: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - name: checkout 12 | uses: actions/checkout@v2 13 | with: 14 | fetch-depth: 2 15 | - name: fix 16 | uses: at-wat/go-sum-fix-action@v0 17 | with: 18 | git_user: Pion Bot 19 | git_email: 59523206+pionbot@users.noreply.github.com 20 | github_token: ${{ secrets.GITHUB_TOKEN }} 21 | commit_style: squash 22 | push: force 23 | -------------------------------------------------------------------------------- /configs/docker/signal.toml: -------------------------------------------------------------------------------- 1 | [global] 2 | # data center id 3 | dc = "dc1" 4 | 5 | [log] 6 | level = "info" 7 | # level = "debug" 8 | 9 | [nats] 10 | url = "nats://nats:4222" 11 | 12 | [signal.grpc] 13 | #listen ip port 14 | host = "0.0.0.0" 15 | port = "5551" 16 | allow_all_origins = true 17 | # cert= "configs/certs/cert.pem" 18 | # key= "configs/certs/key.pem" 19 | 20 | [signal.jwt] 21 | enabled = false 22 | key_type = "HMAC" # this selects the Signing method https://godoc.org/github.com/dgrijalva/jwt-go#SigningMethod 23 | key = "1q2dGu5pzikcrECJgW3ADfXX3EsmoD99SYvSVCpDsJrAqxou5tUNbHPvkEFI4bTS" 24 | 25 | [signal.svc] 26 | services = ["rtc", "room"] 27 | -------------------------------------------------------------------------------- /examples/webserver/README.md: -------------------------------------------------------------------------------- 1 | # Webserver 2 | 3 | ## Run webserver 4 | 5 | ```sh 6 | go build -o bin/webserver examples/webserver/main.go 7 | bin/webserver -addr :8080 -dir examples/echotest 8 | ``` 9 | 10 | ## Open echotest example 11 | 12 | Open [http://localhost:8080](http://localhost:8080) in the browser 13 | 14 | ## Signing a token 15 | 16 | ```sh 17 | curl http://localhost:8080/generate?uid=tony&sid=room1 18 | # Response: {"token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1aWQiOiJ0b255IiwicmlkIjoicm9vbTEifQ.mopgibW3OYONYwzlo-YvkDIkNoYJc3OBQRsqQHZMnD8"} 19 | ``` 20 | 21 | ## Validating a token 22 | 23 | ```sh 24 | curl http://localhost:8080/validate?token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1aWQiOiJ0b255IiwicmlkIjoicm9vbTEifQ.mopgibW3OYONYwzlo-YvkDIkNoYJc3OBQRsqQHZMnD8 25 | # Response: {"uid":"tony","sid":"room1"} 26 | ``` 27 | -------------------------------------------------------------------------------- /pkg/error/code.go: -------------------------------------------------------------------------------- 1 | package error 2 | 3 | type Code int32 4 | 5 | const ( 6 | /* 7 | Canceled = grpc.Canceled 8 | Unauthenticated = grpc.Unauthenticated 9 | Unavailable = grpc.Unavailable 10 | Unimplemented = grpc.Unimplemented 11 | Internal = grpc.Internal 12 | InvalidArgument = grpc.InvalidArgument 13 | PermissionDenied = grpc.PermissionDenied 14 | */ 15 | 16 | Ok Code = 200 17 | BadRequest Code = 400 18 | Forbidden Code = 403 19 | NotFound Code = 404 20 | RequestTimeout Code = 408 21 | UnsupportedMediaType Code = 415 22 | BusyHere Code = 486 23 | TemporarilyUnavailable Code = 480 24 | InternalError Code = 500 25 | NotImplemented Code = 501 26 | ServiceUnavailable Code = 503 27 | ) 28 | -------------------------------------------------------------------------------- /pkg/node/islb/islb_test.go: -------------------------------------------------------------------------------- 1 | package islb 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/nats-io/nats.go" 7 | log "github.com/pion/ion-log" 8 | "github.com/pion/ion/pkg/db" 9 | ) 10 | 11 | var ( 12 | conf = Config{ 13 | Nats: natsConf{ 14 | URL: "nats://127.0.0.1:4222", 15 | }, 16 | Redis: db.Config{ 17 | DB: 0, 18 | Pwd: "", 19 | Addrs: []string{":6379"}, 20 | }, 21 | } 22 | ) 23 | 24 | func init() { 25 | log.Init(conf.Log.Level) 26 | 27 | } 28 | 29 | func TestStart(t *testing.T) { 30 | i := NewISLB() 31 | 32 | err := i.Start(conf) 33 | if err != nil { 34 | t.Error(err) 35 | } 36 | 37 | opts := []nats.Option{nats.Name("nats-grpc echo client")} 38 | // Connect to the NATS server. 39 | nc, err := nats.Connect(conf.Nats.URL, opts...) 40 | if err != nil { 41 | t.Error(err) 42 | } 43 | defer nc.Close() 44 | 45 | i.Close() 46 | } 47 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/pion/ion 2 | 3 | go 1.15 4 | 5 | require ( 6 | github.com/bep/debounce v1.2.0 7 | github.com/cloudwebrtc/nats-discovery v0.3.0 8 | github.com/cloudwebrtc/nats-grpc v1.0.0 9 | github.com/go-redis/redis/v7 v7.4.0 10 | github.com/golang-jwt/jwt/v4 v4.1.0 11 | github.com/golang/protobuf v1.5.2 12 | github.com/improbable-eng/grpc-web v0.14.1 13 | github.com/jhump/protoreflect v1.8.2 14 | github.com/nats-io/nats.go v1.12.0 15 | github.com/onsi/gomega v1.15.0 // indirect 16 | github.com/pion/ion-log v1.2.2 17 | github.com/pion/ion-sfu v1.10.10 18 | github.com/pion/webrtc/v3 v3.1.7 19 | github.com/soheilhy/cmux v0.1.5 20 | github.com/spf13/viper v1.9.0 21 | github.com/stretchr/testify v1.7.0 22 | github.com/tj/assert v0.0.3 23 | golang.org/x/net v0.0.0-20211020060615-d418f374d309 24 | golang.org/x/sync v0.0.0-20210220032951-036812b2e83c 25 | google.golang.org/grpc v1.41.0 26 | google.golang.org/protobuf v1.27.1 27 | ) 28 | -------------------------------------------------------------------------------- /pkg/auth/wrappers.go: -------------------------------------------------------------------------------- 1 | package auth 2 | 3 | import ( 4 | "golang.org/x/net/context" 5 | "google.golang.org/grpc" 6 | ) 7 | 8 | // WrappedServerStream is a thin wrapper around grpc.ServerStream that allows modifying context. 9 | type WrappedServerStream struct { 10 | grpc.ServerStream 11 | // WrappedContext is the wrapper's own Context. You can assign it. 12 | WrappedContext context.Context 13 | } 14 | 15 | // Context returns the wrapper's WrappedContext, overwriting the nested grpc.ServerStream.Context() 16 | func (w *WrappedServerStream) Context() context.Context { 17 | return w.WrappedContext 18 | } 19 | 20 | // WrapServerStream returns a ServerStream that has the ability to overwrite context. 21 | func WrapServerStream(stream grpc.ServerStream) *WrappedServerStream { 22 | if existing, ok := stream.(*WrappedServerStream); ok { 23 | return existing 24 | } 25 | return &WrappedServerStream{ServerStream: stream, WrappedContext: stream.Context()} 26 | } 27 | -------------------------------------------------------------------------------- /scripts/deps_inst: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | APP_DIR=$(cd `dirname $0`/../;pwd) 3 | OS_TYPE="" 4 | . $APP_DIR/scripts/common 5 | 6 | 7 | # Ubuntu 8 | if [[ "$OS_TYPE" =~ "Ubuntu" ]];then 9 | sudo apt-get install -y redis-server 10 | #nodejs-legacy npm 11 | wgetdl https://github.com/nats-io/nats-server/releases/download/v2.1.4/nats-server-v2.1.4-linux-amd64.zip 12 | uz nats-server-v2.1.4-linux-amd64.zip 13 | sudo cp nats-server-v2.1.4-linux-amd64/nats-server /usr/bin 14 | sudo npm install -g n 15 | sudo n stable 16 | fi 17 | 18 | # Centos7 19 | if [[ "$OS_TYPE" =~ "CentOS" ]];then 20 | #npm config set registry http://registry.cnpmjs.org/ 21 | sudo yum install epel-release 22 | sudo yum install -y redis 23 | #nodejs 24 | wgetdl https://github.com/nats-io/nats-server/releases/download/v2.1.4/nats-server-v2.1.4-amd64.rpm 25 | sudo rpm -ivh nats-server-v2.1.4-amd64.rpm 26 | fi 27 | 28 | # Mac 29 | if [[ "$OS_TYPE" =~ "Darwin" ]];then 30 | brew install redis nats-server 31 | fi 32 | 33 | -------------------------------------------------------------------------------- /scripts/README.md: -------------------------------------------------------------------------------- 1 | 2 | ## scripts 3 | 4 | * Support os: Ubuntu MacOS CentOS 5 | 6 | ## 1. Install deps 7 | 8 | ``` 9 | ./scripts/deps_inst 10 | ``` 11 | 12 | It will install all depend modules, support mac, ubuntu, centos 13 | Check these modules installed:nats-server redis 14 | 15 | ## 2. Make key 16 | 17 | ``` 18 | ./scripts/key 19 | ``` 20 | 21 | It will generate key files to configs 22 | 23 | ## 3. Run all services 24 | 25 | First Time 26 | 27 | ``` 28 | ./scripts/all start 29 | ``` 30 | 31 | It will start all services we need, checking 32 | 33 | ``` 34 | ./scripts/all status 35 | ``` 36 | 37 | Next Time, just restart: 38 | 39 | ``` 40 | ./scripts/all restart 41 | ``` 42 | 43 | ## 4. How to run a ion module 44 | 45 | Usage: ./service {start|stop} {app-room|signal|islb|sfu} 46 | 47 | example: 48 | ``` 49 | ./scripts/service start app-room 50 | ``` 51 | 52 | ## 5. How to run a deps module 53 | 54 | Usage: ./deps {start|stop} {nats-server|redis} 55 | 56 | example: 57 | ``` 58 | ./scripts/deps start redis 59 | ``` 60 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /.github/workflows/docker-sfu.yaml: -------------------------------------------------------------------------------- 1 | name: sfu 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 --no-cache --tag pionwebrtc/ion:latest-sfu -f docker/sfu.Dockerfile . 21 | 22 | - name: login 23 | run: echo "${{ secrets.DOCKER_PASSWORD }}" | docker login -u "${{ secrets.DOCKER_USERNAME }}" --password-stdin 24 | 25 | - name: tag 26 | if: github.event_name == 'release' 27 | run: docker tag pionwebrtc/ion:latest-sfu pionwebrtc/ion:"$TAG"-sfu 28 | env: 29 | TAG: ${{ github.event.release.tag_name }} 30 | 31 | - name: push 32 | if: github.event_name == 'release' 33 | run: docker push pionwebrtc/ion:"$TAG"-sfu 34 | env: 35 | TAG: ${{ github.event.release.tag_name }} 36 | 37 | - name: push-master 38 | run: docker push pionwebrtc/ion:latest-sfu 39 | -------------------------------------------------------------------------------- /.github/workflows/docker-islb.yaml: -------------------------------------------------------------------------------- 1 | name: islb 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 --no-cache --tag pionwebrtc/ion:latest-islb -f docker/islb.Dockerfile . 21 | 22 | - name: login 23 | run: echo "${{ secrets.DOCKER_PASSWORD }}" | docker login -u "${{ secrets.DOCKER_USERNAME }}" --password-stdin 24 | 25 | - name: tag 26 | if: github.event_name == 'release' 27 | run: docker tag pionwebrtc/ion:latest-islb pionwebrtc/ion:"$TAG"-islb 28 | env: 29 | TAG: ${{ github.event.release.tag_name }} 30 | 31 | - name: push 32 | if: github.event_name == 'release' 33 | run: docker push pionwebrtc/ion:"$TAG"-islb 34 | env: 35 | TAG: ${{ github.event.release.tag_name }} 36 | 37 | - name: push-master 38 | run: docker push pionwebrtc/ion:latest-islb 39 | -------------------------------------------------------------------------------- /.github/workflows/docker-signal.yaml: -------------------------------------------------------------------------------- 1 | name: signal 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 --no-cache --tag pionwebrtc/ion:latest-signal -f docker/signal.Dockerfile . 21 | 22 | - name: login 23 | run: echo "${{ secrets.DOCKER_PASSWORD }}" | docker login -u "${{ secrets.DOCKER_USERNAME }}" --password-stdin 24 | 25 | - name: tag 26 | if: github.event_name == 'release' 27 | run: docker tag pionwebrtc/ion:latest-signal pionwebrtc/ion:"$TAG"-signal 28 | env: 29 | TAG: ${{ github.event.release.tag_name }} 30 | 31 | - name: push 32 | if: github.event_name == 'release' 33 | run: docker push pionwebrtc/ion:"$TAG"-signal 34 | env: 35 | TAG: ${{ github.event.release.tag_name }} 36 | 37 | - name: push-master 38 | run: docker push pionwebrtc/ion:latest-signal 39 | -------------------------------------------------------------------------------- /.github/workflows/docker-app-room.yaml: -------------------------------------------------------------------------------- 1 | name: app-room 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 --no-cache --tag pionwebrtc/ion:latest-app-room -f docker/app-room.Dockerfile . 21 | 22 | - name: login 23 | run: echo "${{ secrets.DOCKER_PASSWORD }}" | docker login -u "${{ secrets.DOCKER_USERNAME }}" --password-stdin 24 | 25 | - name: tag 26 | if: github.event_name == 'release' 27 | run: docker tag pionwebrtc/ion:latest-app-room pionwebrtc/ion:"$TAG"-app-room 28 | env: 29 | TAG: ${{ github.event.release.tag_name }} 30 | 31 | - name: push 32 | if: github.event_name == 'release' 33 | run: docker push pionwebrtc/ion:"$TAG"-app-room 34 | env: 35 | TAG: ${{ github.event.release.tag_name }} 36 | 37 | - name: push-master 38 | run: docker push pionwebrtc/ion:latest-app-room 39 | -------------------------------------------------------------------------------- /apps/room/main.go: -------------------------------------------------------------------------------- 1 | // Package cmd contains an entrypoint for running an ion-sfu instance. 2 | package main 3 | 4 | import ( 5 | "flag" 6 | "os" 7 | "os/signal" 8 | "syscall" 9 | 10 | log "github.com/pion/ion-log" 11 | room "github.com/pion/ion/apps/room/server" 12 | ) 13 | 14 | // run as distributed node 15 | func main() { 16 | var confFile, addr, cert, key, logLevel string 17 | flag.StringVar(&confFile, "c", "", "config file") 18 | flag.StringVar(&addr, "addr", ":5551", "grpc listening addr") 19 | flag.StringVar(&cert, "cert", "", "cert for tls") 20 | flag.StringVar(&key, "key", "", "key for tls") 21 | flag.StringVar(&logLevel, "l", "info", "log level") 22 | flag.Parse() 23 | 24 | if confFile == "" { 25 | flag.PrintDefaults() 26 | return 27 | } 28 | 29 | log.Init(logLevel) 30 | log.Infof("--- Starting Room Service ---") 31 | 32 | node := room.New() 33 | err := node.Load(confFile) 34 | if err != nil { 35 | log.Errorf("node load error: %v", err) 36 | os.Exit(-1) 37 | } 38 | 39 | err = node.Start() 40 | if err != nil { 41 | log.Errorf("node init start: %v", err) 42 | os.Exit(-1) 43 | } 44 | 45 | defer node.Close() 46 | 47 | // Press Ctrl+C to exit the process 48 | ch := make(chan os.Signal, 1) 49 | signal.Notify(ch, os.Interrupt, syscall.SIGTERM) 50 | <-ch 51 | } 52 | -------------------------------------------------------------------------------- /examples/sfu-v2/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | 6 | log "github.com/pion/ion-log" 7 | "github.com/pion/ion/pkg/node/sfu" 8 | "github.com/pion/ion/pkg/util" 9 | "google.golang.org/grpc" 10 | ) 11 | 12 | func main() { 13 | var confFile, addr, cert, key, logLevel string 14 | flag.StringVar(&confFile, "c", "", "sfu config file") 15 | flag.StringVar(&addr, "addr", ":5551", "grpc listening addr") 16 | flag.StringVar(&cert, "cert", "", "cert for tls") 17 | flag.StringVar(&key, "key", "", "key for tls") 18 | flag.StringVar(&logLevel, "l", "info", "log level") 19 | flag.Parse() 20 | 21 | if confFile == "" { 22 | flag.PrintDefaults() 23 | return 24 | } 25 | 26 | conf := sfu.Config{} 27 | err := conf.Load(confFile) 28 | if err != nil { 29 | log.Errorf("config load error: %v", err) 30 | return 31 | } 32 | 33 | log.Init(conf.Log.Level) 34 | log.Infof("--- Starting ION SFU ---") 35 | 36 | grpcServer := grpc.NewServer() 37 | 38 | sfu := sfu.NewSFUService(conf.Config) 39 | 40 | sfu.RegisterService(grpcServer) 41 | options := util.NewWrapperedServerOptions(addr, cert, key, true) 42 | wrapperedSrv := util.NewWrapperedGRPCWebServer(options, grpcServer) 43 | if err := wrapperedSrv.Serve(); err != nil { 44 | log.Panicf("failed to serve: %v", err) 45 | } 46 | select {} 47 | } 48 | -------------------------------------------------------------------------------- /pkg/error/error.go: -------------------------------------------------------------------------------- 1 | package error 2 | 3 | import ( 4 | "google.golang.org/grpc/codes" 5 | "google.golang.org/grpc/status" 6 | 7 | log "github.com/pion/ion-log" 8 | "github.com/pion/ion/proto/debug" 9 | ) 10 | 11 | // err.NewGrpcIonError(codes.InvalidArgument, "GRPC error with custom error", -1, "custom error") 12 | func NewGrpcIonError(code codes.Code, msg string, errorCode int32, desc string, debugging *debug.Debugging) error { 13 | st := status.New(code, msg) 14 | customErr := &debug.IonError{ 15 | ErrorCode: errorCode, 16 | Description: desc, 17 | Debugging: debugging, 18 | } 19 | stDetails, _ := st.WithDetails(customErr) 20 | return stDetails.Err() 21 | } 22 | 23 | func ParseGrpcIonError(err error) (*debug.IonError, bool) { 24 | st, ok := status.FromError(err) 25 | if !ok { 26 | log.Errorf("Error: %v", err) 27 | return nil, false 28 | } 29 | 30 | log.Infof("GRPC Error : Code [%d], Message [%s]", st.Code(), st.Message()) 31 | 32 | if len(st.Details()) > 0 { 33 | for _, detail := range st.Details() { 34 | switch d := detail.(type) { 35 | case *debug.IonError: 36 | log.Infof(" - Details: IonError: %d, %s", d.ErrorCode, d.Description) 37 | return d, true 38 | default: 39 | log.Infof(" - Details: Unknown: %v", d) 40 | } 41 | } 42 | } 43 | 44 | return nil, false 45 | } 46 | -------------------------------------------------------------------------------- /pkg/util/nats.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/nats-io/nats.go" 7 | log "github.com/pion/ion-log" 8 | ) 9 | 10 | // NewNatsConn . 11 | func NewNatsConn(url string) (*nats.Conn, error) { 12 | // connect options 13 | opts := []nats.Option{nats.Name("nats conn")} 14 | opts = setupConnOptions(opts) 15 | 16 | // connect to nats server 17 | nc, err := nats.Connect(url, opts...) 18 | if err != nil { 19 | return nil, err 20 | } 21 | return nc, nil 22 | } 23 | 24 | func setupConnOptions(opts []nats.Option) []nats.Option { 25 | totalWait := 10 * time.Minute 26 | reconnectDelay := time.Second 27 | 28 | opts = append(opts, nats.ReconnectWait(reconnectDelay)) 29 | opts = append(opts, nats.MaxReconnects(int(totalWait/reconnectDelay))) 30 | opts = append(opts, nats.DisconnectErrHandler(func(nc *nats.Conn, err error) { 31 | if !nc.IsClosed() { 32 | log.Infof("Disconnected due to: %s, will attempt reconnects for %.0fm", err, totalWait.Minutes()) 33 | } 34 | })) 35 | opts = append(opts, nats.ReconnectHandler(func(nc *nats.Conn) { 36 | log.Infof("Reconnected [%s]", nc.ConnectedUrl()) 37 | })) 38 | opts = append(opts, nats.ClosedHandler(func(nc *nats.Conn) { 39 | if !nc.IsClosed() { 40 | log.Errorf("Exiting: no servers available") 41 | } else { 42 | log.Errorf("Exiting") 43 | } 44 | })) 45 | return opts 46 | } 47 | -------------------------------------------------------------------------------- /pkg/auth/jwt.go: -------------------------------------------------------------------------------- 1 | package auth 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/golang-jwt/jwt/v4" 7 | "google.golang.org/grpc/codes" 8 | "google.golang.org/grpc/metadata" 9 | "google.golang.org/grpc/status" 10 | ) 11 | 12 | // AuthConfig auth config 13 | type AuthConfig struct { 14 | Enabled bool `mapstructure:"enabled"` 15 | Key string `mapstructure:"key"` 16 | KeyType string `mapstructure:"key_type"` 17 | } 18 | 19 | // KeyFunc auth key types 20 | func (a AuthConfig) KeyFunc(t *jwt.Token) (interface{}, error) { 21 | // nolint: gocritic 22 | switch a.KeyType { 23 | //TODO: add more support for keytypes here 24 | default: 25 | return []byte(a.Key), nil 26 | } 27 | } 28 | 29 | func GetClaim(ctx context.Context, ac *AuthConfig) (*Claims, error) { 30 | md, ok := metadata.FromIncomingContext(ctx) 31 | if !ok { 32 | return nil, status.Errorf(codes.Unauthenticated, "valid JWT token required") 33 | } 34 | 35 | token, ok := md["authorization"] 36 | if !ok { 37 | return nil, status.Errorf(codes.Unauthenticated, "valid JWT token required") 38 | } 39 | 40 | jwtToken, err := jwt.ParseWithClaims(token[0], &Claims{}, ac.KeyFunc) 41 | 42 | if err != nil { 43 | return nil, status.Errorf(codes.Unauthenticated, "%v", err) 44 | } 45 | 46 | if claims, ok := jwtToken.Claims.(*Claims); ok && jwtToken.Valid { 47 | return claims, nil 48 | } 49 | 50 | return nil, status.Errorf(codes.Unauthenticated, "valid JWT token required: %v", err) 51 | } 52 | -------------------------------------------------------------------------------- /pkg/util/service.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "context" 5 | 6 | nrpc "github.com/cloudwebrtc/nats-grpc/pkg/rpc" 7 | "github.com/cloudwebrtc/nats-grpc/pkg/rpc/reflection" 8 | "github.com/jhump/protoreflect/grpcreflect" 9 | log "github.com/pion/ion-log" 10 | rpb "google.golang.org/grpc/reflection/grpc_reflection_v1alpha" 11 | ) 12 | 13 | // Get service information through reflection, this feature can be used to create json-rpc, restful API 14 | func GetServiceInfo(nc nrpc.NatsConn, nid string, selfnid string) (map[string][]*reflection.MethodDescriptor, error) { 15 | ncli := nrpc.NewClient(nc, nid, selfnid) 16 | ctx, cancel := context.WithCancel(context.Background()) 17 | rc := grpcreflect.NewClient(ctx, rpb.NewServerReflectionClient(ncli)) 18 | 19 | defer func() { 20 | cancel() 21 | ncli.Close() 22 | }() 23 | 24 | reflector := reflection.NewReflector(rc) 25 | list, err := reflector.ListServices() 26 | if err != nil { 27 | log.Errorf("ListServices: error %v\n", err) 28 | return nil, err 29 | } 30 | 31 | log.Debugf("ListServices: %v", list) 32 | 33 | info := make(map[string][]*reflection.MethodDescriptor) 34 | for _, svc := range list { 35 | if svc == "grpc.reflection.v1alpha.ServerReflection" { 36 | continue 37 | } 38 | log.Debugf("Service => %v", svc) 39 | mds, err := reflector.DescribeService(svc) 40 | if err != nil { 41 | return nil, err 42 | } 43 | for _, md := range mds { 44 | log.Debugf("Method => %v", md.GetName()) 45 | } 46 | info[svc] = mds 47 | } 48 | 49 | return info, nil 50 | } 51 | -------------------------------------------------------------------------------- /cmd/sfu/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "net/http" 6 | 7 | "os" 8 | "os/signal" 9 | "syscall" 10 | 11 | log "github.com/pion/ion-log" 12 | "github.com/pion/ion/pkg/node/sfu" 13 | ) 14 | 15 | func main() { 16 | var confFile, addr, cert, key, logLevel, paddr string 17 | flag.StringVar(&confFile, "c", "", "sfu config file") 18 | flag.StringVar(&addr, "addr", ":5551", "grpc listening addr") 19 | flag.StringVar(&cert, "cert", "", "cert for tls") 20 | flag.StringVar(&key, "key", "", "key for tls") 21 | flag.StringVar(&logLevel, "l", "info", "log level") 22 | flag.StringVar(&paddr, "paddr", ":6060", "pprof listening addr") 23 | 24 | flag.Parse() 25 | 26 | if confFile == "" { 27 | flag.PrintDefaults() 28 | return 29 | } 30 | 31 | conf := sfu.Config{} 32 | err := conf.Load(confFile) 33 | if err != nil { 34 | log.Errorf("config load error: %v", err) 35 | return 36 | } 37 | 38 | if paddr != "" { 39 | go func() { 40 | log.Infof("start pprof on %s", paddr) 41 | err := http.ListenAndServe(paddr, nil) 42 | if err != nil { 43 | log.Errorf("http.ListenAndServe err=%v", err) 44 | } 45 | }() 46 | } 47 | 48 | log.Init(conf.Log.Level) 49 | log.Infof("--- starting sfu node ---") 50 | 51 | node := sfu.NewSFU() 52 | if err := node.Start(conf); err != nil { 53 | log.Errorf("sfu init start: %v", err) 54 | os.Exit(-1) 55 | } 56 | defer node.Close() 57 | 58 | // Press Ctrl+C to exit the process 59 | ch := make(chan os.Signal, 1) 60 | signal.Notify(ch, os.Interrupt, syscall.SIGTERM) 61 | <-ch 62 | } 63 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## ION 2 | 3 | #### *ION is a distributed real-time communication system, the goal is to chat anydevice, anytime, anywhere!* 4 | 5 | ![MIT](https://img.shields.io/badge/License-MIT-yellow.svg)[![Go Report Card](https://goreportcard.com/badge/github.com/pion/ion)![GitHub go.mod Go version](https://img.shields.io/github/go-mod/go-version/pion/ion)![GitHub tag (latest SemVer pre-release)](https://img.shields.io/github/v/tag/pion/ion?include_prereleases)](https://goreportcard.com/report/github.com/pion/ion)![Docker Pulls](https://img.shields.io/docker/pulls/pionwebrtc/ion-biz?style=plastic)[![Financial Contributors on Open Collective](https://opencollective.com/pion-ion/all/badge.svg?label=financial+contributors)](https://opencollective.com/pion-ion) ![GitHub contributors](https://img.shields.io/github/contributors-anon/pion/ion)![Twitter Follow](https://img.shields.io/twitter/follow/_PION?style=social)[![slack](https://img.shields.io/badge/join-us%20on%20slack-gray.svg?longCache=true&logo=slack&colorB=brightgreen)](https://pion.ly/slack) 6 | 7 |
8 | 9 | ## Online Docs 10 | 11 | [https://pionion.github.io](https://pionion.github.io/) 12 | 13 | ## Sponsor 14 | 15 | ❤️Sponsor to help this awsome project go faster!🚀 16 | 17 | (https://opencollective.com/pion-ion) 18 | 19 | ## Contributors 20 | 21 | 22 | 23 | ## Original Author 24 | [adwpc](https://github.com/adwpc) [cloudwebrtc](https://github.com/cloudwebrtc)* 25 | 26 | ## Community Hero 27 | [Sean-Der](https://github.com/Sean-Der)* 28 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3.7" 2 | 3 | services: 4 | islb: 5 | image: pionwebrtc/ion:latest-islb 6 | build: 7 | dockerfile: ./docker/islb.Dockerfile 8 | context: . 9 | volumes: 10 | - "./configs/docker/islb.toml:/configs/islb.toml" 11 | depends_on: 12 | - nats 13 | - redis 14 | networks: 15 | - ionnet 16 | 17 | sfu: 18 | image: pionwebrtc/ion:latest-sfu 19 | build: 20 | dockerfile: ./docker/sfu.Dockerfile 21 | context: . 22 | volumes: 23 | - "./configs/docker/sfu.toml:/configs/sfu.toml" 24 | ports: 25 | - "5000:5000/udp" 26 | - 3478:3478 27 | depends_on: 28 | - nats 29 | - islb 30 | networks: 31 | - ionnet 32 | 33 | signal: 34 | image: pionwebrtc/ion:latest-signal 35 | build: 36 | dockerfile: ./docker/signal.Dockerfile 37 | context: . 38 | volumes: 39 | - "./configs/docker/signal.toml:/configs/signal.toml" 40 | ports: 41 | - 5551:5551 42 | depends_on: 43 | - islb 44 | - app-room 45 | networks: 46 | - ionnet 47 | 48 | app-room: 49 | image: pionwebrtc/ion:latest-app-room 50 | build: 51 | dockerfile: ./docker/app-room.Dockerfile 52 | context: . 53 | volumes: 54 | - "./configs/docker/app-room.toml:/configs/app-room.toml" 55 | depends_on: 56 | - nats 57 | - redis 58 | - islb 59 | networks: 60 | - ionnet 61 | 62 | nats: 63 | image: nats 64 | ports: 65 | - 4222:4222 66 | networks: 67 | - ionnet 68 | 69 | redis: 70 | image: redis:6.0.9 71 | ports: 72 | - 6379:6379 73 | networks: 74 | - ionnet 75 | 76 | networks: 77 | ionnet: 78 | name: ionnet 79 | driver: bridge 80 | -------------------------------------------------------------------------------- /cmd/islb/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | 7 | "os" 8 | "os/signal" 9 | "syscall" 10 | 11 | log "github.com/pion/ion-log" 12 | "github.com/pion/ion/pkg/node/islb" 13 | "github.com/spf13/viper" 14 | ) 15 | 16 | var ( 17 | conf = islb.Config{} 18 | file string 19 | ) 20 | 21 | func showHelp() { 22 | fmt.Printf("Usage:%s {params}\n", os.Args[0]) 23 | fmt.Println(" -c {config file}") 24 | fmt.Println(" -h (show help info)") 25 | } 26 | 27 | func load() bool { 28 | _, err := os.Stat(file) 29 | if err != nil { 30 | return false 31 | } 32 | 33 | viper.SetConfigFile(file) 34 | viper.SetConfigType("toml") 35 | 36 | err = viper.ReadInConfig() 37 | if err != nil { 38 | fmt.Printf("config file %s read failed. %v\n", file, err) 39 | return false 40 | } 41 | err = viper.UnmarshalExact(&conf) 42 | if err != nil { 43 | fmt.Printf("config file %s loaded failed. %v\n", file, err) 44 | return false 45 | } 46 | fmt.Printf("config %s load ok!\n", file) 47 | return true 48 | } 49 | 50 | func parse() bool { 51 | flag.StringVar(&file, "c", "configs/islb.toml", "config file") 52 | 53 | help := flag.Bool("h", false, "help info") 54 | flag.Parse() 55 | if !load() { 56 | return false 57 | } 58 | 59 | if *help { 60 | showHelp() 61 | return false 62 | } 63 | return true 64 | } 65 | 66 | func main() { 67 | if !parse() { 68 | showHelp() 69 | os.Exit(-1) 70 | } 71 | 72 | log.Init(conf.Log.Level) 73 | 74 | log.Infof("--- starting islb node ---") 75 | node := islb.NewISLB() 76 | if err := node.Start(conf); err != nil { 77 | log.Errorf("islb start error: %v", err) 78 | os.Exit(-1) 79 | } 80 | defer node.Close() 81 | 82 | // Press Ctrl+C to exit the process 83 | ch := make(chan os.Signal, 1) 84 | signal.Notify(ch, os.Interrupt, syscall.SIGTERM) 85 | <-ch 86 | } 87 | -------------------------------------------------------------------------------- /apps/conference/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "net/http" 6 | "os" 7 | "os/signal" 8 | "syscall" 9 | 10 | log "github.com/pion/ion-log" 11 | room "github.com/pion/ion/apps/room/server" 12 | "github.com/pion/ion/pkg/node/sfu" 13 | "github.com/pion/ion/pkg/util" 14 | 15 | "github.com/pion/ion/pkg/runner" 16 | ) 17 | 18 | func main() { 19 | var roomConfFile, sfuConfFile, addr, logLevel, certFile, keyFile, paddr string 20 | flag.StringVar(&roomConfFile, "bc", "", "room config file") 21 | flag.StringVar(&sfuConfFile, "sc", "", "sfu config file") 22 | flag.StringVar(&addr, "addr", ":5551", "grpc listening addr") 23 | flag.StringVar(&certFile, "cert", "", "cert file") 24 | flag.StringVar(&keyFile, "key", "", "key file") 25 | flag.StringVar(&logLevel, "l", "info", "log level") 26 | flag.StringVar(&paddr, "paddr", ":6060", "pprof listening addr") 27 | flag.Parse() 28 | if roomConfFile == "" && sfuConfFile == "" { 29 | flag.PrintDefaults() 30 | return 31 | } 32 | 33 | log.Init(logLevel) 34 | log.Infof("--- Starting Conference ---") 35 | if paddr != "" { 36 | go func() { 37 | log.Infof("start pprof on %s", paddr) 38 | err := http.ListenAndServe(paddr, nil) 39 | if err != nil { 40 | log.Errorf("http.ListenAndServe err=%v", err) 41 | } 42 | }() 43 | } 44 | 45 | r := runner.New(util.NewWrapperedServerOptions(addr, certFile, keyFile, true)) 46 | err := r.AddService( 47 | runner.ServiceUnit{ 48 | Service: room.New(), 49 | ConfigFile: roomConfFile, 50 | }, 51 | runner.ServiceUnit{ 52 | Service: sfu.New(), 53 | ConfigFile: sfuConfFile, 54 | }, 55 | ) 56 | 57 | if err != nil { 58 | log.Errorf("runner AddService error: %v", err) 59 | return 60 | } 61 | 62 | defer r.Close() 63 | 64 | // Press Ctrl+C to exit the process 65 | ch := make(chan os.Signal, 1) 66 | signal.Notify(ch, os.Interrupt, syscall.SIGTERM) 67 | <-ch 68 | } 69 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | GO_LDFLAGS = -ldflags "-s -w" 2 | GO_VERSION = 1.14 3 | GO_TESTPKGS:=$(shell go list ./... | grep -v cmd | grep -v conf | grep -v node) 4 | GO_COVERPKGS:=$(shell echo $(GO_TESTPKGS) | paste -s -d ',') 5 | TEST_UID:=$(shell id -u) 6 | TEST_GID:=$(shell id -g) 7 | 8 | all: go_deps core app 9 | 10 | go_deps: 11 | go mod download 12 | 13 | core: 14 | go build -o bin/islb $(GO_LDFLAGS) cmd/islb/main.go 15 | go build -o bin/sfu $(GO_LDFLAGS) cmd/sfu/main.go 16 | # go build -o bin/avp $(GO_LDFLAGS) cmd/avp/main.go 17 | go build -o bin/signal $(GO_LDFLAGS) cmd/signal/main.go 18 | 19 | app: 20 | go build -o bin/app-room $(GO_LDFLAGS) apps/room/main.go 21 | 22 | clean: 23 | rm -rf bin 24 | 25 | scripts-start-services: 26 | ./scripts/all start 27 | 28 | scripts-stop-services: 29 | ./scripts/all stop 30 | 31 | docker-start-services: 32 | docker-compose pull 33 | docker-compose -f docker-compose.yml up 34 | 35 | docker-stop-services: 36 | docker-compose -f docker-compose.yml down 37 | 38 | test: go_deps start-services 39 | go test \ 40 | -timeout 120s \ 41 | -coverpkg=${GO_COVERPKGS} -coverprofile=cover.out -covermode=atomic \ 42 | -v -race ${GO_TESTPKGS} 43 | 44 | proto-gen-from-docker: 45 | docker build -t go-protoc ./proto 46 | docker run -v $(CURDIR):/workspace go-protoc proto 47 | 48 | proto: proto_core proto_app 49 | 50 | proto_core: 51 | protoc proto/debug/debug.proto --experimental_allow_proto3_optional --go_opt=module=github.com/pion/ion --go_out=. --go-grpc_opt=module=github.com/pion/ion --go-grpc_out=. 52 | protoc proto/ion/ion.proto --go_opt=module=github.com/pion/ion --go_out=. --go-grpc_opt=module=github.com/pion/ion --go-grpc_out=. 53 | protoc proto/islb/islb.proto --go_opt=module=github.com/pion/ion --go_out=. --go-grpc_opt=module=github.com/pion/ion --go-grpc_out=. 54 | protoc proto/rtc/rtc.proto --go_opt=module=github.com/pion/ion --go_out=. --go-grpc_opt=module=github.com/pion/ion --go-grpc_out=. 55 | 56 | proto_app: 57 | protoc apps/room/proto/room.proto --go_opt=module=github.com/pion/ion --go_out=. --go-grpc_opt=module=github.com/pion/ion --go-grpc_out=. 58 | -------------------------------------------------------------------------------- /pkg/ion/node_test.go: -------------------------------------------------------------------------------- 1 | package ion 2 | 3 | import ( 4 | "sync" 5 | "testing" 6 | 7 | "github.com/cloudwebrtc/nats-discovery/pkg/discovery" 8 | "github.com/cloudwebrtc/nats-discovery/pkg/registry" 9 | 10 | "github.com/nats-io/nats.go" 11 | log "github.com/pion/ion-log" 12 | "github.com/pion/ion/pkg/proto" 13 | "github.com/pion/ion/pkg/util" 14 | "github.com/stretchr/testify/assert" 15 | ) 16 | 17 | var ( 18 | natsURL = "nats://127.0.0.1:4222" 19 | nc *nats.Conn 20 | reg *registry.Registry 21 | wg *sync.WaitGroup 22 | nid = "testnid001" 23 | ) 24 | 25 | func init() { 26 | log.Init("debug") 27 | wg = new(sync.WaitGroup) 28 | nc, _ = util.NewNatsConn(natsURL) 29 | var err error 30 | reg, err = registry.NewRegistry(nc, discovery.DefaultExpire) 31 | if err != nil { 32 | log.Errorf("%v", err) 33 | } 34 | } 35 | 36 | func TestWatch(t *testing.T) { 37 | n := NewNode(nid) 38 | 39 | err := reg.Listen(func(action discovery.Action, node discovery.Node) (bool, error) { 40 | log.Debugf("handleNode: service %v, action %v => id %v, RPC %v", node.Service, action, node.ID(), node.RPC) 41 | assert.Equal(t, node.NID, nid) 42 | assert.Equal(t, node.Service, proto.ServiceROOM) 43 | wg.Done() 44 | return true, nil 45 | }, func(service string, params map[string]interface{}) ([]discovery.Node, error) { 46 | return []discovery.Node{}, nil 47 | }) 48 | if err != nil { 49 | t.Error(err) 50 | } 51 | 52 | wg.Add(1) 53 | err = n.Start(natsURL) 54 | if err != nil { 55 | t.Error(err) 56 | } 57 | 58 | node := discovery.Node{ 59 | DC: "dc", 60 | Service: proto.ServiceROOM, 61 | NID: nid, 62 | RPC: discovery.RPC{ 63 | Protocol: discovery.NGRPC, 64 | Addr: natsURL, 65 | //Params: map[string]string{"username": "foo", "password": "bar"}, 66 | }, 67 | } 68 | 69 | go func() { 70 | err := n.KeepAlive(node) 71 | if err != nil { 72 | t.Error(err) 73 | } 74 | }() 75 | 76 | wg.Wait() 77 | assert.NotEmpty(t, n.ServiceRegistrar()) 78 | 79 | err = n.Watch(proto.ServiceROOM) 80 | if err != nil { 81 | t.Error(err) 82 | } 83 | 84 | n.Close() 85 | } 86 | -------------------------------------------------------------------------------- /proto/islb/islb_grpc.pb.go: -------------------------------------------------------------------------------- 1 | // Code generated by protoc-gen-go-grpc. DO NOT EDIT. 2 | 3 | package islb 4 | 5 | import ( 6 | grpc "google.golang.org/grpc" 7 | ) 8 | 9 | // This is a compile-time assertion to ensure that this generated file 10 | // is compatible with the grpc package it is being compiled against. 11 | // Requires gRPC-Go v1.32.0 or later. 12 | const _ = grpc.SupportPackageIsVersion7 13 | 14 | // ISLBClient is the client API for ISLB service. 15 | // 16 | // For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream. 17 | type ISLBClient interface { 18 | } 19 | 20 | type iSLBClient struct { 21 | cc grpc.ClientConnInterface 22 | } 23 | 24 | func NewISLBClient(cc grpc.ClientConnInterface) ISLBClient { 25 | return &iSLBClient{cc} 26 | } 27 | 28 | // ISLBServer is the server API for ISLB service. 29 | // All implementations must embed UnimplementedISLBServer 30 | // for forward compatibility 31 | type ISLBServer interface { 32 | mustEmbedUnimplementedISLBServer() 33 | } 34 | 35 | // UnimplementedISLBServer must be embedded to have forward compatible implementations. 36 | type UnimplementedISLBServer struct { 37 | } 38 | 39 | func (UnimplementedISLBServer) mustEmbedUnimplementedISLBServer() {} 40 | 41 | // UnsafeISLBServer may be embedded to opt out of forward compatibility for this service. 42 | // Use of this interface is not recommended, as added methods to ISLBServer will 43 | // result in compilation errors. 44 | type UnsafeISLBServer interface { 45 | mustEmbedUnimplementedISLBServer() 46 | } 47 | 48 | func RegisterISLBServer(s grpc.ServiceRegistrar, srv ISLBServer) { 49 | s.RegisterService(&ISLB_ServiceDesc, srv) 50 | } 51 | 52 | // ISLB_ServiceDesc is the grpc.ServiceDesc for ISLB service. 53 | // It's only intended for direct use with grpc.RegisterService, 54 | // and not to be introspected or modified (even as a copy) 55 | var ISLB_ServiceDesc = grpc.ServiceDesc{ 56 | ServiceName: "islb.ISLB", 57 | HandlerType: (*ISLBServer)(nil), 58 | Methods: []grpc.MethodDesc{}, 59 | Streams: []grpc.StreamDesc{}, 60 | Metadata: "proto/islb/islb.proto", 61 | } 62 | -------------------------------------------------------------------------------- /pkg/runner/runner.go: -------------------------------------------------------------------------------- 1 | package runner 2 | 3 | import ( 4 | log "github.com/pion/ion-log" 5 | "github.com/pion/ion/pkg/util" 6 | "google.golang.org/grpc" 7 | ) 8 | 9 | // ConfigBase is a interface used by Service 10 | type ConfigBase interface { 11 | Load(string) error 12 | } 13 | 14 | // Service is a interface 15 | type Service interface { 16 | New() Service 17 | ConfigBase() ConfigBase 18 | StartGRPC(registrar grpc.ServiceRegistrar) error 19 | Close() 20 | } 21 | 22 | // New create a ServiceRunner 23 | func New(options util.WrapperedServerOptions) *ServiceRunner { 24 | return &ServiceRunner{ 25 | options: options, 26 | grpcServer: grpc.NewServer(), 27 | } 28 | } 29 | 30 | // ServiceUnit contain a service and it's config file path 31 | type ServiceUnit struct { 32 | Service Service 33 | ConfigFile string 34 | } 35 | 36 | // ServiceRunner allow you run grpc service all-in-one 37 | type ServiceRunner struct { 38 | options util.WrapperedServerOptions 39 | grpcServer *grpc.Server 40 | serviceUnits []ServiceUnit 41 | } 42 | 43 | // AddService start a grpc server on addr, and registe all serviceUints 44 | func (s *ServiceRunner) AddService(serviceUnits ...ServiceUnit) error { 45 | log.Infof("ServiceRunner.AddService serviceUnits=%+v", serviceUnits) 46 | 47 | // Init all services 48 | for _, serviceUnit := range serviceUnits { 49 | // Init basic config 50 | conf := serviceUnit.Service.ConfigBase() 51 | err := conf.Load(serviceUnit.ConfigFile) 52 | if err != nil { 53 | log.Errorf("config load error: %v", err) 54 | return err 55 | } 56 | 57 | // Registe service to grpc server 58 | err = serviceUnit.Service.StartGRPC(s.grpcServer) 59 | if err != nil { 60 | log.Errorf("Init service error: %v", err) 61 | return err 62 | } 63 | s.serviceUnits = append(s.serviceUnits, serviceUnit) 64 | } 65 | 66 | wrapperedSrv := util.NewWrapperedGRPCWebServer(s.options, s.grpcServer) 67 | if err := wrapperedSrv.Serve(); err != nil { 68 | log.Panicf("failed to serve: %v", err) 69 | return err 70 | } 71 | return nil 72 | } 73 | 74 | // Close close all services 75 | func (s *ServiceRunner) Close() { 76 | for _, u := range s.serviceUnits { 77 | u.Service.Close() 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /apps/room/server/peer.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "errors" 5 | 6 | room "github.com/pion/ion/apps/room/proto" 7 | "github.com/pion/ion/pkg/util" 8 | ) 9 | 10 | // Peer represents a peer for client 11 | type Peer struct { 12 | info *room.Peer 13 | sig room.RoomSignal_SignalServer 14 | room *Room 15 | closed util.AtomicBool 16 | } 17 | 18 | // NewPeer create a peer 19 | // args: sid, uid, dest, name, role, protocol, direction, info, avatar, vendor 20 | // at least sid and uid 21 | func NewPeer() *Peer { 22 | p := &Peer{} 23 | return p 24 | } 25 | 26 | // NewPeer create a peer 27 | // args: sid, uid, dest, name, role, protocol, direction, info, avatar, vendor 28 | // at least sid and uid 29 | // func NewPeer(args ...string) *Peer { 30 | // if len(args) < 2 { 31 | // return nil 32 | // } 33 | 34 | // sid, uid, dest, name, role, protocol, direction, info, avatar, vendor := util.GetArgs(args...) 35 | 36 | // log.Infof("args= %v %v %v %v %v %v %v %v ", sid, uid, dest, name, role, protocol, direction, info) 37 | // p := &Peer{ 38 | // sid: sid, 39 | // uid: uid, 40 | // dest: dest, 41 | // displayName: name, 42 | // role: role, 43 | // protocol: protocol, 44 | // direction: direction, 45 | // info: []byte(info), 46 | // avatar: avatar, 47 | // vendor: vendor, 48 | // } 49 | // return p 50 | // } 51 | 52 | // Close peer 53 | func (p *Peer) Close() { 54 | if !p.closed.Set(true) { 55 | return 56 | } 57 | p.room.delPeer(p) 58 | } 59 | 60 | // UID return peer uid 61 | func (p *Peer) UID() string { 62 | return p.info.Uid 63 | } 64 | 65 | // SID return session id 66 | func (p *Peer) SID() string { 67 | return p.info.Sid 68 | } 69 | 70 | func (p *Peer) send(data *room.Reply) error { 71 | if p.sig == nil { 72 | return errors.New("p.sig == nil maybe not join") 73 | } 74 | return p.sig.Send(data) 75 | } 76 | 77 | func (p *Peer) sendPeerEvent(event *room.PeerEvent) error { 78 | data := &room.Reply{ 79 | Payload: &room.Reply_Peer{ 80 | Peer: event, 81 | }, 82 | } 83 | return p.send(data) 84 | } 85 | 86 | func (p *Peer) sendMessage(msg *room.Message) error { 87 | data := &room.Reply{ 88 | Payload: &room.Reply_Message{ 89 | Message: msg, 90 | }, 91 | } 92 | return p.send(data) 93 | } 94 | -------------------------------------------------------------------------------- /pkg/util/util.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "math/rand" 5 | "runtime" 6 | "runtime/debug" 7 | "strings" 8 | "time" 9 | 10 | log "github.com/pion/ion-log" 11 | ) 12 | 13 | const ( 14 | DefaultStatCycle = time.Second * 3 15 | DefaultGRPCTimeout = 15 * time.Second 16 | ) 17 | 18 | // RandomString generate a random string 19 | func RandomString(n int) string { 20 | var letterRunes = []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890") 21 | rand.Seed(time.Now().UnixNano()) 22 | b := make([]rune, n) 23 | for i := range b { 24 | b[i] = letterRunes[rand.Intn(len(letterRunes))] 25 | } 26 | return string(b) 27 | } 28 | 29 | // Recover print stack 30 | func Recover(flag string) { 31 | _, _, l, _ := runtime.Caller(1) 32 | if err := recover(); err != nil { 33 | log.Errorf("[%s] Recover panic line => %v", flag, l) 34 | log.Errorf("[%s] Recover err => %v", flag, err) 35 | debug.PrintStack() 36 | } 37 | } 38 | 39 | func GetRedisRoomKey(sid string) string { 40 | return "/ion/room/" + sid 41 | } 42 | 43 | func GetRedisPeerKey(sid, uid string) string { 44 | return "/ion/room/" + sid + "/" + uid 45 | } 46 | 47 | func GetRedisPeersPrefixKey(sid string) string { 48 | return "/ion/room/" + sid + "/*" 49 | } 50 | 51 | func GetArgs(args ...string) (arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10 string) { 52 | // at least sid uid 53 | if len(args) < 2 { 54 | return "", "", "", "", "", "", "", "", "", "" 55 | } 56 | // parse args 57 | for i, arg := range args { 58 | switch i { 59 | case 0: 60 | arg1 = arg 61 | case 1: 62 | arg2 = arg 63 | case 2: 64 | arg3 = arg 65 | case 3: 66 | arg4 = arg 67 | case 4: 68 | arg5 = arg 69 | case 5: 70 | arg6 = arg 71 | case 6: 72 | arg7 = arg 73 | case 7: 74 | arg8 = arg 75 | case 8: 76 | arg9 = arg 77 | case 9: 78 | arg10 = arg 79 | default: 80 | 81 | } 82 | } 83 | return arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10 84 | } 85 | 86 | func BoolToString(flag bool) string { 87 | if !flag { 88 | return "false" 89 | } 90 | return "true" 91 | } 92 | 93 | func StringToBool(flag string) bool { 94 | if strings.ToUpper(flag) == "TRUE" { 95 | return true 96 | } 97 | if flag == "1" { 98 | return true 99 | } 100 | return false 101 | } 102 | -------------------------------------------------------------------------------- /pkg/util/grpc.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "fmt" 5 | "net" 6 | 7 | "github.com/cloudwebrtc/nats-discovery/pkg/discovery" 8 | nrpc "github.com/cloudwebrtc/nats-grpc/pkg/rpc" 9 | log "github.com/pion/ion-log" 10 | "google.golang.org/grpc" 11 | ) 12 | 13 | // ClientConnInterface . 14 | type ClientConnInterface interface { 15 | grpc.ClientConnInterface 16 | Close() error 17 | } 18 | 19 | // NewGRPCClientForNode . 20 | func NewGRPCClientConnForNode(node discovery.Node) (ClientConnInterface, error) { 21 | switch node.RPC.Protocol { 22 | case discovery.GRPC: 23 | conn, err := grpc.Dial(node.RPC.Addr, grpc.WithInsecure(), grpc.WithBlock()) 24 | if err != nil { 25 | log.Errorf("did not connect: %v", err) 26 | return nil, err 27 | } 28 | return conn, err 29 | case discovery.NGRPC: 30 | nc, err := NewNatsConn(node.RPC.Addr) 31 | if err != nil { 32 | log.Errorf("new nats conn error %v", err) 33 | return nil, err 34 | } 35 | conn := nrpc.NewClient(nc, node.NID, "unkown") 36 | return conn, nil 37 | case discovery.JSONRPC: 38 | return nil, fmt.Errorf("%v not yet implementation", node.RPC.Protocol) 39 | } 40 | return nil, fmt.Errorf("New grpc client failed") 41 | } 42 | 43 | // ServiceInterface . 44 | type ServiceInterface interface { 45 | grpc.ServiceRegistrar 46 | Serve(lis net.Listener) error 47 | Stop() 48 | } 49 | 50 | type nrpcServer struct { 51 | *nrpc.Server 52 | } 53 | 54 | func (n *nrpcServer) Serve(lis net.Listener) error { 55 | return nil 56 | } 57 | 58 | //NewGRPCServiceForNode . 59 | func NewGRPCServiceForNode(node discovery.Node) (ServiceInterface, error) { 60 | switch node.RPC.Protocol { 61 | case discovery.GRPC: 62 | lis, err := net.Listen("tcp", node.RPC.Addr) 63 | if err != nil { 64 | log.Panicf("failed to listen: %v", err) 65 | } 66 | log.Infof("--- GRPC Server Listening at %s ---", node.RPC.Addr) 67 | 68 | s := grpc.NewServer() 69 | if err := s.Serve(lis); err != nil { 70 | log.Panicf("failed to serve: %v", err) 71 | return nil, err 72 | } 73 | return s, err 74 | case discovery.NGRPC: 75 | nc, err := NewNatsConn(node.RPC.Addr) 76 | if err != nil { 77 | log.Errorf("new nats conn error %v", err) 78 | return nil, err 79 | } 80 | s := nrpc.NewServer(nc, node.NID) 81 | return &nrpcServer{s}, nil 82 | case discovery.JSONRPC: 83 | return nil, fmt.Errorf("%v not yet implementation", node.RPC.Protocol) 84 | } 85 | 86 | return nil, fmt.Errorf("New grpc server failed") 87 | } 88 | -------------------------------------------------------------------------------- /scripts/deps: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | #set -eux 3 | 4 | APP_DIR=$(cd `dirname $0`/../;pwd) 5 | OS_TYPE="" 6 | . $APP_DIR/scripts/common 7 | ACTION=$1 8 | SERVICE=$2 9 | 10 | 11 | function run() 12 | { 13 | if [[ "$ACTION" =~ "status" ]];then 14 | status 15 | return 16 | fi 17 | 18 | if [[ "$OS_TYPE" =~ "Darwin" ]];then 19 | brew services $1 $2 20 | else 21 | sudo systemctl $1 $2 22 | fi 23 | } 24 | 25 | show_help() 26 | { 27 | echo "" 28 | echo "Usage: ./deps {start|stop|status} {nats-server|redis}" 29 | echo "" 30 | } 31 | 32 | function status() { 33 | if [[ "$SERVICE" == "" ]]; then 34 | exit 1 35 | fi 36 | if [ `pgrep -n $SERVICE` ]; then 37 | echo -e "\033[32m[OK]\033[0m: $SERVICE is running" 38 | else 39 | echo -e "\033[32m[Failed]\033[0m: $SERVICE is not running" 40 | fi 41 | } 42 | 43 | if [[ $# -ne 2 ]] ; then 44 | show_help 45 | exit 1 46 | fi 47 | 48 | if [[ "$ACTION" != "start" && "$ACTION" != "stop" && "$ACTION" != "status" ]]; then 49 | show_help 50 | exit 1 51 | fi 52 | 53 | if [[ "$SERVICE" != "nats-server" && "$SERVICE" != "redis" ]]; then 54 | show_help 55 | exit 1 56 | fi 57 | 58 | PID_FILE=$APP_DIR/configs/$SERVICE.pid 59 | LOG_FILE=$APP_DIR/logs/$SERVICE.log 60 | 61 | if [[ "$SERVICE" == "nats-server" && "$OS_TYPE" != "Darwin" ]]; then 62 | if [[ "$ACTION" == "start" ]]; then 63 | ## run command 64 | echo "nohup $SERVICE>$LOG_FILE 2>&1 &" 65 | nohup $SERVICE>>$LOG_FILE 2>&1 & 66 | pid=$! 67 | echo "$pid" 68 | echo "$pid" > $PID_FILE 69 | rpid=`ps aux | grep $pid |grep -v "grep" | awk '{print $2}'` 70 | if [[ $pid != $rpid ]];then 71 | echo "start failly. $pid $rpid" 72 | # rm $PID_FILE 73 | exit 1 74 | fi 75 | exit 0 76 | fi 77 | 78 | if [[ "$ACTION" == "stop" ]]; then 79 | echo "stop $SERVICE..." 80 | PID=`cat $PID_FILE` 81 | if [ ! -n "$PID" ]; then 82 | echo "pid not exist" 83 | exit 1; 84 | fi 85 | SUB_PIDS=`pgrep -P $PID` 86 | if [ -n "$SUB_PIDS" ]; then 87 | GRANDSON_PIDS=`pgrep -P $SUB_PIDS` 88 | fi 89 | 90 | #echo "kill $PID $SUB_PIDS $GRANDSON_PIDS" 91 | kill $PID $SUB_PIDS $GRANDSON_PIDS 92 | rm -rf $PID_FILE 93 | echo "stop $SERVICE ok" 94 | exit 0 95 | fi 96 | fi 97 | 98 | run $ACTION $SERVICE 99 | 100 | -------------------------------------------------------------------------------- /pkg/auth/auth.go: -------------------------------------------------------------------------------- 1 | package auth 2 | 3 | import ( 4 | "context" 5 | 6 | "google.golang.org/grpc" 7 | ) 8 | 9 | // AuthFunc is the pluggable function that performs authentication. 10 | // 11 | // The passed in `Context` will contain the gRPC metadata.MD object (for header-based authentication) and 12 | // the peer.Peer information that can contain transport-based credentials (e.g. `credentials.AuthInfo`). 13 | // 14 | // The returned context will be propagated to handlers, allowing user changes to `Context`. However, 15 | // please make sure that the `Context` returned is a child `Context` of the one passed in. 16 | // 17 | // If error is returned, its `grpc.Code()` will be returned to the user as well as the verbatim message. 18 | // Please make sure you use `codes.Unauthenticated` (lacking auth) and `codes.PermissionDenied` 19 | // (authed, but lacking perms) appropriately. 20 | type AuthFunc func(ctx context.Context, fullMethod string) (context.Context, error) 21 | 22 | // ServiceAuthFuncOverride allows a given gRPC service implementation to override the global `AuthFunc`. 23 | // 24 | // If a service implements the AuthFuncOverride method, it takes precedence over the `AuthFunc` method, 25 | // and will be called instead of AuthFunc for all method invocations within that service. 26 | type ServiceAuthFuncOverride interface { 27 | AuthFuncOverride(ctx context.Context, fullMethodName string) (context.Context, error) 28 | } 29 | 30 | // UnaryServerInterceptor returns a new unary server interceptors that performs per-request auth. 31 | func UnaryServerInterceptor(authFunc AuthFunc) grpc.UnaryServerInterceptor { 32 | return func(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) { 33 | var newCtx context.Context 34 | var err error 35 | if overrideSrv, ok := info.Server.(ServiceAuthFuncOverride); ok { 36 | newCtx, err = overrideSrv.AuthFuncOverride(ctx, info.FullMethod) 37 | } else { 38 | newCtx, err = authFunc(ctx, info.FullMethod) 39 | } 40 | if err != nil { 41 | return nil, err 42 | } 43 | return handler(newCtx, req) 44 | } 45 | } 46 | 47 | // StreamServerInterceptor returns a new unary server interceptors that performs per-request auth. 48 | func StreamServerInterceptor(authFunc AuthFunc) grpc.StreamServerInterceptor { 49 | return func(srv interface{}, stream grpc.ServerStream, info *grpc.StreamServerInfo, handler grpc.StreamHandler) error { 50 | var newCtx context.Context 51 | var err error 52 | if overrideSrv, ok := srv.(ServiceAuthFuncOverride); ok { 53 | newCtx, err = overrideSrv.AuthFuncOverride(stream.Context(), info.FullMethod) 54 | } else { 55 | newCtx, err = authFunc(stream.Context(), info.FullMethod) 56 | } 57 | if err != nil { 58 | return err 59 | } 60 | wrapped := WrapServerStream(stream) 61 | wrapped.WrappedContext = newCtx 62 | return handler(srv, wrapped) 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /pkg/node/islb/server.go: -------------------------------------------------------------------------------- 1 | package islb 2 | 3 | import ( 4 | "github.com/pion/ion/pkg/db" 5 | islb "github.com/pion/ion/proto/islb" 6 | ) 7 | 8 | type islbServer struct { 9 | islb.UnimplementedISLBServer 10 | redis *db.Redis 11 | islb *ISLB 12 | conf Config 13 | //watchers map[string]islb.ISLB_WatchISLBEventServer 14 | } 15 | 16 | func newISLBServer(conf Config, in *ISLB, redis *db.Redis) *islbServer { 17 | return &islbServer{ 18 | conf: conf, 19 | islb: in, 20 | redis: redis, 21 | //watchers: make(map[string]islb.ISLB_WatchISLBEventServer), 22 | } 23 | } 24 | 25 | /* 26 | //PostISLBEvent Receive ISLBEvent(stream or session events) from ion-SFU, ion-AVP and ion-SIP 27 | //the stream and session event will be save to redis db, which is used to create the 28 | //global location of the media stream 29 | // key = dc/ion-sfu-1/room1/uid 30 | // value = [...stream/track info ...] 31 | func (s *islbServer) PostISLBEvent(ctx context.Context, event *islb.ISLBEvent) (*ion.Empty, error) { 32 | log.Infof("ISLBServer.PostISLBEvent") 33 | switch payload := event.Payload.(type) { 34 | case *islb.ISLBEvent_Stream: 35 | stream := payload.Stream 36 | state := stream.State 37 | mkey := s.conf.Global.Dc + "/" + stream.Nid + "/" + stream.Sid + "/" + stream.Uid 38 | data, err := json.Marshal(stream.Streams) 39 | if err != nil { 40 | log.Errorf("json.Marshal err => %v", err) 41 | } 42 | 43 | jstr, err := json.MarshalIndent(stream.Streams, "", " ") 44 | if err != nil { 45 | log.Errorf("json.MarshalIndent failed %v", err) 46 | } 47 | log.Infof("ISLBEvent:\nmkey=> %v\nstate = %v\nstreams => %v", mkey, state.String(), string(jstr)) 48 | 49 | switch state { 50 | case ion.StreamEvent_ADD: 51 | err := s.redis.Set(mkey, string(data), redisLongKeyTTL) 52 | if err != nil { 53 | log.Errorf("s.Redis.Set failed %v", err) 54 | } 55 | case ion.StreamEvent_REMOVE: 56 | err := s.redis.Del(mkey) 57 | if err != nil { 58 | log.Errorf("s.Redis.Del failed %v", err) 59 | } 60 | } 61 | 62 | for _, wstream := range s.watchers { 63 | err := wstream.Send(event) 64 | if err != nil { 65 | log.Errorf("wstream.Send(event): failed %v", err) 66 | } 67 | } 68 | 69 | case *islb.ISLBEvent_Session: 70 | //session := payload.Session 71 | //log.Infof("ISLBEvent_Session event %v", session.String()) 72 | } 73 | return &ion.Empty{}, nil 74 | } 75 | 76 | //WatchISLBEvent broadcast ISLBEvent to ion-biz node. 77 | //The stream metadata is forwarded to biz node and coupled with the peer in the client through UID 78 | func (s *islbServer) WatchISLBEvent(stream islb.ISLB_WatchISLBEventServer) error { 79 | var sid string 80 | defer func() { 81 | delete(s.watchers, sid) 82 | }() 83 | for { 84 | req, err := stream.Recv() 85 | if err != nil { 86 | log.Errorf("ISLBServer.WatchISLBEvent server stream.Recv() err: %v", err) 87 | return err 88 | } 89 | log.Infof("ISLBServer.WatchISLBEvent req => %v", req) 90 | sid = req.Sid 91 | if _, found := s.watchers[sid]; !found { 92 | s.watchers[sid] = stream 93 | } 94 | } 95 | } 96 | */ 97 | -------------------------------------------------------------------------------- /pkg/node/islb/islb.go: -------------------------------------------------------------------------------- 1 | package islb 2 | 3 | import ( 4 | "errors" 5 | 6 | "github.com/cloudwebrtc/nats-discovery/pkg/discovery" 7 | nrpc "github.com/cloudwebrtc/nats-grpc/pkg/rpc" 8 | "github.com/cloudwebrtc/nats-grpc/pkg/rpc/reflection" 9 | log "github.com/pion/ion-log" 10 | "github.com/pion/ion/pkg/db" 11 | "github.com/pion/ion/pkg/ion" 12 | "github.com/pion/ion/pkg/proto" 13 | "github.com/pion/ion/pkg/util" 14 | pb "github.com/pion/ion/proto/islb" 15 | ) 16 | 17 | const ( 18 | // redisLongKeyTTL = 24 * time.Hour 19 | ) 20 | 21 | type global struct { 22 | Dc string `mapstructure:"dc"` 23 | } 24 | 25 | type logConf struct { 26 | Level string `mapstructure:"level"` 27 | } 28 | 29 | type natsConf struct { 30 | URL string `mapstructure:"url"` 31 | } 32 | 33 | // Config for islb node 34 | type Config struct { 35 | Global global `mapstructure:"global"` 36 | Log logConf `mapstructure:"log"` 37 | Nats natsConf `mapstructure:"nats"` 38 | Redis db.Config `mapstructure:"redis"` 39 | CfgFile string 40 | } 41 | 42 | // ISLB represents islb node 43 | type ISLB struct { 44 | ion.Node 45 | s *islbServer 46 | registry *Registry 47 | redis *db.Redis 48 | } 49 | 50 | // NewISLB create a islb node instance 51 | func NewISLB() *ISLB { 52 | return &ISLB{Node: ion.NewNode("islb-" + util.RandomString(6))} 53 | } 54 | 55 | // Start islb node 56 | func (i *ISLB) Start(conf Config) error { 57 | var err error 58 | 59 | err = i.Node.Start(conf.Nats.URL) 60 | if err != nil { 61 | i.Close() 62 | return err 63 | } 64 | 65 | i.redis = db.NewRedis(conf.Redis) 66 | if i.redis == nil { 67 | return errors.New("new redis error") 68 | } 69 | 70 | //registry for node discovery. 71 | i.registry, err = NewRegistry(conf.Global.Dc, i.Node.NatsConn(), i.redis) 72 | if err != nil { 73 | log.Errorf("%v", err) 74 | return err 75 | } 76 | 77 | i.s = newISLBServer(conf, i, i.redis) 78 | pb.RegisterISLBServer(i.Node.ServiceRegistrar(), i.s) 79 | 80 | // Register reflection service on nats-rpc server. 81 | reflection.Register(i.Node.ServiceRegistrar().(*nrpc.Server)) 82 | 83 | node := discovery.Node{ 84 | DC: conf.Global.Dc, 85 | Service: proto.ServiceISLB, 86 | NID: i.Node.NID, 87 | RPC: discovery.RPC{ 88 | Protocol: discovery.NGRPC, 89 | Addr: conf.Nats.URL, 90 | //Params: map[string]string{"username": "foo", "password": "bar"}, 91 | }, 92 | } 93 | 94 | go func() { 95 | err := i.Node.KeepAlive(node) 96 | if err != nil { 97 | log.Errorf("islb.Node.KeepAlive: error => %v", err) 98 | } 99 | }() 100 | 101 | //Watch ALL nodes. 102 | go func() { 103 | err := i.Node.Watch(proto.ServiceALL) 104 | if err != nil { 105 | log.Errorf("Node.Watch(proto.ServiceALL) error %v", err) 106 | } 107 | }() 108 | 109 | return nil 110 | } 111 | 112 | // Close all 113 | func (i *ISLB) Close() { 114 | i.Node.Close() 115 | if i.redis != nil { 116 | i.redis.Close() 117 | } 118 | if i.registry != nil { 119 | i.registry.Close() 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /pkg/node/islb/registry.go: -------------------------------------------------------------------------------- 1 | package islb 2 | 3 | import ( 4 | "sync" 5 | 6 | "github.com/cloudwebrtc/nats-discovery/pkg/discovery" 7 | "github.com/cloudwebrtc/nats-discovery/pkg/registry" 8 | "github.com/nats-io/nats.go" 9 | log "github.com/pion/ion-log" 10 | "github.com/pion/ion/pkg/db" 11 | "github.com/pion/ion/pkg/proto" 12 | ) 13 | 14 | type Registry struct { 15 | dc string 16 | redis *db.Redis 17 | reg *registry.Registry 18 | mutex sync.Mutex 19 | nodes map[string]discovery.Node 20 | } 21 | 22 | func NewRegistry(dc string, nc *nats.Conn, redis *db.Redis) (*Registry, error) { 23 | 24 | reg, err := registry.NewRegistry(nc, discovery.DefaultExpire) 25 | if err != nil { 26 | log.Errorf("registry.NewRegistry: error => %v", err) 27 | return nil, err 28 | } 29 | 30 | r := &Registry{ 31 | dc: dc, 32 | reg: reg, 33 | redis: redis, 34 | nodes: make(map[string]discovery.Node), 35 | } 36 | 37 | err = reg.Listen(r.handleNodeAction, r.handleGetNodes) 38 | 39 | if err != nil { 40 | log.Errorf("registry.Listen: error => %v", err) 41 | r.Close() 42 | return nil, err 43 | } 44 | 45 | return r, nil 46 | } 47 | 48 | func (r *Registry) Close() { 49 | r.reg.Close() 50 | } 51 | 52 | // handleNodeAction handle all Node from service discovery. 53 | // This callback can observe all nodes in the ion cluster, 54 | // TODO: Upload all node information to redis DB so that info 55 | // can be shared when there are more than one ISLB in the later. 56 | func (r *Registry) handleNodeAction(action discovery.Action, node discovery.Node) (bool, error) { 57 | //Add authentication here 58 | log.Debugf("handleNode: service %v, action %v => id %v, RPC %v", node.Service, action, node.ID(), node.RPC) 59 | 60 | //TODO: Put node info into the redis. 61 | r.mutex.Lock() 62 | defer r.mutex.Unlock() 63 | 64 | switch action { 65 | case discovery.Save: 66 | fallthrough 67 | case discovery.Update: 68 | r.nodes[node.ID()] = node 69 | case discovery.Delete: 70 | delete(r.nodes, node.ID()) 71 | } 72 | 73 | return true, nil 74 | } 75 | 76 | func (r *Registry) handleGetNodes(service string, params map[string]interface{}) ([]discovery.Node, error) { 77 | //Add load balancing here. 78 | log.Infof("Get node by %v, params %v", service, params) 79 | 80 | if service == proto.ServiceRTC { 81 | nid := "*" 82 | sid := "" 83 | if val, ok := params["nid"]; ok { 84 | nid = val.(string) 85 | } 86 | 87 | if val, ok := params["sid"]; ok { 88 | sid = val.(string) 89 | } 90 | 91 | // find node by nid/sid from reids 92 | mkey := r.dc + "/" + nid + "/" + sid 93 | log.Infof("islb.FindNode: mkey => %v", mkey) 94 | for _, key := range r.redis.Keys(mkey) { 95 | value := r.redis.Get(key) 96 | log.Debugf("key: %v, value: %v", key, value) 97 | } 98 | } 99 | 100 | r.mutex.Lock() 101 | defer r.mutex.Unlock() 102 | 103 | nodesResp := []discovery.Node{} 104 | for _, item := range r.nodes { 105 | if item.Service == service || service == "*" { 106 | nodesResp = append(nodesResp, item) 107 | } 108 | } 109 | 110 | return nodesResp, nil 111 | } 112 | -------------------------------------------------------------------------------- /scripts/all: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | #set -eux 3 | 4 | APP_DIR=$(cd `dirname $0`/../; pwd) 5 | OS_TYPE="" 6 | . $APP_DIR/scripts/common 7 | ACTION=$1 8 | 9 | show_help() 10 | { 11 | echo "" 12 | echo "Usage: ./all {start|stop|restart}" 13 | echo "" 14 | } 15 | 16 | if [[ $# -ne 1 ]] ; then 17 | show_help 18 | exit 1 19 | fi 20 | 21 | function start() 22 | { 23 | echo "------------nats-server--------------" 24 | $APP_DIR/scripts/deps start nats-server 25 | 26 | echo "------------redis--------------" 27 | $APP_DIR/scripts/deps start redis 28 | 29 | echo "------------islb--------------" 30 | $APP_DIR/scripts/service start islb 31 | 32 | echo "------------signal--------------" 33 | $APP_DIR/scripts/service start signal 34 | 35 | echo "------------room--------------" 36 | $APP_DIR/scripts/service start app-room 37 | 38 | echo "------------sfu--------------" 39 | $APP_DIR/scripts/service start sfu 40 | 41 | # echo "------------avp--------------" 42 | # $APP_DIR/scripts/service start avp 43 | 44 | echo "--------------------------" 45 | } 46 | 47 | function stop() 48 | { 49 | echo "------------room--------------" 50 | $APP_DIR/scripts/service stop app-room 51 | 52 | echo "------------signal--------------" 53 | $APP_DIR/scripts/service stop signal 54 | 55 | echo "------------islb--------------" 56 | $APP_DIR/scripts/service stop islb 57 | 58 | echo "------------sfu--------------" 59 | $APP_DIR/scripts/service stop sfu 60 | 61 | # echo "------------avp--------------" 62 | # $APP_DIR/scripts/service stop avp 63 | 64 | echo "------------nats-server--------------" 65 | $APP_DIR/scripts/deps stop nats-server 66 | 67 | echo "------------redis--------------" 68 | $APP_DIR/scripts/deps stop redis 69 | 70 | echo "--------------------------" 71 | } 72 | 73 | function status() 74 | { 75 | echo "------------nats-server--------------" 76 | $APP_DIR/scripts/deps status nats-server 77 | 78 | echo "------------redis--------------" 79 | $APP_DIR/scripts/deps status redis 80 | 81 | echo "------------room--------------" 82 | $APP_DIR/scripts/service status app-room 83 | 84 | echo "------------signal--------------" 85 | $APP_DIR/scripts/service status signal 86 | 87 | echo "------------islb--------------" 88 | $APP_DIR/scripts/service status islb 89 | 90 | echo "------------sfu--------------" 91 | $APP_DIR/scripts/service status sfu 92 | 93 | echo "--------------------------" 94 | } 95 | 96 | if [[ "$ACTION" != "start" && "$ACTION" != "stop" && "$ACTION" != "restart" && "$ACTION" != "status" ]]; then 97 | show_help 98 | exit 1 99 | fi 100 | 101 | if [[ "$ACTION" == "start" ]]; then 102 | start 103 | exit 0 104 | fi 105 | 106 | if [[ "$ACTION" == "stop" ]]; then 107 | stop 108 | exit 0 109 | fi 110 | 111 | if [[ "$ACTION" == "restart" ]]; then 112 | stop 113 | start 114 | exit 0 115 | fi 116 | 117 | if [[ "$ACTION" == "status" ]]; then 118 | status 119 | exit 0 120 | fi 121 | 122 | -------------------------------------------------------------------------------- /proto/rtc/rtc.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | option go_package = "github.com/pion/ion/proto/rtc"; 4 | 5 | package rtc; 6 | 7 | service RTC { 8 | rpc Signal(stream Request) returns (stream Reply) {} 9 | } 10 | 11 | message JoinRequest { 12 | string sid = 1; 13 | string uid = 2; 14 | map config = 3; 15 | SessionDescription description = 4; 16 | } 17 | 18 | message JoinReply { 19 | bool success = 1; 20 | Error error = 2; 21 | SessionDescription description = 3; 22 | } 23 | 24 | enum Target { 25 | PUBLISHER = 0; 26 | SUBSCRIBER = 1; 27 | } 28 | 29 | enum MediaType { 30 | MediaUnknown = 0; 31 | UserMedia = 1; 32 | ScreenCapture = 2; 33 | Cavans = 3; 34 | Streaming = 4; 35 | VoIP = 5; 36 | } 37 | 38 | message TrackInfo { 39 | // basic info 40 | string id = 1; 41 | string kind = 2; 42 | bool muted = 3; 43 | MediaType type = 4; 44 | string streamId = 5; 45 | string label = 6; 46 | 47 | // extra info 48 | string layer = 7; // simulcast or svc layer 49 | uint32 width = 8; 50 | uint32 height = 9; 51 | uint32 frameRate = 10; 52 | } 53 | 54 | message SessionDescription { 55 | Target target = 1; 56 | // 'offer' | 'answer' 57 | string type = 2; 58 | // sdp contents 59 | string sdp = 3; 60 | // sdp metdata 61 | repeated TrackInfo trackInfos = 4; 62 | } 63 | 64 | message Trickle { 65 | Target target = 1; 66 | string init = 2; 67 | } 68 | 69 | message Error { 70 | int32 code = 1; 71 | string reason = 2; 72 | } 73 | 74 | message TrackEvent { 75 | enum State { 76 | ADD = 0; 77 | UPDATE = 1; 78 | REMOVE = 2; 79 | } 80 | State state = 1; 81 | string uid = 2; 82 | repeated TrackInfo tracks = 3; 83 | } 84 | 85 | message Subscription{ 86 | string trackId = 2; 87 | bool mute = 3; // mute track or not 88 | bool subscribe = 4; // sub track or not 89 | string layer = 5; // simulcast or svc layer 90 | } 91 | 92 | message SubscriptionRequest { 93 | repeated Subscription subscriptions = 1; 94 | } 95 | 96 | message SubscriptionReply { 97 | bool success = 1; 98 | Error error = 2; 99 | } 100 | 101 | message UpdateTrackReply { 102 | bool success = 1; 103 | Error error = 2; 104 | } 105 | 106 | message ActiveSpeaker { 107 | repeated AudioLevelSpeaker speakers = 1; 108 | } 109 | 110 | message AudioLevelSpeaker { 111 | string sid = 1; 112 | // audio level 113 | float level = 2; 114 | // speaker active or not 115 | bool active = 3; 116 | } 117 | 118 | message Request { 119 | oneof payload { 120 | // Basic API Request 121 | JoinRequest join = 1; 122 | SessionDescription description = 2; 123 | Trickle trickle = 3; 124 | 125 | // Command 126 | SubscriptionRequest subscription = 4; 127 | } 128 | } 129 | 130 | message Reply { 131 | oneof payload { 132 | // Basic API Reply 133 | JoinReply join = 1; 134 | SessionDescription description = 2; 135 | Trickle trickle = 3; 136 | 137 | // Event 138 | TrackEvent trackEvent = 4; 139 | 140 | // Command Reply 141 | SubscriptionReply subscription = 5; 142 | 143 | // Error 144 | Error error = 7; 145 | } 146 | } -------------------------------------------------------------------------------- /cmd/signal/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/http" 8 | "os" 9 | 10 | nrpc "github.com/cloudwebrtc/nats-grpc/pkg/rpc" 11 | nproxy "github.com/cloudwebrtc/nats-grpc/pkg/rpc/proxy" 12 | log "github.com/pion/ion-log" 13 | "github.com/pion/ion/pkg/node/signal" 14 | "github.com/pion/ion/pkg/util" 15 | "github.com/spf13/viper" 16 | "google.golang.org/grpc" 17 | ) 18 | 19 | var ( 20 | conf = signal.Config{} 21 | file, paddr string 22 | ) 23 | 24 | func showHelp() { 25 | fmt.Printf("Usage:%s {params}\n", os.Args[0]) 26 | fmt.Println(" -c {config file}") 27 | fmt.Println(" -h (show help info)") 28 | } 29 | 30 | func unmarshal(rawVal interface{}) bool { 31 | if err := viper.Unmarshal(rawVal); err != nil { 32 | fmt.Printf("config file %s loaded failed. %v\n", file, err) 33 | return false 34 | } 35 | return true 36 | } 37 | 38 | func load() bool { 39 | _, err := os.Stat(file) 40 | if err != nil { 41 | return false 42 | } 43 | 44 | viper.SetConfigFile(file) 45 | viper.SetConfigType("toml") 46 | 47 | err = viper.ReadInConfig() 48 | if err != nil { 49 | fmt.Printf("config file %s read failed. %v\n", file, err) 50 | return false 51 | } 52 | 53 | if !unmarshal(&conf) || !unmarshal(&conf.Signal) { 54 | return false 55 | } 56 | if err != nil { 57 | fmt.Printf("config file %s loaded failed. %v\n", file, err) 58 | return false 59 | } 60 | 61 | fmt.Printf("config %s load ok!\n", file) 62 | 63 | return true 64 | } 65 | 66 | func parse() bool { 67 | flag.StringVar(&file, "c", "configs/sig.toml", "config file") 68 | flag.StringVar(&paddr, "paddr", ":6060", "pprof listening addr") 69 | 70 | help := flag.Bool("h", false, "help info") 71 | flag.Parse() 72 | if !load() { 73 | return false 74 | } 75 | 76 | if *help { 77 | showHelp() 78 | return false 79 | } 80 | return true 81 | } 82 | 83 | func main() { 84 | if !parse() { 85 | showHelp() 86 | os.Exit(-1) 87 | } 88 | 89 | log.Init(conf.Log.Level) 90 | 91 | if paddr != "" { 92 | go func() { 93 | log.Infof("start pprof on %s", paddr) 94 | err := http.ListenAndServe(paddr, nil) 95 | if err != nil { 96 | log.Errorf("http.ListenAndServe err=%v", err) 97 | } 98 | }() 99 | } 100 | 101 | addr := fmt.Sprintf("%s:%d", conf.Signal.GRPC.Host, conf.Signal.GRPC.Port) 102 | log.Infof("--- Starting Signal (gRPC + gRPC-Web) Server ---") 103 | log.Infof("--- Bind to %s ---", addr) 104 | 105 | sig, err := signal.NewSignal(conf) 106 | if err != nil { 107 | log.Errorf("new signal: %v", err) 108 | os.Exit(-1) 109 | } 110 | err = sig.Start() 111 | if err != nil { 112 | log.Errorf("signal.Start: %v", err) 113 | os.Exit(-1) 114 | } 115 | defer sig.Close() 116 | 117 | srv := grpc.NewServer( 118 | grpc.CustomCodec(nrpc.Codec()), // nolint:staticcheck 119 | grpc.UnknownServiceHandler(nproxy.TransparentLongConnectionHandler(sig.Director))) 120 | 121 | s := util.NewWrapperedGRPCWebServer(util.NewWrapperedServerOptions( 122 | addr, conf.Signal.GRPC.Cert, conf.Signal.GRPC.Key, true), srv) 123 | 124 | if err := s.Serve(); err != nil { 125 | log.Panicf("failed to serve: %v", err) 126 | } 127 | select {} 128 | } 129 | -------------------------------------------------------------------------------- /scripts/service: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | #set -eux 3 | 4 | APP_DIR=$(cd `dirname $0`/../; pwd) 5 | cd $APP_DIR 6 | mkdir -p $APP_DIR/logs 7 | SERVICE=$1 8 | #COMMAND=$APP_DIR/bin/$SERVICE 9 | # CONFIG=$APP_DIR/configs/$SERVICE.toml 10 | # PID_FILE=$APP_DIR/configs/$SERVICE.pid 11 | # LOG_FILE=$APP_DIR/logs/$SERVICE.log 12 | 13 | ACTION=$1 14 | SERVICE=$2 15 | 16 | show_help() 17 | { 18 | echo "" 19 | echo "Usage: ./service {start|stop|status} {app-room|signal|islb|sfu}" 20 | echo "" 21 | } 22 | 23 | if [[ $# -ne 2 ]] ; then 24 | show_help 25 | exit 1 26 | fi 27 | 28 | function start() 29 | { 30 | COMMAND=$APP_DIR/bin/$1 31 | CONFIG=$APP_DIR/configs/$SERVICE.toml 32 | PID_FILE=$APP_DIR/configs/$SERVICE.pid 33 | LOG_FILE=$APP_DIR/logs/$SERVICE.log 34 | 35 | count=`ps -ef |grep " $COMMAND " |grep -v "grep" |wc -l` 36 | if [ 0 != $count ];then 37 | ps aux | grep " $COMMAND " | grep -v "grep" 38 | echo "already start" 39 | return 1; 40 | fi 41 | 42 | if [ ! -r $CONFIG ]; then 43 | echo "$CONFIG not exsist" 44 | return 1; 45 | fi 46 | 47 | BUILD_PATH=$APP_DIR/cmd/$SERVICE 48 | if [[ "$SERVICE" == app-room ]]; then 49 | BUILD_PATH=$APP_DIR/apps/${SERVICE:4} 50 | fi 51 | cd $BUILD_PATH 52 | echo "go build -o $COMMAND" 53 | go build -o $COMMAND 54 | cd $APP_DIR 55 | 56 | echo "nohup $COMMAND -c $CONFIG >$LOG_FILE 2>&1 &" 57 | nohup $COMMAND -c $CONFIG >$LOG_FILE 2>&1 & 58 | pid=$! 59 | echo "$pid" > $PID_FILE 60 | rpid=`ps aux | grep $pid |grep -v "grep" | awk '{print $2}'` 61 | if [[ $pid != $rpid ]];then 62 | echo "start failly." 63 | rm $PID_FILE 64 | return 1 65 | fi 66 | } 67 | 68 | function stop(){ 69 | PID_FILE=$APP_DIR/configs/$1.pid 70 | echo "$PID_FILE......" 71 | 72 | echo "stop $SERVICE..." 73 | PID=`cat $PID_FILE` 74 | if [ ! -n "$PID" ]; then 75 | PID=`ps -ef | grep "$1" | grep -v "grep" | grep -v "service" | awk '{print $2}'` 76 | if [ ! -n "$PID" ]; then 77 | return 1 78 | fi 79 | fi 80 | SUB_PIDS=`pgrep -P $PID` 81 | if [ -n "$SUB_PIDS" ]; then 82 | GRANDSON_PIDS=`pgrep -P $SUB_PIDS` 83 | fi 84 | 85 | # echo "kill $PID $SUB_PIDS $GRANDSON_PIDS" 86 | kill $PID $SUB_PIDS $GRANDSON_PIDS 87 | rm -rf $PID_FILE 88 | echo "stop ok" 89 | } 90 | 91 | function restart() { 92 | echo " $1" 93 | stop $1 94 | start $1 95 | } 96 | 97 | function status() { 98 | if [[ "$SERVICE" == "" ]]; then 99 | exit 1 100 | fi 101 | if [ `pgrep -n $SERVICE` ]; then 102 | echo -e "\033[32m[OK]\033[0m: $SERVICE is running" 103 | else 104 | echo -e "\033[31m[Failed]\033[0m: $SERVICE is not running, run by service scripts" 105 | show_help 106 | fi 107 | } 108 | 109 | if [[ "$ACTION" != "start" && "$ACTION" != "stop" && "$ACTION" != "status" && "$ACTION" != "restart" ]]; then 110 | show_help 111 | exit 1 112 | fi 113 | 114 | if [[ "$SERVICE" != "app-room" && "$SERVICE" != "signal" && "$SERVICE" != "islb" && "$SERVICE" != "sfu" ]]; then 115 | show_help 116 | exit 1 117 | fi 118 | 119 | if [[ "$ACTION" == "start" ]]; then 120 | start $SERVICE 121 | exit 0 122 | fi 123 | 124 | if [[ "$ACTION" == "stop" ]]; then 125 | stop $SERVICE 126 | exit 0 127 | fi 128 | 129 | if [[ "$ACTION" == "status" ]]; then 130 | status $SERVICE 131 | exit 0 132 | fi 133 | 134 | if [[ "$ACTION" == "restart" ]]; then 135 | restart $SERVICE 136 | exit 0 137 | fi 138 | -------------------------------------------------------------------------------- /pkg/db/redis_test.go: -------------------------------------------------------------------------------- 1 | package db 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | ) 7 | 8 | func NewRedisSingle() *Redis { 9 | r := NewRedis(Config{ 10 | Addrs: []string{ 11 | ":6379", 12 | }, 13 | DB: 0, 14 | Pwd: "", 15 | }) 16 | return r 17 | } 18 | 19 | func TestNew_Close(t *testing.T) { 20 | r := NewRedisSingle() 21 | defer r.Close() 22 | } 23 | 24 | func TestAcquire_Release(t *testing.T) { 25 | r := NewRedisSingle() 26 | defer r.Close() 27 | 28 | lockKey := "123" 29 | r.Acquire(lockKey) 30 | res := r.Get("lock-" + lockKey) 31 | if res != "1" { 32 | t.Errorf("acquire error") 33 | } 34 | t.Log("acquire ok") 35 | 36 | r.Release(lockKey) 37 | res = r.Get("lock-" + lockKey) 38 | if res != "" { 39 | t.Errorf("release error") 40 | } 41 | t.Log("release ok") 42 | } 43 | 44 | func TestConcurrencyAcquire(t *testing.T) { 45 | r := NewRedisSingle() 46 | defer r.Close() 47 | lockKey := "123" 48 | t.Log("A acquire") 49 | r.Acquire(lockKey) 50 | 51 | go func() { 52 | t.Log("B try acquire") 53 | r.Acquire(lockKey) 54 | t.Log("B acquire ok") 55 | r.Release(lockKey) 56 | t.Log("B release") 57 | }() 58 | 59 | time.Sleep(time.Second * 2) 60 | t.Log("A wait 2 second") 61 | r.Release(lockKey) 62 | t.Log("A release ok") 63 | time.Sleep(time.Second * 2) 64 | } 65 | 66 | func TestSet_Get_Del(t *testing.T) { 67 | r := NewRedisSingle() 68 | defer r.Close() 69 | key, value := "key", "value" 70 | err := r.Set(key, value, time.Second) 71 | if err != nil { 72 | t.Error("Set error") 73 | } 74 | res := r.Get(key) 75 | if res != value { 76 | t.Error("Get error") 77 | } 78 | err = r.Del(key) 79 | if err != nil { 80 | t.Error("Del error") 81 | } 82 | res = r.Get(key) 83 | if res != "" { 84 | t.Error("Get error") 85 | } 86 | } 87 | 88 | func TestHSet_HGet_HDel(t *testing.T) { 89 | r := NewRedisSingle() 90 | defer r.Close() 91 | key, field, value := "key", "field", "value" 92 | err := r.HSet(key, field, value) 93 | if err != nil { 94 | t.Error("HSet error:", err) 95 | } 96 | 97 | res := r.HGet(key, field) 98 | if res != value { 99 | t.Error("HGet error:", err) 100 | } 101 | 102 | err = r.HDel(key, field) 103 | if err != nil { 104 | t.Error("HDel error:", err) 105 | } 106 | } 107 | 108 | func TestHMSet_HMGet_HGetAll_HDel(t *testing.T) { 109 | r := NewRedisSingle() 110 | defer r.Close() 111 | key, field1, value1, field2, value2 := "key", "field1", "value1", "field2", "value2" 112 | err := r.HMSet(key, field1, value1, field2, value2) 113 | if err != nil { 114 | t.Error("HMSet error:", err) 115 | } 116 | 117 | res := r.HMGet(key, field1, field2) 118 | if res[0] != value1 && res[1] != value2 { 119 | t.Error("HMGet error:", err) 120 | } 121 | 122 | resMap := r.HGetAll(key) 123 | if resMap[field1] != value1 && resMap[field2] != value2 { 124 | t.Error("HGetAll error:", err) 125 | } 126 | 127 | err = r.Del(key) 128 | if err != nil { 129 | t.Error("Del error:", err) 130 | } 131 | } 132 | 133 | func TestHSetTTL_HMSetTTL(t *testing.T) { 134 | r := NewRedisSingle() 135 | defer r.Close() 136 | key, field, value := "key", "field", "value" 137 | err := r.HSetTTL(time.Second, key, field, value) 138 | if err != nil { 139 | t.Error("HSetTTL error:", err) 140 | } 141 | 142 | res := r.HGet(key, field) 143 | if res != value { 144 | t.Error("HGet error:", err) 145 | } 146 | time.Sleep(time.Second * 2) 147 | 148 | res = r.HGet(key, field) 149 | if res == value { 150 | t.Error("HGet error:", err) 151 | } 152 | 153 | err = r.HDel(key, field) 154 | if err != nil { 155 | t.Error("HDel error:", err) 156 | } 157 | } 158 | -------------------------------------------------------------------------------- /pkg/node/sfu/sfu_test.go: -------------------------------------------------------------------------------- 1 | package sfu 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "testing" 7 | 8 | "github.com/cloudwebrtc/nats-grpc/pkg/rpc" 9 | "github.com/nats-io/nats.go" 10 | log "github.com/pion/ion-log" 11 | pb "github.com/pion/ion-sfu/cmd/signal/grpc/proto" 12 | "github.com/pion/webrtc/v3" 13 | "github.com/tj/assert" 14 | ) 15 | 16 | var ( 17 | conf = Config{ 18 | Global: global{ 19 | Dc: "dc1", 20 | }, 21 | Nats: natsConf{ 22 | URL: "nats://127.0.0.1:4222", 23 | }, 24 | } 25 | 26 | nid = "sfu-01" 27 | ) 28 | 29 | func init() { 30 | log.Init("info") 31 | 32 | } 33 | 34 | func TestStart(t *testing.T) { 35 | s := NewSFU() 36 | 37 | err := s.Start(conf) 38 | if err != nil { 39 | t.Error(err) 40 | } 41 | 42 | opts := []nats.Option{nats.Name("nats-grpc echo client")} 43 | // Connect to the NATS server. 44 | nc, err := nats.Connect(conf.Nats.URL, opts...) 45 | if err != nil { 46 | t.Error(err) 47 | } 48 | defer nc.Close() 49 | 50 | ncli := rpc.NewClient(nc, nid, "unkown") 51 | cli := pb.NewSFUClient(ncli) 52 | 53 | stream, err := cli.Signal(context.Background()) 54 | if err != nil { 55 | t.Error(err) 56 | } 57 | 58 | me := webrtc.MediaEngine{} 59 | assert.NoError(t, err) 60 | api := webrtc.NewAPI(webrtc.WithMediaEngine(&me)) 61 | pub, err := api.NewPeerConnection(webrtc.Configuration{}) 62 | assert.NoError(t, err) 63 | 64 | pub.OnICEConnectionStateChange(func(state webrtc.ICEConnectionState) { 65 | log.Infof("ICEConnectionState %v", state.String()) 66 | }) 67 | 68 | pub.OnICECandidate(func(candidate *webrtc.ICECandidate) { 69 | if candidate == nil { 70 | return 71 | } 72 | log.Infof("OnICECandidate %v", candidate) 73 | bytes, err := json.Marshal(candidate) 74 | if err != nil { 75 | log.Errorf("OnIceCandidate error %s", err) 76 | } 77 | err = stream.Send(&pb.SignalRequest{ 78 | Payload: &pb.SignalRequest_Trickle{ 79 | Trickle: &pb.Trickle{ 80 | Target: pb.Trickle_PUBLISHER, 81 | Init: string(bytes), 82 | }, 83 | }, 84 | }) 85 | if err != nil { 86 | t.Error(err) 87 | } 88 | }) 89 | 90 | _, err = pub.CreateDataChannel("ion-sfu", nil) 91 | if err != nil { 92 | t.Error(err) 93 | } 94 | offer, err := pub.CreateOffer(nil) 95 | if err != nil { 96 | t.Error(err) 97 | } 98 | log.Infof("offer => %v", offer) 99 | 100 | marshalled, err := json.Marshal(offer) 101 | if err != nil { 102 | t.Error(err) 103 | } 104 | 105 | err = stream.Send(&pb.SignalRequest{ 106 | Payload: &pb.SignalRequest_Join{ 107 | Join: &pb.JoinRequest{ 108 | Sid: "room1", 109 | Description: marshalled, 110 | }, 111 | }, 112 | }) 113 | 114 | if err != nil { 115 | t.Error(err) 116 | } 117 | 118 | err = pub.SetLocalDescription(offer) 119 | 120 | if err != nil { 121 | t.Error(err) 122 | } 123 | 124 | for { 125 | reply, err := stream.Recv() 126 | if err != nil { 127 | t.Fatalf("Signal: err %s", err) 128 | break 129 | } 130 | log.Debugf("\nReply: reply %v\n", reply) 131 | 132 | switch payload := reply.Payload.(type) { 133 | case *pb.SignalReply_Description: 134 | var sdp webrtc.SessionDescription 135 | err := json.Unmarshal(payload.Description, &offer) 136 | if err != nil { 137 | t.Error(err) 138 | } 139 | err = pub.SetRemoteDescription(sdp) 140 | if err != nil { 141 | t.Error(err) 142 | } 143 | case *pb.SignalReply_Trickle: 144 | var candidate webrtc.ICECandidateInit 145 | err := json.Unmarshal([]byte(payload.Trickle.Init), &candidate) 146 | if err != nil { 147 | t.Error(err) 148 | } 149 | err = pub.AddICECandidate(candidate) 150 | if err != nil { 151 | t.Error(err) 152 | } 153 | //return 154 | } 155 | } 156 | 157 | s.Close() 158 | } 159 | -------------------------------------------------------------------------------- /configs/docker/sfu.toml: -------------------------------------------------------------------------------- 1 | [global] 2 | # data center id 3 | dc = "dc1" 4 | 5 | [nats] 6 | url = "nats://nats:4222" 7 | 8 | [sfu] 9 | # Ballast size in MiB, will allocate memory to reduce the GC trigger upto 2x the 10 | # size of ballast. Be aware that the ballast should be less than the half of memory 11 | # available. 12 | ballast = 0 13 | # enable prometheus sfu statistics 14 | withstats = false 15 | 16 | [router] 17 | # Limit the remb bandwidth in kbps 18 | # zero means no limits 19 | maxbandwidth = 1500 20 | # max number of video tracks packets the SFU will keep track 21 | maxpackettrack = 500 22 | # Sets the audio level volume threshold. 23 | # Values from [0-127] where 0 is the loudest. 24 | # Audio levels are read from rtp extension header according to: 25 | # https://tools.ietf.org/html/rfc6464 26 | audiolevelthreshold = 40 27 | # Sets the interval in which the SFU will check the audio level 28 | # in [ms]. If the active speaker has changed, the sfu will 29 | # emit an event to clients. 30 | audiolevelinterval=1000 31 | # Sets minimum percentage of events required to fire an audio level 32 | # according to the expected events from the audiolevelinterval, 33 | # calculated as audiolevelinterval/packetization time (20ms for 8kHz) 34 | # Values from [0-100] 35 | audiolevelfilter = 20 36 | 37 | [router.simulcast] 38 | # Prefer best quality initially 39 | bestqualityfirst = true 40 | # EXPERIMENTAL enable temporal layer change is currently an experimental feature, 41 | # enable only for testing. 42 | enabletemporallayer = false 43 | 44 | [webrtc] 45 | # Single port, portrange will not work if you enable this 46 | singleport = 5000 47 | 48 | # Range of ports that ion accepts WebRTC traffic on 49 | # Format: [min, max] and max - min >= 100 50 | # portrange = [5000, 5200] 51 | # if sfu behind nat, set iceserver 52 | # [[webrtc.iceserver]] 53 | # urls = ["stun:stun.stunprotocol.org:3478"] 54 | # [[webrtc.iceserver]] 55 | # urls = ["turn:turn.awsome.org:3478"] 56 | # username = "awsome" 57 | # credential = "awsome" 58 | 59 | # sdp semantics: 60 | # "unified-plan" 61 | # "plan-b" 62 | # "unified-plan-with-fallback" 63 | sdpsemantics = "unified-plan" 64 | # toggle multicast dns support: https://tools.ietf.org/html/draft-mdns-ice-candidates-00 65 | mdns = true 66 | 67 | [webrtc.candidates] 68 | # In case you're deploying ion-sfu on a server which is configured with 69 | # a 1:1 NAT (e.g., Amazon EC2), you might want to also specify the public 70 | # address of the machine using the setting below. This will result in 71 | # all host candidates (which normally have a private IP address) to 72 | # be rewritten with the public address provided in the settings. As 73 | # such, use the option with caution and only if you know what you're doing. 74 | # Multiple public IP addresses can be specified as a comma separated list 75 | # if the sfu is deployed in a DMZ between two 1-1 NAT for internal and 76 | # external users. 77 | nat1to1 = ["127.0.0.1"] 78 | icelite = true 79 | 80 | [webrtc.timeouts] 81 | # The duration in [sec] without network activity before a ICE Agent is considered disconnected 82 | disconnected = 5 83 | # The duration in [sec] without network activity before a ICE Agent is considered failed after disconnected 84 | failed = 25 85 | # How often in [sec] the ICE Agent sends extra traffic if there is no activity, if media is flowing no traffic will be sent 86 | keepalive = 2 87 | 88 | [turn] 89 | # Enables embeded turn server 90 | enabled = false 91 | # Sets the realm for turn server 92 | realm = "ion" 93 | # The address the TURN server will listen on. 94 | address = "0.0.0.0:3478" 95 | # Certs path to config tls/dtls 96 | # cert="path/to/cert.pem" 97 | # key="path/to/key.pem" 98 | # Port range that turn relays to SFU 99 | # WARNING: It shouldn't overlap webrtc.portrange 100 | # Format: [min, max] 101 | # portrange = [5201, 5400] 102 | [turn.auth] 103 | # Use an auth secret to generate long-term credentials defined in RFC5389-10.2 104 | # NOTE: This takes precedence over `credentials` if defined. 105 | # secret = "secret" 106 | # Sets the credentials pairs 107 | credentials = "pion=ion,pion2=ion2" 108 | 109 | [log] 110 | # 0 - INFO 1 - DEBUG 2 - TRACE 111 | v = 1 112 | -------------------------------------------------------------------------------- /configs/sfu.toml: -------------------------------------------------------------------------------- 1 | [global] 2 | # data center id 3 | dc = "dc1" 4 | 5 | [nats] 6 | url = "nats://127.0.0.1:4222" 7 | 8 | 9 | [sfu] 10 | # Ballast size in MiB, will allocate memory to reduce the GC trigger upto 2x the 11 | # size of ballast. Be aware that the ballast should be less than the half of memory 12 | # available. 13 | ballast = 0 14 | # enable prometheus sfu statistics 15 | withstats = false 16 | 17 | [router] 18 | # Limit the remb bandwidth in kbps 19 | # zero means no limits 20 | maxbandwidth = 1500 21 | # max number of video tracks packets the SFU will keep track 22 | maxpackettrack = 500 23 | # Sets the audio level volume threshold. 24 | # Values from [0-127] where 0 is the loudest. 25 | # Audio levels are read from rtp extension header according to: 26 | # https://tools.ietf.org/html/rfc6464 27 | audiolevelthreshold = 40 28 | # Sets the interval in which the SFU will check the audio level 29 | # in [ms]. If the active speaker has changed, the sfu will 30 | # emit an event to clients. 31 | audiolevelinterval=1000 32 | # Sets minimum percentage of events required to fire an audio level 33 | # according to the expected events from the audiolevelinterval, 34 | # calculated as audiolevelinterval/packetization time (20ms for 8kHz) 35 | # Values from [0-100] 36 | audiolevelfilter = 20 37 | 38 | [router.simulcast] 39 | # Prefer best quality initially 40 | bestqualityfirst = true 41 | # EXPERIMENTAL enable temporal layer change is currently an experimental feature, 42 | # enable only for testing. 43 | enabletemporallayer = false 44 | 45 | [webrtc] 46 | # Single port, portrange will not work if you enable this 47 | singleport = 5000 48 | 49 | # Range of ports that ion accepts WebRTC traffic on 50 | # Format: [min, max] and max - min >= 100 51 | #portrange = [5000, 5200] 52 | # if sfu behind nat, set iceserver 53 | # [[webrtc.iceserver]] 54 | # urls = ["stun:stun.stunprotocol.org:3478"] 55 | # [[webrtc.iceserver]] 56 | # urls = ["turn:turn.awsome.org:3478"] 57 | # username = "awsome" 58 | # credential = "awsome" 59 | 60 | # sdp semantics: 61 | # "unified-plan" 62 | # "plan-b" 63 | # "unified-plan-with-fallback" 64 | sdpsemantics = "unified-plan" 65 | # toggle multicast dns support: https://tools.ietf.org/html/draft-mdns-ice-candidates-00 66 | mdns = true 67 | 68 | [webrtc.candidates] 69 | # In case you're deploying ion-sfu on a server which is configured with 70 | # a 1:1 NAT (e.g., Amazon EC2), you might want to also specify the public 71 | # address of the machine using the setting below. This will result in 72 | # all host candidates (which normally have a private IP address) to 73 | # be rewritten with the public address provided in the settings. As 74 | # such, use the option with caution and only if you know what you're doing. 75 | # Multiple public IP addresses can be specified as a comma separated list 76 | # if the sfu is deployed in a DMZ between two 1-1 NAT for internal and 77 | # external users. 78 | nat1to1 = ["127.0.0.1"] 79 | #icelite = true 80 | 81 | [webrtc.timeouts] 82 | # The duration in [sec] without network activity before a ICE Agent is considered disconnected 83 | disconnected = 5 84 | # The duration in [sec] without network activity before a ICE Agent is considered failed after disconnected 85 | failed = 25 86 | # How often in [sec] the ICE Agent sends extra traffic if there is no activity, if media is flowing no traffic will be sent 87 | keepalive = 2 88 | 89 | [turn] 90 | # Enables embeded turn server 91 | enabled = false 92 | # Sets the realm for turn server 93 | realm = "ion" 94 | # The address the TURN server will listen on. 95 | address = "0.0.0.0:3478" 96 | # Certs path to config tls/dtls 97 | # cert="path/to/cert.pem" 98 | # key="path/to/key.pem" 99 | # Port range that turn relays to SFU 100 | # WARNING: It shouldn't overlap webrtc.portrange 101 | # Format: [min, max] 102 | # portrange = [5201, 5400] 103 | [turn.auth] 104 | # Use an auth secret to generate long-term credentials defined in RFC5389-10.2 105 | # NOTE: This takes precedence over `credentials` if defined. 106 | # secret = "secret" 107 | # Sets the credentials pairs 108 | credentials = "pion=ion,pion2=ion2" 109 | 110 | [log] 111 | # 0 - INFO 1 - DEBUG 2 - TRACE 112 | v = 1 113 | -------------------------------------------------------------------------------- /proto/rtc/rtc_grpc.pb.go: -------------------------------------------------------------------------------- 1 | // Code generated by protoc-gen-go-grpc. DO NOT EDIT. 2 | 3 | package rtc 4 | 5 | import ( 6 | context "context" 7 | grpc "google.golang.org/grpc" 8 | codes "google.golang.org/grpc/codes" 9 | status "google.golang.org/grpc/status" 10 | ) 11 | 12 | // This is a compile-time assertion to ensure that this generated file 13 | // is compatible with the grpc package it is being compiled against. 14 | // Requires gRPC-Go v1.32.0 or later. 15 | const _ = grpc.SupportPackageIsVersion7 16 | 17 | // RTCClient is the client API for RTC service. 18 | // 19 | // For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream. 20 | type RTCClient interface { 21 | Signal(ctx context.Context, opts ...grpc.CallOption) (RTC_SignalClient, error) 22 | } 23 | 24 | type rTCClient struct { 25 | cc grpc.ClientConnInterface 26 | } 27 | 28 | func NewRTCClient(cc grpc.ClientConnInterface) RTCClient { 29 | return &rTCClient{cc} 30 | } 31 | 32 | func (c *rTCClient) Signal(ctx context.Context, opts ...grpc.CallOption) (RTC_SignalClient, error) { 33 | stream, err := c.cc.NewStream(ctx, &RTC_ServiceDesc.Streams[0], "/rtc.RTC/Signal", opts...) 34 | if err != nil { 35 | return nil, err 36 | } 37 | x := &rTCSignalClient{stream} 38 | return x, nil 39 | } 40 | 41 | type RTC_SignalClient interface { 42 | Send(*Request) error 43 | Recv() (*Reply, error) 44 | grpc.ClientStream 45 | } 46 | 47 | type rTCSignalClient struct { 48 | grpc.ClientStream 49 | } 50 | 51 | func (x *rTCSignalClient) Send(m *Request) error { 52 | return x.ClientStream.SendMsg(m) 53 | } 54 | 55 | func (x *rTCSignalClient) Recv() (*Reply, error) { 56 | m := new(Reply) 57 | if err := x.ClientStream.RecvMsg(m); err != nil { 58 | return nil, err 59 | } 60 | return m, nil 61 | } 62 | 63 | // RTCServer is the server API for RTC service. 64 | // All implementations must embed UnimplementedRTCServer 65 | // for forward compatibility 66 | type RTCServer interface { 67 | Signal(RTC_SignalServer) error 68 | mustEmbedUnimplementedRTCServer() 69 | } 70 | 71 | // UnimplementedRTCServer must be embedded to have forward compatible implementations. 72 | type UnimplementedRTCServer struct { 73 | } 74 | 75 | func (UnimplementedRTCServer) Signal(RTC_SignalServer) error { 76 | return status.Errorf(codes.Unimplemented, "method Signal not implemented") 77 | } 78 | func (UnimplementedRTCServer) mustEmbedUnimplementedRTCServer() {} 79 | 80 | // UnsafeRTCServer may be embedded to opt out of forward compatibility for this service. 81 | // Use of this interface is not recommended, as added methods to RTCServer will 82 | // result in compilation errors. 83 | type UnsafeRTCServer interface { 84 | mustEmbedUnimplementedRTCServer() 85 | } 86 | 87 | func RegisterRTCServer(s grpc.ServiceRegistrar, srv RTCServer) { 88 | s.RegisterService(&RTC_ServiceDesc, srv) 89 | } 90 | 91 | func _RTC_Signal_Handler(srv interface{}, stream grpc.ServerStream) error { 92 | return srv.(RTCServer).Signal(&rTCSignalServer{stream}) 93 | } 94 | 95 | type RTC_SignalServer interface { 96 | Send(*Reply) error 97 | Recv() (*Request, error) 98 | grpc.ServerStream 99 | } 100 | 101 | type rTCSignalServer struct { 102 | grpc.ServerStream 103 | } 104 | 105 | func (x *rTCSignalServer) Send(m *Reply) error { 106 | return x.ServerStream.SendMsg(m) 107 | } 108 | 109 | func (x *rTCSignalServer) Recv() (*Request, error) { 110 | m := new(Request) 111 | if err := x.ServerStream.RecvMsg(m); err != nil { 112 | return nil, err 113 | } 114 | return m, nil 115 | } 116 | 117 | // RTC_ServiceDesc is the grpc.ServiceDesc for RTC service. 118 | // It's only intended for direct use with grpc.RegisterService, 119 | // and not to be introspected or modified (even as a copy) 120 | var RTC_ServiceDesc = grpc.ServiceDesc{ 121 | ServiceName: "rtc.RTC", 122 | HandlerType: (*RTCServer)(nil), 123 | Methods: []grpc.MethodDesc{}, 124 | Streams: []grpc.StreamDesc{ 125 | { 126 | StreamName: "Signal", 127 | Handler: _RTC_Signal_Handler, 128 | ServerStreams: true, 129 | ClientStreams: true, 130 | }, 131 | }, 132 | Metadata: "proto/rtc/rtc.proto", 133 | } 134 | -------------------------------------------------------------------------------- /examples/webserver/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/json" 5 | "flag" 6 | "fmt" 7 | "log" 8 | "net/http" 9 | 10 | "github.com/golang-jwt/jwt/v4" 11 | ) 12 | 13 | // key under [signal.auth_connection] of configs/biz.toml 14 | const key = "1q2dGu5pzikcrECJgW3ADfXX3EsmoD99SYvSVCpDsJrAqxou5tUNbHPvkEFI4bTS" 15 | 16 | type result struct { 17 | Token string `json:"token"` 18 | } 19 | 20 | type claims struct { 21 | UID string `json:"uid"` 22 | SID string `json:"sid"` 23 | Services []string `json:"services"` 24 | jwt.StandardClaims 25 | } 26 | 27 | func main() { 28 | addr := flag.String("addr", ":8080", "server listen address") 29 | dir := flag.String("dir", "", "Static file directory") 30 | certFile := flag.String("cert", "", "Certificate file") 31 | keyFile := flag.String("key", "", "Private key file") 32 | flag.Parse() 33 | 34 | if len(*addr) <= 0 { 35 | log.Fatal("No listen address") 36 | } 37 | 38 | // http://localhost:8080/generate?uid=tony&sid=room1 39 | http.HandleFunc("/generate", sign) 40 | 41 | // http://localhost:8080/validate?token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1aWQiOiJ0b255IiwicmlkIjoicm9vbTEifQ.mopgibW3OYONYwzlo-YvkDIkNoYJc3OBQRsqQHZMnD8 42 | http.HandleFunc("/validate", validate) 43 | 44 | if len(*dir) > 0 { 45 | http.Handle("/", http.FileServer(http.Dir(*dir))) 46 | } 47 | 48 | var url string 49 | useTLS := len(*certFile) > 0 && len(*keyFile) > 0 50 | if useTLS { 51 | url = "https://" 52 | } else { 53 | url = "http://" 54 | } 55 | if (*addr)[0] == ':' { 56 | url = url + "localhost" + *addr 57 | } else { 58 | url = url + *addr 59 | } 60 | fmt.Printf("web server: %s\n", url) 61 | 62 | var err error 63 | if len(*certFile) > 0 && len(*keyFile) > 0 { 64 | err = http.ListenAndServeTLS(*addr, *certFile, *keyFile, nil) 65 | } else { 66 | err = http.ListenAndServe(*addr, nil) 67 | } 68 | if err != nil { 69 | log.Fatal(err) 70 | } 71 | } 72 | 73 | // building and signing a token 74 | func sign(w http.ResponseWriter, r *http.Request) { 75 | values := r.URL.Query()["uid"] 76 | if values == nil || len(values) < 1 { 77 | log.Printf("invalid uid, %v", values) 78 | http.Error(w, "invalid uid", http.StatusForbidden) 79 | return 80 | } 81 | uid := values[0] 82 | 83 | values = r.URL.Query()["sid"] 84 | if values == nil || len(values) < 1 { 85 | log.Printf("invalid sid, %v", values) 86 | http.Error(w, "invalid sid", http.StatusForbidden) 87 | return 88 | } 89 | sid := values[0] 90 | 91 | token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims{ 92 | UID: uid, 93 | SID: sid, 94 | Services: []string{"sfu", "biz"}, 95 | }) 96 | tokenString, err := token.SignedString([]byte(key)) 97 | if err != nil { 98 | log.Printf("sign error: %v", err) 99 | http.Error(w, "sign error", http.StatusInternalServerError) 100 | } 101 | 102 | data, err := json.Marshal(result{ 103 | Token: tokenString, 104 | }) 105 | if err != nil { 106 | log.Printf("json marshal error: %v", err) 107 | http.Error(w, "json marshal error", http.StatusInternalServerError) 108 | } 109 | log.Printf(string(data)) 110 | w.Header().Set("Content-Type", "application/json") 111 | w.Write(data) 112 | } 113 | 114 | // parsing and validating a token 115 | func validate(w http.ResponseWriter, r *http.Request) { 116 | values := r.URL.Query()["token"] 117 | if values == nil || len(values) < 1 { 118 | log.Printf("invalid token, %v", values) 119 | http.Error(w, "invalid token", http.StatusForbidden) 120 | return 121 | } 122 | tokenString := values[0] 123 | 124 | token, err := jwt.ParseWithClaims(tokenString, &claims{}, func(t *jwt.Token) (interface{}, error) { 125 | return []byte(key), nil 126 | }) 127 | if err != nil { 128 | log.Printf("parse token error: %v", err) 129 | http.Error(w, "parse token error", http.StatusForbidden) 130 | return 131 | } 132 | c := token.Claims.(*claims) 133 | 134 | data, err := json.Marshal(c) 135 | if err != nil { 136 | log.Printf("json marshal error: %v", err) 137 | http.Error(w, "json marshal error", http.StatusInternalServerError) 138 | } 139 | log.Printf(string(data)) 140 | w.Header().Set("Content-Type", "application/json") 141 | w.Write(data) 142 | } 143 | -------------------------------------------------------------------------------- /pkg/ion/node.go: -------------------------------------------------------------------------------- 1 | package ion 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "sync" 7 | 8 | ndc "github.com/cloudwebrtc/nats-discovery/pkg/client" 9 | "github.com/cloudwebrtc/nats-discovery/pkg/discovery" 10 | nrpc "github.com/cloudwebrtc/nats-grpc/pkg/rpc" 11 | "github.com/nats-io/nats.go" 12 | log "github.com/pion/ion-log" 13 | "github.com/pion/ion/pkg/util" 14 | "google.golang.org/grpc" 15 | ) 16 | 17 | //Node . 18 | type Node struct { 19 | // Node ID 20 | NID string 21 | // Nats Client Conn 22 | nc *nats.Conn 23 | // gRPC Service Registrar 24 | nrpc *nrpc.Server 25 | // Service discovery client 26 | ndc *ndc.Client 27 | 28 | nodeLock sync.RWMutex 29 | //neighbor nodes 30 | neighborNodes map[string]discovery.Node 31 | 32 | cliLock sync.RWMutex 33 | clis map[string]*nrpc.Client 34 | } 35 | 36 | //NewNode . 37 | func NewNode(nid string) Node { 38 | return Node{ 39 | NID: nid, 40 | neighborNodes: make(map[string]discovery.Node), 41 | clis: make(map[string]*nrpc.Client), 42 | } 43 | } 44 | 45 | //Start . 46 | func (n *Node) Start(natURL string) error { 47 | var err error 48 | n.nc, err = util.NewNatsConn(natURL) 49 | if err != nil { 50 | log.Errorf("new nats conn error %v", err) 51 | n.Close() 52 | return err 53 | } 54 | n.ndc, err = ndc.NewClient(n.nc) 55 | if err != nil { 56 | log.Errorf("new discovery client error %v", err) 57 | n.Close() 58 | return err 59 | } 60 | n.nrpc = nrpc.NewServer(n.nc, n.NID) 61 | return nil 62 | } 63 | 64 | //NatsConn . 65 | func (n *Node) NatsConn() *nats.Conn { 66 | return n.nc 67 | } 68 | 69 | //KeepAlive Upload your node info to registry. 70 | func (n *Node) KeepAlive(node discovery.Node) error { 71 | return n.ndc.KeepAlive(node) 72 | } 73 | 74 | func (n *Node) NewNatsRPCClient(service, peerNID string, parameters map[string]interface{}) (*nrpc.Client, error) { 75 | var cli *nrpc.Client = nil 76 | selfNID := n.NID 77 | for id, node := range n.neighborNodes { 78 | if node.Service == service && (id == peerNID || peerNID == "*") { 79 | cli = nrpc.NewClient(n.nc, id, selfNID) 80 | } 81 | } 82 | 83 | if cli == nil { 84 | resp, err := n.ndc.Get(service, parameters) 85 | if err != nil { 86 | log.Errorf("failed to Get service [%v]: %v", service, err) 87 | return nil, err 88 | } 89 | 90 | if len(resp.Nodes) == 0 { 91 | err := fmt.Errorf("get service [%v], node cnt == 0", service) 92 | return nil, err 93 | } 94 | 95 | cli = nrpc.NewClient(n.nc, resp.Nodes[0].NID, selfNID) 96 | } 97 | 98 | n.cliLock.Lock() 99 | defer n.cliLock.Unlock() 100 | n.clis[util.RandomString(12)] = cli 101 | return cli, nil 102 | } 103 | 104 | //Watch the neighbor nodes 105 | func (n *Node) Watch(service string) error { 106 | resp, err := n.ndc.Get(service, map[string]interface{}{}) 107 | if err != nil { 108 | log.Errorf("Watch service %v error %v", service, err) 109 | return err 110 | } 111 | 112 | for _, node := range resp.Nodes { 113 | n.handleNeighborNodes(discovery.NodeUp, &node) 114 | } 115 | 116 | return n.ndc.Watch(context.Background(), service, n.handleNeighborNodes) 117 | } 118 | 119 | // GetNeighborNodes get neighbor nodes. 120 | func (n *Node) GetNeighborNodes() map[string]discovery.Node { 121 | n.nodeLock.Lock() 122 | defer n.nodeLock.Unlock() 123 | return n.neighborNodes 124 | } 125 | 126 | // handleNeighborNodes handle nodes up/down 127 | func (n *Node) handleNeighborNodes(state discovery.NodeState, node *discovery.Node) { 128 | id := node.NID 129 | service := node.Service 130 | if state == discovery.NodeUp { 131 | log.Infof("Service up: "+service+" node id => [%v], rpc => %v", id, node.RPC.Protocol) 132 | if _, found := n.neighborNodes[id]; !found { 133 | n.nodeLock.Lock() 134 | n.neighborNodes[id] = *node 135 | n.nodeLock.Unlock() 136 | } 137 | } else if state == discovery.NodeDown { 138 | log.Infof("Service down: "+service+" node id => [%v]", id) 139 | 140 | n.nodeLock.Lock() 141 | delete(n.neighborNodes, id) 142 | n.nodeLock.Unlock() 143 | 144 | err := n.nrpc.CloseStream(id) 145 | if err != nil { 146 | log.Errorf("nrpc.CloseStream: err %v", err) 147 | } 148 | 149 | n.cliLock.Lock() 150 | defer n.cliLock.Unlock() 151 | for cid, cli := range n.clis { 152 | if cli.CloseStream(id) { 153 | delete(n.clis, cid) 154 | } 155 | } 156 | } 157 | } 158 | 159 | //ServiceRegistrar return grpc.ServiceRegistrar of this node, used to create grpc services 160 | func (n *Node) ServiceRegistrar() grpc.ServiceRegistrar { 161 | return n.nrpc 162 | } 163 | 164 | //Close . 165 | func (n *Node) Close() { 166 | if n.nrpc != nil { 167 | n.nrpc.Stop() 168 | } 169 | if n.nc != nil { 170 | n.nc.Close() 171 | } 172 | if n.ndc != nil { 173 | n.ndc.Close() 174 | } 175 | } 176 | -------------------------------------------------------------------------------- /pkg/node/sfu/sfu.go: -------------------------------------------------------------------------------- 1 | package sfu 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | 7 | "github.com/cloudwebrtc/nats-discovery/pkg/discovery" 8 | nrpc "github.com/cloudwebrtc/nats-grpc/pkg/rpc" 9 | "github.com/cloudwebrtc/nats-grpc/pkg/rpc/reflection" 10 | log "github.com/pion/ion-log" 11 | isfu "github.com/pion/ion-sfu/pkg/sfu" 12 | "github.com/pion/ion/pkg/ion" 13 | "github.com/pion/ion/pkg/proto" 14 | "github.com/pion/ion/pkg/runner" 15 | "github.com/pion/ion/pkg/util" 16 | pb "github.com/pion/ion/proto/rtc" 17 | "github.com/spf13/viper" 18 | "google.golang.org/grpc" 19 | ) 20 | 21 | const ( 22 | portRangeLimit = 100 23 | ) 24 | 25 | type global struct { 26 | Dc string `mapstructure:"dc"` 27 | } 28 | 29 | type natsConf struct { 30 | URL string `mapstructure:"url"` 31 | } 32 | 33 | // Config defines parameters for the logger 34 | type logConf struct { 35 | Level string `mapstructure:"level"` 36 | } 37 | 38 | // Config for sfu node 39 | type Config struct { 40 | Global global `mapstructure:"global"` 41 | Log logConf `mapstructure:"log"` 42 | Nats natsConf `mapstructure:"nats"` 43 | isfu.Config 44 | } 45 | 46 | func unmarshal(rawVal interface{}) error { 47 | if err := viper.Unmarshal(rawVal); err != nil { 48 | return err 49 | } 50 | return nil 51 | } 52 | 53 | func (c *Config) Load(file string) error { 54 | _, err := os.Stat(file) 55 | if err != nil { 56 | return err 57 | } 58 | 59 | viper.SetConfigFile(file) 60 | viper.SetConfigType("toml") 61 | 62 | err = viper.ReadInConfig() 63 | if err != nil { 64 | log.Errorf("config file %s read failed. %v\n", file, err) 65 | return err 66 | } 67 | 68 | err = unmarshal(c) 69 | if err != nil { 70 | return err 71 | } 72 | err = unmarshal(&c.Config) 73 | if err != nil { 74 | return err 75 | } 76 | if err != nil { 77 | log.Errorf("config file %s loaded failed. %v\n", file, err) 78 | return err 79 | } 80 | 81 | if len(c.WebRTC.ICEPortRange) > 2 { 82 | err = fmt.Errorf("config file %s loaded failed. range port must be [min,max]", file) 83 | log.Errorf("err=%v", err) 84 | return err 85 | } 86 | 87 | if len(c.WebRTC.ICEPortRange) != 0 && c.WebRTC.ICEPortRange[1]-c.WebRTC.ICEPortRange[0] < portRangeLimit { 88 | err = fmt.Errorf("config file %s loaded failed. range port must be [min, max] and max - min >= %d", file, portRangeLimit) 89 | log.Errorf("err=%v", err) 90 | return err 91 | } 92 | 93 | log.Infof("config %s load ok!", file) 94 | return nil 95 | } 96 | 97 | // SFU represents a sfu node 98 | type SFU struct { 99 | ion.Node 100 | s *SFUService 101 | runner.Service 102 | conf Config 103 | } 104 | 105 | // New create a sfu node instance 106 | func New() *SFU { 107 | s := &SFU{ 108 | Node: ion.NewNode("sfu-" + util.RandomString(6)), 109 | } 110 | return s 111 | } 112 | 113 | func (s *SFU) ConfigBase() runner.ConfigBase { 114 | return &s.conf 115 | } 116 | 117 | // NewSFU create a sfu node instance 118 | func NewSFU() *SFU { 119 | s := &SFU{ 120 | Node: ion.NewNode("sfu-" + util.RandomString(6)), 121 | } 122 | return s 123 | } 124 | 125 | // Load load config file 126 | func (s *SFU) Load(confFile string) error { 127 | err := s.conf.Load(confFile) 128 | if err != nil { 129 | log.Errorf("config load error: %v", err) 130 | return err 131 | } 132 | return nil 133 | } 134 | 135 | // StartGRPC start with grpc.ServiceRegistrar 136 | func (s *SFU) StartGRPC(registrar grpc.ServiceRegistrar) error { 137 | s.s = NewSFUService(s.conf.Config) 138 | pb.RegisterRTCServer(registrar, s.s) 139 | log.Infof("sfu pb.RegisterRTCServer(registrar, s.s)") 140 | return nil 141 | } 142 | 143 | // Start sfu node 144 | func (s *SFU) Start(conf Config) error { 145 | err := s.Node.Start(conf.Nats.URL) 146 | if err != nil { 147 | s.Close() 148 | return err 149 | } 150 | 151 | s.s = NewSFUService(conf.Config) 152 | //grpc service 153 | pb.RegisterRTCServer(s.Node.ServiceRegistrar(), s.s) 154 | 155 | // Register reflection service on nats-rpc server. 156 | reflection.Register(s.Node.ServiceRegistrar().(*nrpc.Server)) 157 | 158 | node := discovery.Node{ 159 | DC: conf.Global.Dc, 160 | Service: proto.ServiceRTC, 161 | NID: s.Node.NID, 162 | RPC: discovery.RPC{ 163 | Protocol: discovery.NGRPC, 164 | Addr: conf.Nats.URL, 165 | //Params: map[string]string{"username": "foo", "password": "bar"}, 166 | }, 167 | } 168 | 169 | go func() { 170 | err := s.Node.KeepAlive(node) 171 | if err != nil { 172 | log.Errorf("sfu.Node.KeepAlive(%v) error %v", s.Node.NID, err) 173 | } 174 | }() 175 | 176 | //Watch ALL nodes. 177 | go func() { 178 | err := s.Node.Watch(proto.ServiceALL) 179 | if err != nil { 180 | log.Errorf("Node.Watch(proto.ServiceALL) error %v", err) 181 | } 182 | }() 183 | 184 | return nil 185 | } 186 | 187 | // Close all 188 | func (s *SFU) Close() { 189 | s.Node.Close() 190 | } 191 | -------------------------------------------------------------------------------- /scripts/common: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | #set -eux 3 | #adwpc 4 | 5 | CPU='' 6 | MEM='' 7 | OS_TYPE='' 8 | OS_VER='' 9 | CUR_DIR=$(cd `dirname $0`; pwd) 10 | FNAME=`basename $0` 11 | LOG="$CUR_DIR/$FNAME.log" 12 | ERR="$CUR_DIR/$FNAME.err" 13 | 14 | #check os 15 | if [ -f /etc/os-release ]; then 16 | # freedesktop.org and systemd 17 | . /etc/os-release 18 | CPU=`cat /proc/cpuinfo | grep "processor" | wc -l` 19 | MEM=`free -b|grep "Mem"|awk -F' ' '{print $2}'` 20 | OS_TYPE=$NAME 21 | unset NAME 22 | OS_VER=$VERSION_ID 23 | elif type lsb_release >/dev/null 2>&1; then 24 | # linuxbase.org 25 | CPU=`cat /proc/cpuinfo | grep "processor" | wc -l` 26 | MEM=`free -b|grep "Mem"|awk -F' ' '{print $2}'` 27 | OS_TYPE=$(lsb_release -si) 28 | OS_VER=$(lsb_release -sr) 29 | elif [ -f /etc/lsb-release ]; then 30 | # For some versions of Debian/Ubuntu without lsb_release command 31 | . /etc/lsb-release 32 | CPU=`cat /proc/cpuinfo | grep "processor" | wc -l` 33 | MEM=`free -b|grep "Mem"|awk -F' ' '{print $2}'` 34 | OS_TYPE=$DISTRIB_ID 35 | OS_VER=$DISTRIB_RELEASE 36 | elif [ -f /etc/debian_version ]; then 37 | # Older Debian/Ubuntu/etc. 38 | CPU=`cat /proc/cpuinfo | grep "processor" | wc -l` 39 | MEM=`free -b|grep "Mem"|awk -F' ' '{print $2}'` 40 | OS_TYPE=Debian 41 | OS_VER=$(cat /etc/debian_version) 42 | elif [ -f /etc/SuSe-release ]; then 43 | # Older SuSE/etc. 44 | CPU=`cat /proc/cpuinfo | grep "processor" | wc -l` 45 | MEM=`free -b|grep "Mem"|awk -F' ' '{print $2}'` 46 | ... 47 | elif [ -f /etc/redhat-release ]; then 48 | # Older Red Hat, CentOS, etc. 49 | CPU=`cat /proc/cpuinfo | grep "processor" | wc -l` 50 | MEM=`free -b|grep "Mem"|awk -F' ' '{print $2}'` 51 | ... 52 | else 53 | # Fall back to uname, e.g. "Linux ", also works for BSD, etc. 54 | OS_TYPE=$(uname -s) 55 | OS_VER=$(uname -r) 56 | CPU=`sysctl -n machdep.cpu.thread_count` 57 | MEM=`sysctl -n hw.memsize` 58 | fi 59 | 60 | 61 | #check if cmd exist 62 | function exist() { 63 | # $1 --help > /dev/null 64 | type "$1" > /dev/null 2>&1 65 | if [ "$?" -eq 0 ] ; then 66 | return 0 67 | else 68 | return 1 69 | fi 70 | } 71 | 72 | #echol LOGLEVEL ... 73 | function echol() 74 | { 75 | local mode="\033[0m" 76 | case "$1" in 77 | INFO) mode="\033[34;1m";;#bule 78 | USER) mode="\033[32;1m";;#green 79 | WARN) mode="\033[33;1m";;#yellow 80 | ERROR) mode="\033[31;1m";;#red 81 | *) mode="\033[35;1m";;#pink 82 | esac 83 | echo -e "$mode$@\033[0m" 84 | echo -e "$@" >> "$LOG" 85 | } 86 | 87 | 88 | #run cmd {params...} 89 | function run() 90 | { 91 | echol "$@" 92 | eval $@ 1>>"$LOG" 2>>"$ERR" 93 | local ret=$? 94 | if [[ $ret -ne 0 ]];then 95 | eval $@ 1>>"$LOG" 2>>"$ERR" 96 | if [[ $ret -eq 2 ]];then 97 | #e.g. make distclean fail return 2 98 | echol WARN "warning:$@, ret=$ret" 99 | else 100 | echol ERROR "failed:$@, ret=$ret" 101 | fi 102 | # exit -3 103 | fi 104 | } 105 | 106 | #mv to tmp 107 | function saferm() 108 | { 109 | # local name=`echo "$1" | awk -F'/' '{print $NF}' | awk -F'.' '{print $1}'` 110 | local name="${1%/}" 111 | name="${name##*/}" 112 | mv $1 "/tmp/$name`date +%Y%m%d%H%M%S`" > /dev/null 2>&1 113 | } 114 | 115 | 116 | #download url 117 | #dl url {rename} 118 | function wgetdl() 119 | { 120 | local file="${1##*/}" 121 | local rename="$2" 122 | echol "$FUNCNAME:$@" 123 | if [ ! -f "$file" ];then 124 | rm -fr "$file" 125 | if [ "$rename" = "" ];then 126 | run wget --no-verbose -c "$1" > /dev/null 127 | else 128 | run wget --no-verbose -c -O "$2" "$1" > /dev/null 129 | fi 130 | fi 131 | echol "success:$@" 132 | } 133 | 134 | 135 | #download repo to yum.repos.d 136 | function dlrepo() 137 | { 138 | cd /etc/yum.repos.d 139 | run sudo wget --no-verbose -c "$1" 140 | cd - 141 | } 142 | 143 | function rmrepo() { 144 | cd /etc/yum.repos.d 145 | run sudo rm "$1" 146 | cd - 147 | } 148 | 149 | #unzip file 150 | #uz file 151 | function uz() 152 | { 153 | echol "$@" 154 | local ftype=`file "$1"` # Note ' and ` is different 155 | case "$ftype" in 156 | "$1: Zip archive"*) 157 | run unzip "$1" > /dev/null;; 158 | "$1: gzip compressed"*) 159 | run tar zxvf "$1" > /dev/null;; 160 | "$1: bzip2 compressed"*) 161 | run tar jxvf "$1" > /dev/null;; 162 | "$1: xz compressed data"*) 163 | run tar xf "$1" > /dev/null;; 164 | "$1: 7-zip archive data"*) 165 | run 7za x "$1" > /dev/null;; 166 | *) 167 | echol ERROR "failed:File $1 can not be unzip" 168 | return;; 169 | esac 170 | echol "success:$@" 171 | } 172 | 173 | 174 | -------------------------------------------------------------------------------- /apps/room/proto/room.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | option go_package = "github.com/pion/ion/apps/room/proto"; 4 | 5 | package room; 6 | 7 | service RoomService { 8 | // Manager API 9 | // Room API 10 | rpc CreateRoom(CreateRoomRequest) returns (CreateRoomReply) {} 11 | rpc UpdateRoom(UpdateRoomRequest) returns (UpdateRoomReply) {} 12 | rpc EndRoom(EndRoomRequest) returns (EndRoomReply) {} 13 | rpc GetRooms(GetRoomsRequest) returns (GetRoomsReply) {} 14 | 15 | // Peer API 16 | rpc AddPeer(AddPeerRequest) returns (AddPeerReply) {} 17 | rpc UpdatePeer(UpdatePeerRequest) returns (UpdatePeerReply) {} 18 | rpc RemovePeer(RemovePeerRequest) returns (RemovePeerReply) {} 19 | rpc GetPeers(GetPeersRequest) returns (GetPeersReply) {} 20 | } 21 | 22 | service RoomSignal { 23 | // Signal 24 | rpc Signal(stream Request) returns (stream Reply) {} 25 | } 26 | 27 | enum ErrorType { 28 | None = 0; 29 | UnkownError = 1; 30 | PermissionDenied = 2; 31 | ServiceUnavailable = 3; 32 | RoomLocked = 4; 33 | PasswordRequired = 5; 34 | RoomAlreadyExist = 6; 35 | RoomNotExist = 7; 36 | InvalidParams = 8; 37 | PeerAlreadyExist = 9; 38 | PeerNotExist = 10; 39 | } 40 | 41 | message Error { 42 | ErrorType code = 1; 43 | string reason = 2; 44 | } 45 | 46 | message Request { 47 | oneof payload { 48 | JoinRequest join = 1; 49 | LeaveRequest leave = 2; 50 | SendMessageRequest sendMessage = 3; 51 | UpdateRoomRequest updateRoom = 4; 52 | } 53 | } 54 | 55 | message Reply { 56 | oneof payload { 57 | JoinReply join = 1; 58 | LeaveReply leave = 2; 59 | SendMessageReply sendMessage = 3; 60 | PeerEvent Peer = 4; 61 | Message message = 5; 62 | Disconnect disconnect = 6; 63 | Room room = 7; 64 | } 65 | } 66 | 67 | message CreateRoomRequest { 68 | Room room = 1; 69 | } 70 | 71 | message CreateRoomReply { 72 | bool success = 1; 73 | Error error = 2; 74 | } 75 | 76 | message DeleteRoomRequest { 77 | string sid = 1; 78 | } 79 | 80 | message DeleteRoomReply { 81 | bool success = 1; 82 | Error error = 2; 83 | } 84 | 85 | message JoinRequest { 86 | Peer peer = 1; 87 | string password = 2; 88 | } 89 | 90 | message Room { 91 | string sid = 1; 92 | string name = 2; 93 | bool lock = 3; 94 | string password = 4; 95 | string description = 5; 96 | uint32 maxPeers = 6; 97 | } 98 | 99 | message JoinReply { 100 | bool success = 1; 101 | Error error = 2; 102 | Role role = 3; 103 | Room room = 4; 104 | } 105 | 106 | message LeaveRequest { 107 | string sid = 1; 108 | string uid = 2; 109 | } 110 | 111 | message LeaveReply { 112 | bool success = 1; 113 | Error error = 2; 114 | } 115 | 116 | enum Role { 117 | Host = 0; 118 | Guest = 1; 119 | } 120 | 121 | enum Protocol { 122 | ProtocolUnknown = 0; 123 | WebRTC = 1; 124 | SIP = 2; 125 | RTMP = 3; 126 | RTSP = 4; 127 | } 128 | 129 | message Peer { 130 | enum Direction { 131 | INCOMING = 0; 132 | OUTGOING = 1; 133 | BILATERAL = 2; 134 | } 135 | string sid = 1; 136 | string uid = 2; 137 | string displayName = 3; 138 | bytes extraInfo = 4; 139 | string destination = 5; // rtsp/rtmp/sip url 140 | Role role = 6; 141 | Protocol protocol = 7; 142 | string avatar = 8; 143 | Direction direction = 9; 144 | string vendor = 10; 145 | } 146 | 147 | message AddPeerRequest { 148 | Peer peer = 1; 149 | } 150 | 151 | message AddPeerReply { 152 | bool success = 1; 153 | Error error = 2; 154 | } 155 | 156 | message GetPeersRequest { string sid = 1; } 157 | 158 | message GetPeersReply { 159 | bool success = 1; 160 | Error error = 2; 161 | repeated Peer Peers = 3; 162 | } 163 | 164 | message Message { 165 | string from = 1; // UUID of the sending Peer. 166 | string to = 2; // UUID of the receiving Peer. 167 | string type = 3; // MIME content-type of the message, usually text/plain. 168 | bytes payload = 4; // Payload message contents. 169 | } 170 | 171 | message SendMessageRequest { 172 | string sid = 1; 173 | Message message = 2; 174 | } 175 | 176 | message SendMessageReply { 177 | bool success = 1; 178 | Error error = 2; 179 | } 180 | 181 | message Disconnect { 182 | string sid = 1; 183 | string reason = 2; 184 | } 185 | 186 | enum PeerState { 187 | JOIN = 0; 188 | UPDATE = 1; 189 | LEAVE = 2; 190 | } 191 | 192 | message PeerEvent { 193 | Peer Peer = 1; 194 | PeerState state = 2; 195 | } 196 | 197 | message UpdateRoomRequest { 198 | Room room = 1; 199 | } 200 | 201 | message UpdateRoomReply { 202 | bool success = 1; 203 | Error error = 2; 204 | } 205 | 206 | message EndRoomRequest { 207 | string sid = 1; 208 | string reason = 2; 209 | bool delete = 3; 210 | } 211 | 212 | message EndRoomReply { 213 | bool success = 1; 214 | Error error = 2; 215 | } 216 | 217 | message GetRoomsRequest { 218 | 219 | } 220 | 221 | message GetRoomsReply { 222 | bool success = 1; 223 | Error error = 2; 224 | repeated Room rooms = 3; 225 | } 226 | 227 | message UpdatePeerRequest { 228 | Peer peer = 1; 229 | } 230 | 231 | message UpdatePeerReply { 232 | bool success = 1; 233 | Error error = 2; 234 | } 235 | 236 | message RemovePeerRequest { 237 | string sid = 1; 238 | string uid = 2; 239 | } 240 | 241 | message RemovePeerReply { 242 | bool success = 1; 243 | Error error = 2; 244 | } 245 | -------------------------------------------------------------------------------- /pkg/node/signal/signal.go: -------------------------------------------------------------------------------- 1 | package signal 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "strings" 7 | 8 | dc "github.com/cloudwebrtc/nats-discovery/pkg/client" 9 | "github.com/cloudwebrtc/nats-discovery/pkg/discovery" 10 | "github.com/nats-io/nats.go" 11 | log "github.com/pion/ion-log" 12 | "github.com/pion/ion/pkg/auth" 13 | "github.com/pion/ion/pkg/ion" 14 | "github.com/pion/ion/pkg/proto" 15 | "github.com/pion/ion/pkg/util" 16 | "google.golang.org/grpc" 17 | "google.golang.org/grpc/codes" 18 | "google.golang.org/grpc/metadata" 19 | "google.golang.org/grpc/status" 20 | ) 21 | 22 | type svcConf struct { 23 | Services []string `mapstructure:"services"` 24 | } 25 | 26 | type signalConf struct { 27 | GRPC grpcConf `mapstructure:"grpc"` 28 | JWT auth.AuthConfig `mapstructure:"jwt"` 29 | SVC svcConf `mapstructure:"svc"` 30 | } 31 | 32 | // signalConf represents signal server configuration 33 | type grpcConf struct { 34 | Host string `mapstructure:"host"` 35 | Port int `mapstructure:"port"` 36 | Cert string `mapstructure:"cert"` 37 | Key string `mapstructure:"key"` 38 | AllowAllOrigins bool `mapstructure:"allow_all_origins"` 39 | } 40 | 41 | type global struct { 42 | Dc string `mapstructure:"dc"` 43 | } 44 | 45 | type logConf struct { 46 | Level string `mapstructure:"level"` 47 | } 48 | 49 | type natsConf struct { 50 | URL string `mapstructure:"url"` 51 | } 52 | 53 | type avpConf struct { 54 | Elements []string `mapstructure:"elements"` 55 | } 56 | 57 | // Config for biz node 58 | type Config struct { 59 | Global global `mapstructure:"global"` 60 | Log logConf `mapstructure:"log"` 61 | Nats natsConf `mapstructure:"nats"` 62 | Avp avpConf `mapstructure:"avp"` 63 | Signal signalConf `mapstructure:"signal"` 64 | } 65 | 66 | type Signal struct { 67 | ion.Node 68 | conf Config 69 | nc *nats.Conn 70 | ndc *dc.Client 71 | } 72 | 73 | func NewSignal(conf Config) (*Signal, error) { 74 | nc, err := util.NewNatsConn(conf.Nats.URL) 75 | if err != nil { 76 | log.Errorf("new nats conn error %v", err) 77 | nc.Close() 78 | return nil, err 79 | } 80 | ndc, err := dc.NewClient(nc) 81 | if err != nil { 82 | log.Errorf("failed to create discovery client: %v", err) 83 | ndc.Close() 84 | return nil, err 85 | } 86 | return &Signal{ 87 | conf: conf, 88 | nc: nc, 89 | ndc: ndc, 90 | Node: ion.NewNode("signal-" + util.RandomString(6)), 91 | }, nil 92 | } 93 | 94 | func (s *Signal) Start() error { 95 | log.Infof("s.Node.Start node=%+v", s.conf.Nats.URL) 96 | err := s.Node.Start(s.conf.Nats.URL) 97 | if err != nil { 98 | log.Errorf("s.Node.Start error err=%+v", err) 99 | s.Close() 100 | return err 101 | } 102 | node := discovery.Node{ 103 | DC: s.conf.Global.Dc, 104 | Service: proto.ServiceSIG, 105 | NID: s.Node.NID, 106 | RPC: discovery.RPC{ 107 | Protocol: discovery.NGRPC, 108 | Addr: s.conf.Nats.URL, 109 | //Params: map[string]string{"username": "foo", "password": "bar"}, 110 | }, 111 | } 112 | 113 | go func() { 114 | log.Infof("KeepAlive node=%+v", node) 115 | err := s.Node.KeepAlive(node) 116 | if err != nil { 117 | log.Errorf("sig.Node.KeepAlive(%v) error %v", s.Node.NID, err) 118 | } 119 | }() 120 | 121 | //Watch ALL nodes. 122 | go func() { 123 | err := s.Node.Watch(proto.ServiceALL) 124 | if err != nil { 125 | log.Errorf("Node.Watch(proto.ServiceALL) error %v", err) 126 | } 127 | }() 128 | 129 | return nil 130 | } 131 | 132 | func (s *Signal) Director(ctx context.Context, fullMethodName string) (context.Context, grpc.ClientConnInterface, error) { 133 | md, ok := metadata.FromIncomingContext(ctx) 134 | if ok { 135 | log.Infof("fullMethodName: %v, md %v", fullMethodName, md) 136 | } 137 | 138 | //Authenticate here. 139 | authConfig := &s.conf.Signal.JWT 140 | if authConfig.Enabled { 141 | claims, err := auth.GetClaim(ctx, authConfig) 142 | if err != nil { 143 | return ctx, nil, status.Errorf(codes.Unauthenticated, fmt.Sprintf("Failed to Get Claims JWT : %v", err)) 144 | } 145 | 146 | log.Infof("claims: UID: %s, SID: %v, Services: %v", claims.UID, claims.SID, claims.Services) 147 | 148 | allowed := false 149 | for _, svc := range claims.Services { 150 | if strings.Contains(fullMethodName, "/"+svc+".") { 151 | allowed = true 152 | break 153 | } 154 | } 155 | 156 | if !allowed { 157 | return ctx, nil, status.Errorf(codes.Unauthenticated, fmt.Sprintf("Service %v access denied!", fullMethodName)) 158 | } 159 | } 160 | 161 | //Find service in neighbor nodes. 162 | svcConf := s.conf.Signal.SVC 163 | for _, svc := range svcConf.Services { 164 | if strings.HasPrefix(fullMethodName, "/"+svc+".") { 165 | //Using grpc.Metadata as a parameters for ndc.Get. 166 | var parameters = make(map[string]interface{}) 167 | for key, value := range md { 168 | parameters[key] = value[0] 169 | } 170 | cli, err := s.NewNatsRPCClient(svc, "*", parameters) 171 | if err != nil { 172 | log.Errorf("failed to Get service [%v]: %v", svc, err) 173 | return ctx, nil, status.Errorf(codes.Unavailable, "Service Unavailable: %v", err) 174 | } 175 | return ctx, cli, nil 176 | } 177 | } 178 | 179 | return ctx, nil, status.Errorf(codes.Unimplemented, "Unknown Service.Method %v", fullMethodName) 180 | } 181 | 182 | func (s *Signal) Close() { 183 | s.nc.Close() 184 | s.ndc.Close() 185 | } 186 | -------------------------------------------------------------------------------- /pkg/util/wrapped.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "crypto/tls" 5 | "net" 6 | "net/http" 7 | "time" 8 | 9 | "github.com/improbable-eng/grpc-web/go/grpcweb" 10 | log "github.com/pion/ion-log" 11 | "github.com/soheilhy/cmux" 12 | "golang.org/x/sync/errgroup" 13 | "google.golang.org/grpc" 14 | ) 15 | 16 | type WrapperedServerOptions struct { 17 | Addr string 18 | Cert string 19 | Key string 20 | AllowAllOrigins bool 21 | AllowedOrigins *[]string 22 | AllowedHeaders *[]string 23 | UseWebSocket bool 24 | WebsocketPingInterval time.Duration 25 | } 26 | 27 | func DefaultWrapperedServerOptions() WrapperedServerOptions { 28 | return WrapperedServerOptions{ 29 | Addr: ":9090", 30 | Cert: "", 31 | Key: "", 32 | AllowAllOrigins: true, 33 | AllowedHeaders: &[]string{}, 34 | AllowedOrigins: &[]string{}, 35 | UseWebSocket: true, 36 | WebsocketPingInterval: 0, 37 | } 38 | } 39 | 40 | func NewWrapperedServerOptions(addr, cert, key string, websocket bool) WrapperedServerOptions { 41 | return WrapperedServerOptions{ 42 | Addr: addr, 43 | Cert: cert, 44 | Key: key, 45 | AllowAllOrigins: true, 46 | AllowedHeaders: &[]string{}, 47 | AllowedOrigins: &[]string{}, 48 | UseWebSocket: true, 49 | WebsocketPingInterval: 0, 50 | } 51 | } 52 | 53 | type WrapperedGRPCWebServer struct { 54 | options WrapperedServerOptions 55 | GRPCServer *grpc.Server 56 | } 57 | 58 | func NewWrapperedGRPCWebServer(options WrapperedServerOptions, s *grpc.Server) *WrapperedGRPCWebServer { 59 | return &WrapperedGRPCWebServer{ 60 | options: options, 61 | GRPCServer: s, 62 | } 63 | } 64 | 65 | type allowedOrigins struct { 66 | origins map[string]struct{} 67 | } 68 | 69 | func (a *allowedOrigins) IsAllowed(origin string) bool { 70 | _, ok := a.origins[origin] 71 | return ok 72 | } 73 | 74 | func makeAllowedOrigins(origins []string) *allowedOrigins { 75 | o := map[string]struct{}{} 76 | for _, allowedOrigin := range origins { 77 | o[allowedOrigin] = struct{}{} 78 | } 79 | return &allowedOrigins{ 80 | origins: o, 81 | } 82 | } 83 | 84 | func (s *WrapperedGRPCWebServer) makeHTTPOriginFunc(allowedOrigins *allowedOrigins) func(origin string) bool { 85 | if s.options.AllowAllOrigins { 86 | return func(origin string) bool { 87 | return true 88 | } 89 | } 90 | return allowedOrigins.IsAllowed 91 | } 92 | 93 | func (s *WrapperedGRPCWebServer) makeWebsocketOriginFunc(allowedOrigins *allowedOrigins) func(req *http.Request) bool { 94 | if s.options.AllowAllOrigins { 95 | return func(req *http.Request) bool { 96 | return true 97 | } 98 | } 99 | return func(req *http.Request) bool { 100 | origin, err := grpcweb.WebsocketRequestOrigin(req) 101 | if err != nil { 102 | log.Warnf("%v", err) 103 | return false 104 | } 105 | return allowedOrigins.IsAllowed(origin) 106 | } 107 | } 108 | 109 | func (s *WrapperedGRPCWebServer) Serve() error { 110 | addr := s.options.Addr 111 | 112 | if s.options.AllowAllOrigins && s.options.AllowedOrigins != nil && len(*s.options.AllowedOrigins) != 0 { 113 | 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.") 114 | } 115 | 116 | allowedOrigins := makeAllowedOrigins(*s.options.AllowedOrigins) 117 | 118 | options := []grpcweb.Option{ 119 | grpcweb.WithCorsForRegisteredEndpointsOnly(false), 120 | grpcweb.WithOriginFunc(s.makeHTTPOriginFunc(allowedOrigins)), 121 | } 122 | 123 | if s.options.UseWebSocket { 124 | log.Infof("Using websockets") 125 | options = append( 126 | options, 127 | grpcweb.WithWebsockets(true), 128 | grpcweb.WithWebsocketOriginFunc(s.makeWebsocketOriginFunc(allowedOrigins)), 129 | ) 130 | 131 | if s.options.WebsocketPingInterval >= time.Second { 132 | log.Infof("websocket keepalive pinging enabled, the timeout interval is %s", s.options.WebsocketPingInterval.String()) 133 | options = append( 134 | options, 135 | grpcweb.WithWebsocketPingInterval(s.options.WebsocketPingInterval), 136 | ) 137 | } 138 | } 139 | 140 | if s.options.AllowedHeaders != nil && len(*s.options.AllowedHeaders) > 0 { 141 | options = append( 142 | options, 143 | grpcweb.WithAllowedRequestHeaders(*s.options.AllowedHeaders), 144 | ) 145 | } 146 | 147 | wrappedServer := grpcweb.WrapServer(s.GRPCServer, options...) 148 | handler := func(resp http.ResponseWriter, req *http.Request) { 149 | wrappedServer.ServeHTTP(resp, req) 150 | } 151 | 152 | httpServer := http.Server{ 153 | Addr: addr, 154 | Handler: http.HandlerFunc(handler), 155 | } 156 | 157 | var listener net.Listener 158 | 159 | enableTLS := s.options.Cert != "" && s.options.Key != "" 160 | 161 | if enableTLS { 162 | cer, err := tls.LoadX509KeyPair(s.options.Cert, s.options.Key) 163 | if err != nil { 164 | log.Panicf("failed to load x509 key pair: %v", err) 165 | return err 166 | } 167 | config := &tls.Config{Certificates: []tls.Certificate{cer}} 168 | tls, err := tls.Listen("tcp", addr, config) 169 | if err != nil { 170 | log.Panicf("failed to listen: tls %v", err) 171 | return err 172 | } 173 | listener = tls 174 | } else { 175 | tcp, err := net.Listen("tcp", addr) 176 | if err != nil { 177 | log.Panicf("failed to listen: tcp %v", err) 178 | return err 179 | } 180 | listener = tcp 181 | } 182 | 183 | log.Infof("Starting gRPC/gRPC-Web combo server, bind: %s, with TLS: %v", addr, enableTLS) 184 | 185 | m := cmux.New(listener) 186 | grpcListener := m.Match(cmux.HTTP2()) 187 | httpListener := m.Match(cmux.HTTP1Fast()) 188 | g := new(errgroup.Group) 189 | g.Go(func() error { return s.GRPCServer.Serve(grpcListener) }) 190 | g.Go(func() error { return httpServer.Serve(httpListener) }) 191 | g.Go(m.Serve) 192 | log.Infof("Run server: %v", g.Wait()) 193 | return nil 194 | } 195 | -------------------------------------------------------------------------------- /examples/sfu-v2/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 | 40 |
41 |
42 | 43 |
44 |
45 | Local 46 | 47 | 75 |
76 |
77 | Remotes 78 |
79 |
80 |
81 | 82 | 83 | 84 | 167 | 168 | 169 | -------------------------------------------------------------------------------- /pkg/db/redis.go: -------------------------------------------------------------------------------- 1 | package db 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "time" 7 | 8 | log "github.com/pion/ion-log" 9 | 10 | "github.com/go-redis/redis/v7" 11 | ) 12 | 13 | var ( 14 | lockExpire = 3 * time.Second 15 | ) 16 | 17 | type Config struct { 18 | Addrs []string `mapstructure:"addrs"` 19 | Pwd string `mapstructure:"password"` 20 | DB int `mapstructure:"db"` 21 | } 22 | 23 | type Redis struct { 24 | cluster *redis.ClusterClient 25 | single *redis.Client 26 | clusterMode bool 27 | } 28 | 29 | func NewRedis(c Config) *Redis { 30 | if len(c.Addrs) == 0 { 31 | log.Errorf("invalid addrs: %v", c.Addrs) 32 | return nil 33 | } 34 | 35 | r := &Redis{} 36 | if len(c.Addrs) == 1 { 37 | r.single = redis.NewClient( 38 | &redis.Options{ 39 | Addr: c.Addrs[0], // use default Addr 40 | Password: c.Pwd, // no password set 41 | DB: c.DB, // use default DB 42 | DialTimeout: 3 * time.Second, 43 | ReadTimeout: 5 * time.Second, 44 | WriteTimeout: 5 * time.Second, 45 | }) 46 | if err := r.single.Ping().Err(); err != nil { 47 | log.Errorf(err.Error()) 48 | return nil 49 | } 50 | r.single.Do("CONFIG", "SET", "notify-keyspace-events", "AKE") 51 | r.clusterMode = false 52 | log.Infof("redis new client single mode: %v", r) 53 | return r 54 | } 55 | 56 | r.cluster = redis.NewClusterClient( 57 | &redis.ClusterOptions{ 58 | Addrs: c.Addrs, 59 | Password: c.Pwd, 60 | DialTimeout: 5 * time.Second, 61 | ReadTimeout: 10 * time.Second, 62 | WriteTimeout: 10 * time.Second, 63 | }) 64 | if err := r.cluster.Ping().Err(); err != nil { 65 | log.Errorf(err.Error()) 66 | } 67 | r.cluster.Do("CONFIG", "SET", "notify-keyspace-events", "AKE") 68 | r.clusterMode = true 69 | log.Infof("redis new client cluster mode: %v", r) 70 | return r 71 | } 72 | 73 | func (r *Redis) Close() { 74 | if r.single != nil { 75 | r.single.Close() 76 | } 77 | if r.cluster != nil { 78 | r.cluster.Close() 79 | } 80 | } 81 | 82 | func (r *Redis) Set(key string, value interface{}, t time.Duration) error { 83 | r.Acquire(key) 84 | defer r.Release(key) 85 | if r.clusterMode { 86 | return r.cluster.Set(key, value, t).Err() 87 | } 88 | return r.single.Set(key, value, t).Err() 89 | } 90 | 91 | func (r *Redis) Get(k string) string { 92 | r.Acquire(k) 93 | defer r.Release(k) 94 | if r.clusterMode { 95 | return r.cluster.Get(k).Val() 96 | } 97 | return r.single.Get(k).Val() 98 | } 99 | 100 | func (r *Redis) HSet(key, field string, value interface{}) error { 101 | r.Acquire(key) 102 | defer r.Release(key) 103 | if r.clusterMode { 104 | return r.cluster.HSet(key, field, value).Err() 105 | } 106 | return r.single.HSet(key, field, value).Err() 107 | } 108 | 109 | func (r *Redis) HGet(key, field string) string { 110 | r.Acquire(key) 111 | defer r.Release(key) 112 | if r.clusterMode { 113 | return r.cluster.HGet(key, field).Val() 114 | } 115 | return r.single.HGet(key, field).Val() 116 | } 117 | 118 | func (r *Redis) HMSet(key string, values ...string) error { 119 | r.Acquire(key) 120 | defer r.Release(key) 121 | if r.clusterMode { 122 | return r.cluster.HMSet(key, values).Err() 123 | } 124 | return r.single.HMSet(key, values).Err() 125 | } 126 | 127 | func (r *Redis) HMGet(key string, fields ...string) []interface{} { 128 | r.Acquire(key) 129 | defer r.Release(key) 130 | if r.clusterMode { 131 | return r.cluster.HMGet(key, fields...).Val() 132 | } 133 | return r.single.HMGet(key, fields...).Val() 134 | } 135 | 136 | func (r *Redis) HGetAll(key string) map[string]string { 137 | r.Acquire(key) 138 | defer r.Release(key) 139 | if r.clusterMode { 140 | return r.cluster.HGetAll(key).Val() 141 | } 142 | return r.single.HGetAll(key).Val() 143 | } 144 | 145 | func (r *Redis) HDel(key, field string) error { 146 | r.Acquire(key) 147 | defer r.Release(key) 148 | if r.clusterMode { 149 | return r.cluster.HDel(key, field).Err() 150 | } 151 | return r.single.HDel(key, field).Err() 152 | } 153 | 154 | func (r *Redis) Expire(key string, t time.Duration) error { 155 | r.Acquire(key) 156 | defer r.Release(key) 157 | if r.clusterMode { 158 | return r.cluster.Expire(key, t).Err() 159 | } 160 | 161 | return r.single.Expire(key, t).Err() 162 | } 163 | 164 | func (r *Redis) HSetTTL(t time.Duration, key, field string, value interface{}) error { 165 | r.Acquire(key) 166 | defer r.Release(key) 167 | if r.clusterMode { 168 | if err := r.cluster.HSet(key, field, value).Err(); err != nil { 169 | return err 170 | } 171 | return r.cluster.Expire(key, t).Err() 172 | } 173 | if err := r.single.HSet(key, field, value).Err(); err != nil { 174 | return err 175 | } 176 | return r.single.Expire(key, t).Err() 177 | } 178 | 179 | func (r *Redis) HMSetTTL(t time.Duration, k string, values ...interface{}) error { 180 | r.Acquire(k) 181 | defer r.Release(k) 182 | if r.clusterMode { 183 | if err := r.cluster.HMSet(k, values...).Err(); err != nil { 184 | return err 185 | } 186 | return r.cluster.Expire(k, t).Err() 187 | } 188 | 189 | if err := r.single.HMSet(k, values...).Err(); err != nil { 190 | return err 191 | } 192 | return r.single.Expire(k, t).Err() 193 | } 194 | 195 | func (r *Redis) Keys(key string) []string { 196 | r.Acquire(key) 197 | defer r.Release(key) 198 | if r.clusterMode { 199 | return r.cluster.Keys(key).Val() 200 | } 201 | return r.single.Keys(key).Val() 202 | } 203 | 204 | func (r *Redis) Del(k string) error { 205 | r.Acquire(k) 206 | defer r.Release(k) 207 | if r.clusterMode { 208 | return r.cluster.Del(k).Err() 209 | } 210 | return r.single.Del(k).Err() 211 | } 212 | 213 | // Watch http://redisdoc.com/topic/notification.html 214 | func (r *Redis) Watch(ctx context.Context, key string) <-chan interface{} { 215 | r.Acquire(key) 216 | defer r.Release(key) 217 | var pubsub *redis.PubSub 218 | if r.clusterMode { 219 | pubsub = r.cluster.PSubscribe(fmt.Sprintf("__key*__:%s", key)) 220 | } else { 221 | pubsub = r.single.PSubscribe(fmt.Sprintf("__key*__:%s", key)) 222 | } 223 | 224 | res := make(chan interface{}) 225 | go func() { 226 | for { 227 | select { 228 | case msg := <-pubsub.Channel(): 229 | op := msg.Payload 230 | log.Infof("key => %s, op => %s", key, op) 231 | res <- op 232 | case <-ctx.Done(): 233 | pubsub.Close() 234 | close(res) 235 | return 236 | } 237 | } 238 | }() 239 | 240 | return res 241 | } 242 | 243 | func (r *Redis) lock(key string) bool { 244 | // Tips: use ("lock-"+key) as lock key is better than (key+"-lock") 245 | // this avoid "keys /xxxxx/*" to get this lock 246 | if r.clusterMode { 247 | ok, _ := r.cluster.SetNX("lock-"+key, 1, lockExpire).Result() 248 | return ok 249 | } 250 | ok, _ := r.single.SetNX("lock-"+key, 1, lockExpire).Result() 251 | return ok 252 | } 253 | 254 | func (r *Redis) unlock(key string) { 255 | if r.clusterMode { 256 | _, _ = r.cluster.Del("lock-" + key).Result() 257 | } 258 | _, _ = r.single.Del("lock-" + key).Result() 259 | } 260 | 261 | // Acquire a destributed lock 262 | func (r *Redis) Acquire(key string) { 263 | // retry if lock failed 264 | for !r.lock(key) { 265 | time.Sleep(time.Millisecond * 100) 266 | } 267 | } 268 | 269 | // Release a destributed lock 270 | func (r *Redis) Release(key string) { 271 | r.unlock(key) 272 | } 273 | -------------------------------------------------------------------------------- /examples/ion-pubsub/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 11 | 16 | 17 | 20 | 23 | 26 | 27 | Pion ion-cluster | Pub Sub test 28 | 29 | 30 | 31 | 34 |
35 |
36 |
37 |
38 | 41 | 44 | 47 | 50 |
51 |
52 |
53 |
54 |
55 | 59 | 67 |
68 |
69 | 70 |
71 |
72 | Local 73 | 74 | 102 |
103 |
104 | Remotes 105 |
106 | 111 |
112 | 117 | 122 |
123 | 124 |
125 | 126 |
127 |
Event
128 |
129 |
130 |
131 |

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

178 |         
179 |
180 | 181 |
182 | 183 | 184 | 185 | 186 | 187 | 188 | -------------------------------------------------------------------------------- /proto/islb/islb.pb.go: -------------------------------------------------------------------------------- 1 | // Code generated by protoc-gen-go. DO NOT EDIT. 2 | // versions: 3 | // protoc-gen-go v1.25.0 4 | // protoc v3.12.4 5 | // source: proto/islb/islb.proto 6 | 7 | package islb 8 | 9 | import ( 10 | proto "github.com/golang/protobuf/proto" 11 | ion "github.com/pion/ion/proto/ion" 12 | protoreflect "google.golang.org/protobuf/reflect/protoreflect" 13 | protoimpl "google.golang.org/protobuf/runtime/protoimpl" 14 | reflect "reflect" 15 | sync "sync" 16 | ) 17 | 18 | const ( 19 | // Verify that this generated code is sufficiently up-to-date. 20 | _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) 21 | // Verify that runtime/protoimpl is sufficiently up-to-date. 22 | _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) 23 | ) 24 | 25 | // This is a compile-time assertion that a sufficiently up-to-date version 26 | // of the legacy proto package is being used. 27 | const _ = proto.ProtoPackageIsVersion4 28 | 29 | type FindNodeRequest struct { 30 | state protoimpl.MessageState 31 | sizeCache protoimpl.SizeCache 32 | unknownFields protoimpl.UnknownFields 33 | 34 | Sid string `protobuf:"bytes,1,opt,name=sid,proto3" json:"sid,omitempty"` 35 | Nid string `protobuf:"bytes,2,opt,name=nid,proto3" json:"nid,omitempty"` 36 | Service string `protobuf:"bytes,3,opt,name=service,proto3" json:"service,omitempty"` 37 | } 38 | 39 | func (x *FindNodeRequest) Reset() { 40 | *x = FindNodeRequest{} 41 | if protoimpl.UnsafeEnabled { 42 | mi := &file_proto_islb_islb_proto_msgTypes[0] 43 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 44 | ms.StoreMessageInfo(mi) 45 | } 46 | } 47 | 48 | func (x *FindNodeRequest) String() string { 49 | return protoimpl.X.MessageStringOf(x) 50 | } 51 | 52 | func (*FindNodeRequest) ProtoMessage() {} 53 | 54 | func (x *FindNodeRequest) ProtoReflect() protoreflect.Message { 55 | mi := &file_proto_islb_islb_proto_msgTypes[0] 56 | if protoimpl.UnsafeEnabled && x != nil { 57 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 58 | if ms.LoadMessageInfo() == nil { 59 | ms.StoreMessageInfo(mi) 60 | } 61 | return ms 62 | } 63 | return mi.MessageOf(x) 64 | } 65 | 66 | // Deprecated: Use FindNodeRequest.ProtoReflect.Descriptor instead. 67 | func (*FindNodeRequest) Descriptor() ([]byte, []int) { 68 | return file_proto_islb_islb_proto_rawDescGZIP(), []int{0} 69 | } 70 | 71 | func (x *FindNodeRequest) GetSid() string { 72 | if x != nil { 73 | return x.Sid 74 | } 75 | return "" 76 | } 77 | 78 | func (x *FindNodeRequest) GetNid() string { 79 | if x != nil { 80 | return x.Nid 81 | } 82 | return "" 83 | } 84 | 85 | func (x *FindNodeRequest) GetService() string { 86 | if x != nil { 87 | return x.Service 88 | } 89 | return "" 90 | } 91 | 92 | type FindNodeReply struct { 93 | state protoimpl.MessageState 94 | sizeCache protoimpl.SizeCache 95 | unknownFields protoimpl.UnknownFields 96 | 97 | Nodes []*ion.Node `protobuf:"bytes,1,rep,name=nodes,proto3" json:"nodes,omitempty"` 98 | } 99 | 100 | func (x *FindNodeReply) Reset() { 101 | *x = FindNodeReply{} 102 | if protoimpl.UnsafeEnabled { 103 | mi := &file_proto_islb_islb_proto_msgTypes[1] 104 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 105 | ms.StoreMessageInfo(mi) 106 | } 107 | } 108 | 109 | func (x *FindNodeReply) String() string { 110 | return protoimpl.X.MessageStringOf(x) 111 | } 112 | 113 | func (*FindNodeReply) ProtoMessage() {} 114 | 115 | func (x *FindNodeReply) ProtoReflect() protoreflect.Message { 116 | mi := &file_proto_islb_islb_proto_msgTypes[1] 117 | if protoimpl.UnsafeEnabled && x != nil { 118 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 119 | if ms.LoadMessageInfo() == nil { 120 | ms.StoreMessageInfo(mi) 121 | } 122 | return ms 123 | } 124 | return mi.MessageOf(x) 125 | } 126 | 127 | // Deprecated: Use FindNodeReply.ProtoReflect.Descriptor instead. 128 | func (*FindNodeReply) Descriptor() ([]byte, []int) { 129 | return file_proto_islb_islb_proto_rawDescGZIP(), []int{1} 130 | } 131 | 132 | func (x *FindNodeReply) GetNodes() []*ion.Node { 133 | if x != nil { 134 | return x.Nodes 135 | } 136 | return nil 137 | } 138 | 139 | var File_proto_islb_islb_proto protoreflect.FileDescriptor 140 | 141 | var file_proto_islb_islb_proto_rawDesc = []byte{ 142 | 0x0a, 0x15, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2f, 0x69, 0x73, 0x6c, 0x62, 0x2f, 0x69, 0x73, 0x6c, 143 | 0x62, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x04, 0x69, 0x73, 0x6c, 0x62, 0x1a, 0x13, 0x70, 144 | 0x72, 0x6f, 0x74, 0x6f, 0x2f, 0x69, 0x6f, 0x6e, 0x2f, 0x69, 0x6f, 0x6e, 0x2e, 0x70, 0x72, 0x6f, 145 | 0x74, 0x6f, 0x22, 0x4f, 0x0a, 0x0f, 0x46, 0x69, 0x6e, 0x64, 0x4e, 0x6f, 0x64, 0x65, 0x52, 0x65, 146 | 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x10, 0x0a, 0x03, 0x73, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 147 | 0x28, 0x09, 0x52, 0x03, 0x73, 0x69, 0x64, 0x12, 0x10, 0x0a, 0x03, 0x6e, 0x69, 0x64, 0x18, 0x02, 148 | 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6e, 0x69, 0x64, 0x12, 0x18, 0x0a, 0x07, 0x73, 0x65, 0x72, 149 | 0x76, 0x69, 0x63, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x73, 0x65, 0x72, 0x76, 150 | 0x69, 0x63, 0x65, 0x22, 0x30, 0x0a, 0x0d, 0x46, 0x69, 0x6e, 0x64, 0x4e, 0x6f, 0x64, 0x65, 0x52, 151 | 0x65, 0x70, 0x6c, 0x79, 0x12, 0x1f, 0x0a, 0x05, 0x6e, 0x6f, 0x64, 0x65, 0x73, 0x18, 0x01, 0x20, 152 | 0x03, 0x28, 0x0b, 0x32, 0x09, 0x2e, 0x69, 0x6f, 0x6e, 0x2e, 0x4e, 0x6f, 0x64, 0x65, 0x52, 0x05, 153 | 0x6e, 0x6f, 0x64, 0x65, 0x73, 0x32, 0x06, 0x0a, 0x04, 0x49, 0x53, 0x4c, 0x42, 0x42, 0x20, 0x5a, 154 | 0x1e, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x70, 0x69, 0x6f, 0x6e, 155 | 0x2f, 0x69, 0x6f, 0x6e, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2f, 0x69, 0x73, 0x6c, 0x62, 0x62, 156 | 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, 157 | } 158 | 159 | var ( 160 | file_proto_islb_islb_proto_rawDescOnce sync.Once 161 | file_proto_islb_islb_proto_rawDescData = file_proto_islb_islb_proto_rawDesc 162 | ) 163 | 164 | func file_proto_islb_islb_proto_rawDescGZIP() []byte { 165 | file_proto_islb_islb_proto_rawDescOnce.Do(func() { 166 | file_proto_islb_islb_proto_rawDescData = protoimpl.X.CompressGZIP(file_proto_islb_islb_proto_rawDescData) 167 | }) 168 | return file_proto_islb_islb_proto_rawDescData 169 | } 170 | 171 | var file_proto_islb_islb_proto_msgTypes = make([]protoimpl.MessageInfo, 2) 172 | var file_proto_islb_islb_proto_goTypes = []interface{}{ 173 | (*FindNodeRequest)(nil), // 0: islb.FindNodeRequest 174 | (*FindNodeReply)(nil), // 1: islb.FindNodeReply 175 | (*ion.Node)(nil), // 2: ion.Node 176 | } 177 | var file_proto_islb_islb_proto_depIdxs = []int32{ 178 | 2, // 0: islb.FindNodeReply.nodes:type_name -> ion.Node 179 | 1, // [1:1] is the sub-list for method output_type 180 | 1, // [1:1] is the sub-list for method input_type 181 | 1, // [1:1] is the sub-list for extension type_name 182 | 1, // [1:1] is the sub-list for extension extendee 183 | 0, // [0:1] is the sub-list for field type_name 184 | } 185 | 186 | func init() { file_proto_islb_islb_proto_init() } 187 | func file_proto_islb_islb_proto_init() { 188 | if File_proto_islb_islb_proto != nil { 189 | return 190 | } 191 | if !protoimpl.UnsafeEnabled { 192 | file_proto_islb_islb_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} { 193 | switch v := v.(*FindNodeRequest); i { 194 | case 0: 195 | return &v.state 196 | case 1: 197 | return &v.sizeCache 198 | case 2: 199 | return &v.unknownFields 200 | default: 201 | return nil 202 | } 203 | } 204 | file_proto_islb_islb_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} { 205 | switch v := v.(*FindNodeReply); i { 206 | case 0: 207 | return &v.state 208 | case 1: 209 | return &v.sizeCache 210 | case 2: 211 | return &v.unknownFields 212 | default: 213 | return nil 214 | } 215 | } 216 | } 217 | type x struct{} 218 | out := protoimpl.TypeBuilder{ 219 | File: protoimpl.DescBuilder{ 220 | GoPackagePath: reflect.TypeOf(x{}).PkgPath(), 221 | RawDescriptor: file_proto_islb_islb_proto_rawDesc, 222 | NumEnums: 0, 223 | NumMessages: 2, 224 | NumExtensions: 0, 225 | NumServices: 1, 226 | }, 227 | GoTypes: file_proto_islb_islb_proto_goTypes, 228 | DependencyIndexes: file_proto_islb_islb_proto_depIdxs, 229 | MessageInfos: file_proto_islb_islb_proto_msgTypes, 230 | }.Build() 231 | File_proto_islb_islb_proto = out.File 232 | file_proto_islb_islb_proto_rawDesc = nil 233 | file_proto_islb_islb_proto_goTypes = nil 234 | file_proto_islb_islb_proto_depIdxs = nil 235 | } 236 | -------------------------------------------------------------------------------- /apps/room/server/room_signal.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "errors" 5 | "io" 6 | 7 | log "github.com/pion/ion-log" 8 | room "github.com/pion/ion/apps/room/proto" 9 | "github.com/pion/ion/pkg/util" 10 | "google.golang.org/grpc/codes" 11 | "google.golang.org/grpc/status" 12 | ) 13 | 14 | // RoomSignalService represents an RoomSignalService instance 15 | type RoomSignalService struct { 16 | room.UnimplementedRoomSignalServer 17 | rs *RoomService 18 | } 19 | 20 | // NewRoomService creates a new room app server instance 21 | func NewRoomSignalService(rs *RoomService) *RoomSignalService { 22 | s := &RoomSignalService{ 23 | rs: rs, 24 | } 25 | return s 26 | } 27 | 28 | func (s *RoomSignalService) Signal(stream room.RoomSignal_SignalServer) error { 29 | var p *Peer = nil 30 | defer func() { 31 | if p != nil { 32 | p.Close() 33 | } 34 | }() 35 | 36 | for { 37 | req, err := stream.Recv() 38 | 39 | if err != nil { 40 | if err == io.EOF { 41 | return nil 42 | } 43 | 44 | errStatus, _ := status.FromError(err) 45 | if errStatus.Code() == codes.Canceled { 46 | return nil 47 | } 48 | 49 | log.Errorf("signal error %v %v", errStatus.Message(), errStatus.Code()) 50 | return err 51 | } 52 | 53 | switch payload := req.Payload.(type) { 54 | case *room.Request_Join: 55 | log.Infof("[C->S]=%+v", payload) 56 | reply, peer, err := s.Join(payload, stream) 57 | if err != nil { 58 | log.Errorf("Join err: %v", err) 59 | return err 60 | } 61 | 62 | p = peer 63 | log.Infof("[S->C]=%+v", reply) 64 | err = stream.Send(&room.Reply{Payload: reply}) 65 | if err != nil { 66 | log.Errorf("stream send error: %v", err) 67 | } 68 | case *room.Request_Leave: 69 | log.Infof("[C->S]=%+v", payload) 70 | reply, err := s.Leave(payload) 71 | if err != nil { 72 | log.Errorf("Leave err: %v", err) 73 | return err 74 | } 75 | log.Infof("[S->C]=%+v", reply) 76 | err = stream.Send(&room.Reply{Payload: reply}) 77 | if err != nil { 78 | log.Errorf("stream send error: %v", err) 79 | } 80 | case *room.Request_SendMessage: 81 | log.Infof("[C->S]=%+v", payload) 82 | reply, err := s.SendMessage(payload) 83 | if err != nil { 84 | log.Errorf("LockConference err: %v", err) 85 | return err 86 | } 87 | log.Infof("[S->C]=%+v", reply) 88 | err = stream.Send(&room.Reply{Payload: reply}) 89 | if err != nil { 90 | log.Errorf("stream send error: %v", err) 91 | } 92 | 93 | case *room.Request_UpdateRoom: 94 | log.Infof("[C->S]=%+v", payload) 95 | reply, err := s.UpdateRoom(payload) 96 | if err != nil { 97 | log.Errorf("UpdateRoom err: %v", err) 98 | return err 99 | } 100 | log.Infof("[S->C]=%+v", reply) 101 | if err != nil { 102 | log.Errorf("stream send error: %v", err) 103 | } 104 | default: 105 | log.Errorf("unknown signal!! payload=%+v", payload) 106 | } 107 | } 108 | } 109 | 110 | func (s *RoomSignalService) Join(in *room.Request_Join, stream room.RoomSignal_SignalServer) (*room.Reply_Join, *Peer, error) { 111 | pinfo := in.Join.Peer 112 | 113 | if pinfo == nil || pinfo.Sid == "" && pinfo.Uid == "" { 114 | reply := &room.Reply_Join{ 115 | Join: &room.JoinReply{ 116 | Success: false, 117 | Room: nil, 118 | Error: &room.Error{ 119 | Code: room.ErrorType_InvalidParams, 120 | Reason: "sid/uid is empty", 121 | }, 122 | }, 123 | } 124 | return reply, nil, status.Errorf(codes.Internal, "sid/uid is empty") 125 | } 126 | 127 | sid := pinfo.Sid 128 | uid := pinfo.Uid 129 | 130 | key := util.GetRedisRoomKey(sid) 131 | // create in redis if room not exist 132 | if sid == "" { 133 | // store room info 134 | sid = pinfo.Sid 135 | err := s.rs.redis.HMSetTTL(roomRedisExpire, key, "sid", sid, "name", pinfo.DisplayName, 136 | "password", "", "description", "", "lock", "0") 137 | if err != nil { 138 | reply := &room.Reply_Join{ 139 | Join: &room.JoinReply{ 140 | Success: false, 141 | Room: nil, 142 | Error: &room.Error{ 143 | Code: room.ErrorType_ServiceUnavailable, 144 | Reason: err.Error(), 145 | }, 146 | }, 147 | } 148 | return reply, nil, err 149 | } 150 | } 151 | 152 | var peer *Peer = nil 153 | r := s.rs.getRoom(sid) 154 | 155 | if r == nil { 156 | // create room and store 157 | r = s.rs.createRoom(sid) 158 | err := s.rs.redis.HMSetTTL(roomRedisExpire, key, "sid", sid, "name", "", 159 | "password", "", "description", "", "lock", false) 160 | if err != nil { 161 | reply := &room.Reply_Join{ 162 | Join: &room.JoinReply{ 163 | Success: false, 164 | Room: nil, 165 | Error: &room.Error{ 166 | Code: room.ErrorType_ServiceUnavailable, 167 | Reason: err.Error(), 168 | }, 169 | }, 170 | } 171 | return reply, nil, err 172 | } 173 | } 174 | 175 | // some one didn't create room before join 176 | if r.info == nil { 177 | r.info = new(room.Room) 178 | } 179 | r.info.Sid = sid 180 | 181 | peer = NewPeer() 182 | peer.info = pinfo 183 | peer.sig = stream 184 | r.addPeer(peer) 185 | // TODO 186 | /* 187 | //Generate necessary metadata for routing. 188 | header := metadata.New(map[string]string{"service": "sfu", "nid": r.nid, "sid": sid, "uid": uid}) 189 | err := stream.SendHeader(header) 190 | if err != nil { 191 | log.Errorf("stream.SendHeader failed %v", err) 192 | } 193 | */ 194 | 195 | // store peer to redis 196 | key = util.GetRedisPeerKey(sid, uid) 197 | err := s.rs.redis.HMSetTTL(roomRedisExpire, key, "sid", sid, "uid", uid, "dest", in.Join.Peer.Destination, 198 | "name", in.Join.Peer.DisplayName, "role", in.Join.Peer.Role.String(), "protocol", in.Join.Peer.Protocol.String(), "direction", in.Join.Peer.Direction.String(), "avatar", in.Join.Peer.Avatar, "info", in.Join.Peer.ExtraInfo) 199 | if err != nil { 200 | reply := &room.Reply_Join{ 201 | Join: &room.JoinReply{ 202 | Success: false, 203 | Room: nil, 204 | Error: &room.Error{ 205 | Code: room.ErrorType_ServiceUnavailable, 206 | Reason: err.Error(), 207 | }, 208 | }, 209 | } 210 | return reply, nil, err 211 | } 212 | 213 | reply := &room.Reply_Join{ 214 | Join: &room.JoinReply{ 215 | Success: true, 216 | Room: r.info, 217 | Error: nil, 218 | }, 219 | } 220 | 221 | // find peer in room 222 | key = util.GetRedisPeersPrefixKey(sid) 223 | peersKeys := s.rs.redis.Keys(key) 224 | 225 | for _, pkey := range peersKeys { 226 | res := s.rs.redis.HGetAll(pkey) 227 | sid = res["sid"] 228 | uid = res["uid"] 229 | if sid == "" || uid == "" || uid == pinfo.Uid { 230 | continue 231 | } 232 | key = util.GetRedisPeerKey(sid, uid) 233 | res = s.rs.redis.HGetAll(key) 234 | if len(res) != 0 { 235 | info := &room.Peer{ 236 | Sid: res["sid"], 237 | Uid: res["uid"], 238 | DisplayName: res["name"], 239 | ExtraInfo: []byte(res["info"]), 240 | Role: room.Role(room.Role_value[res["role"]]), 241 | Protocol: room.Protocol(room.Protocol_value[res["protocol"]]), 242 | Avatar: res["avatar"], 243 | Direction: room.Peer_Direction(room.Peer_Direction_value["direction"]), 244 | Vendor: res["vendor"], 245 | } 246 | 247 | err := peer.sendPeerEvent(&room.PeerEvent{ 248 | State: room.PeerState_JOIN, 249 | Peer: info, 250 | }) 251 | 252 | if err != nil { 253 | log.Errorf("signal send peer event error: %v", err) 254 | } 255 | } 256 | } 257 | log.Infof("Join OK: replay=%+v", reply) 258 | return reply, peer, nil 259 | } 260 | 261 | func (s *RoomSignalService) Leave(in *room.Request_Leave) (*room.Reply_Leave, error) { 262 | sid := in.Leave.Sid 263 | uid := in.Leave.Uid 264 | if sid == "" || uid == "" { 265 | return &room.Reply_Leave{ 266 | Leave: &room.LeaveReply{ 267 | Success: false, 268 | Error: &room.Error{ 269 | Code: room.ErrorType_RoomNotExist, 270 | Reason: "sid/uid is empty", 271 | }, 272 | }, 273 | }, status.Errorf(codes.Internal, "sid/uid is empty") 274 | } 275 | 276 | r := s.rs.getRoom(sid) 277 | if r == nil { 278 | return &room.Reply_Leave{ 279 | Leave: &room.LeaveReply{ 280 | Success: false, 281 | Error: &room.Error{ 282 | Code: room.ErrorType_RoomNotExist, 283 | Reason: "room not exist", 284 | }, 285 | }, 286 | }, status.Errorf(codes.Internal, "room not exist") 287 | } 288 | 289 | peer := r.getPeer(uid) 290 | if peer != nil && peer.info.Uid == uid { 291 | if r.delPeer(peer) == 0 { 292 | s.rs.delRoom(r) 293 | r = nil 294 | } 295 | } 296 | 297 | reply := &room.Reply_Leave{ 298 | Leave: &room.LeaveReply{ 299 | Success: true, 300 | Error: nil, 301 | }, 302 | } 303 | 304 | return reply, nil 305 | } 306 | 307 | func (s *RoomSignalService) SendMessage(in *room.Request_SendMessage) (*room.Reply_SendMessage, error) { 308 | msg := in.SendMessage.Message 309 | sid := in.SendMessage.Sid 310 | log.Infof("Message: %+v", msg) 311 | r := s.rs.getRoom(sid) 312 | if r == nil { 313 | log.Warnf("room not found, maybe the peer did not join") 314 | return &room.Reply_SendMessage{}, errors.New("room not exist") 315 | } 316 | r.sendMessage(msg) 317 | return &room.Reply_SendMessage{}, nil 318 | } 319 | 320 | func (s *RoomSignalService) UpdateRoom(in *room.Request_UpdateRoom) (*room.UpdateRoomReply, error) { 321 | // TODO: Do not allow update room for peers that are host 322 | reply, err := s.rs.UpdateRoom(nil, in.UpdateRoom) 323 | return reply, err 324 | } 325 | -------------------------------------------------------------------------------- /apps/room/server/room.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "os" 5 | "sync" 6 | "time" 7 | 8 | natsDiscoveryClient "github.com/cloudwebrtc/nats-discovery/pkg/client" 9 | "github.com/cloudwebrtc/nats-discovery/pkg/discovery" 10 | natsRPC "github.com/cloudwebrtc/nats-grpc/pkg/rpc" 11 | "github.com/cloudwebrtc/nats-grpc/pkg/rpc/reflection" 12 | "github.com/nats-io/nats.go" 13 | log "github.com/pion/ion-log" 14 | 15 | room "github.com/pion/ion/apps/room/proto" 16 | "github.com/pion/ion/pkg/db" 17 | "github.com/pion/ion/pkg/ion" 18 | "github.com/pion/ion/pkg/proto" 19 | "github.com/pion/ion/pkg/runner" 20 | "github.com/pion/ion/pkg/util" 21 | "github.com/spf13/viper" 22 | "google.golang.org/grpc" 23 | ) 24 | 25 | type global struct { 26 | Dc string `mapstructure:"dc"` 27 | } 28 | 29 | type logConf struct { 30 | Level string `mapstructure:"level"` 31 | } 32 | 33 | type natsConf struct { 34 | URL string `mapstructure:"url"` 35 | } 36 | 37 | // Config for room node 38 | type Config struct { 39 | runner.ConfigBase 40 | Global global `mapstructure:"global"` 41 | Log logConf `mapstructure:"log"` 42 | Nats natsConf `mapstructure:"nats"` 43 | Redis db.Config `mapstructure:"redis"` 44 | } 45 | 46 | func unmarshal(rawVal interface{}) error { 47 | if err := viper.Unmarshal(rawVal); err != nil { 48 | return err 49 | } 50 | return nil 51 | } 52 | 53 | func (c *Config) Load(file string) error { 54 | _, err := os.Stat(file) 55 | if err != nil { 56 | return err 57 | } 58 | 59 | viper.SetConfigFile(file) 60 | viper.SetConfigType("toml") 61 | 62 | err = viper.ReadInConfig() 63 | if err != nil { 64 | log.Errorf("config file %s read failed. %v\n", file, err) 65 | return err 66 | } 67 | 68 | err = unmarshal(c) 69 | if err != nil { 70 | return err 71 | } 72 | 73 | if err != nil { 74 | log.Errorf("config file %s loaded failed. %v\n", file, err) 75 | return err 76 | } 77 | 78 | log.Infof("config %s load ok!", file) 79 | return nil 80 | } 81 | 82 | // Room represents a Room which manage peers 83 | type Room struct { 84 | sync.RWMutex 85 | sid string 86 | peers map[string]*Peer 87 | info *room.Room 88 | update time.Time 89 | redis *db.Redis 90 | } 91 | 92 | type RoomServer struct { 93 | // for standalone running 94 | runner.Service 95 | 96 | // grpc room service 97 | RoomService 98 | RoomSignalService 99 | 100 | // for distributed node running 101 | ion.Node 102 | natsConn *nats.Conn 103 | natsDiscoveryCli *natsDiscoveryClient.Client 104 | 105 | // config 106 | conf Config 107 | } 108 | 109 | // New create a room node instance 110 | func New() *RoomServer { 111 | return &RoomServer{ 112 | Node: ion.NewNode("room-" + util.RandomString(6)), 113 | } 114 | } 115 | 116 | // Load load config file 117 | func (r *RoomServer) Load(confFile string) error { 118 | err := r.conf.Load(confFile) 119 | if err != nil { 120 | log.Errorf("config load error: %v", err) 121 | return err 122 | } 123 | return nil 124 | } 125 | 126 | // ConfigBase used for runner 127 | func (r *RoomServer) ConfigBase() runner.ConfigBase { 128 | return &r.conf 129 | } 130 | 131 | // StartGRPC for standalone bin 132 | func (r *RoomServer) StartGRPC(registrar grpc.ServiceRegistrar) error { 133 | var err error 134 | 135 | ndc, err := natsDiscoveryClient.NewClient(nil) 136 | if err != nil { 137 | log.Errorf("failed to create discovery client: %v", err) 138 | ndc.Close() 139 | return err 140 | } 141 | 142 | r.natsDiscoveryCli = ndc 143 | r.natsConn = nil 144 | r.RoomService = *NewRoomService(r.conf.Redis) 145 | log.Infof("NewRoomService r.conf.Redis=%+v r.redis=%+v", r.conf.Redis, r.redis) 146 | r.RoomSignalService = *NewRoomSignalService(&r.RoomService) 147 | 148 | room.RegisterRoomServiceServer(registrar, &r.RoomService) 149 | room.RegisterRoomSignalServer(registrar, &r.RoomSignalService) 150 | 151 | return nil 152 | } 153 | 154 | // Start for distributed node 155 | func (r *RoomServer) Start() error { 156 | var err error 157 | 158 | log.Infof("r.conf.Nats.URL===%+v", r.conf.Nats.URL) 159 | err = r.Node.Start(r.conf.Nats.URL) 160 | if err != nil { 161 | r.Close() 162 | return err 163 | } 164 | 165 | ndc, err := natsDiscoveryClient.NewClient(r.NatsConn()) 166 | if err != nil { 167 | log.Errorf("failed to create discovery client: %v", err) 168 | ndc.Close() 169 | return err 170 | } 171 | 172 | r.natsDiscoveryCli = ndc 173 | r.natsConn = r.NatsConn() 174 | r.RoomService = *NewRoomService(r.conf.Redis) 175 | log.Infof("NewRoomService r.conf.Redis=%+v r.redis=%+v", r.conf.Redis, r.redis) 176 | r.RoomSignalService = *NewRoomSignalService(&r.RoomService) 177 | 178 | if err != nil { 179 | r.Close() 180 | return err 181 | } 182 | 183 | room.RegisterRoomServiceServer(r.Node.ServiceRegistrar(), &r.RoomService) 184 | room.RegisterRoomSignalServer(r.Node.ServiceRegistrar(), &r.RoomSignalService) 185 | // Register reflection service on nats-rpc server. 186 | reflection.Register(r.Node.ServiceRegistrar().(*natsRPC.Server)) 187 | 188 | node := discovery.Node{ 189 | DC: r.conf.Global.Dc, 190 | Service: proto.ServiceROOM, 191 | NID: r.Node.NID, 192 | RPC: discovery.RPC{ 193 | Protocol: discovery.NGRPC, 194 | Addr: r.conf.Nats.URL, 195 | }, 196 | } 197 | 198 | go func() { 199 | err := r.Node.KeepAlive(node) 200 | if err != nil { 201 | log.Errorf("Room.Node.KeepAlive(%v) error %v", r.Node.NID, err) 202 | } 203 | }() 204 | 205 | //Watch ALL nodes. 206 | go func() { 207 | err := r.Node.Watch(proto.ServiceALL) 208 | if err != nil { 209 | log.Errorf("Node.Watch(proto.ServiceALL) error %v", err) 210 | } 211 | }() 212 | return nil 213 | } 214 | 215 | func (s *RoomServer) Close() { 216 | s.RoomService.Close() 217 | s.Node.Close() 218 | } 219 | 220 | // newRoom creates a new room instance 221 | func newRoom(sid string, redis *db.Redis) *Room { 222 | r := &Room{ 223 | sid: sid, 224 | peers: make(map[string]*Peer), 225 | update: time.Now(), 226 | redis: redis, 227 | } 228 | return r 229 | } 230 | 231 | // Room name 232 | func (r *Room) Name() string { 233 | return r.info.Name 234 | } 235 | 236 | // SID room id 237 | func (r *Room) SID() string { 238 | return r.sid 239 | } 240 | 241 | // addPeer add a peer to room 242 | func (r *Room) addPeer(p *Peer) { 243 | event := &room.PeerEvent{ 244 | Peer: p.info, 245 | State: room.PeerState_JOIN, 246 | } 247 | 248 | r.broadcastPeerEvent(event) 249 | 250 | r.Lock() 251 | p.room = r 252 | r.peers[p.info.Uid] = p 253 | r.update = time.Now() 254 | r.Unlock() 255 | } 256 | 257 | // func (r *Room) roomLocked() bool { 258 | // r.RLock() 259 | // defer r.RUnlock() 260 | // r.update = time.Now() 261 | // return r.info.Lock 262 | // } 263 | 264 | // getPeer get a peer by peer id 265 | func (r *Room) getPeer(uid string) *Peer { 266 | r.RLock() 267 | defer r.RUnlock() 268 | return r.peers[uid] 269 | } 270 | 271 | // getPeers get peers in the room 272 | func (r *Room) getPeers() []*Peer { 273 | r.RLock() 274 | defer r.RUnlock() 275 | p := make([]*Peer, 0, len(r.peers)) 276 | for _, peer := range r.peers { 277 | p = append(p, peer) 278 | } 279 | return p 280 | } 281 | 282 | // delPeer delete a peer in the room 283 | func (r *Room) delPeer(p *Peer) int { 284 | uid := p.info.Uid 285 | r.Lock() 286 | r.update = time.Now() 287 | found := r.peers[uid] == p 288 | if !found { 289 | r.Unlock() 290 | return -1 291 | } 292 | 293 | delete(r.peers, uid) 294 | peerCount := len(r.peers) 295 | r.Unlock() 296 | 297 | event := &room.PeerEvent{ 298 | Peer: p.info, 299 | State: room.PeerState_LEAVE, 300 | } 301 | 302 | key := util.GetRedisPeerKey(p.info.Sid, uid) 303 | err := r.redis.Del(key) 304 | if err != nil { 305 | log.Errorf("err=%v", err) 306 | } 307 | 308 | r.broadcastPeerEvent(event) 309 | 310 | return peerCount 311 | } 312 | 313 | // count return count of peers in room 314 | func (r *Room) count() int { 315 | r.RLock() 316 | defer r.RUnlock() 317 | return len(r.peers) 318 | } 319 | 320 | func (r *Room) broadcastRoomEvent(uid string, event *room.Reply) { 321 | log.Infof("event=%+v", event) 322 | peers := r.getPeers() 323 | r.update = time.Now() 324 | for _, p := range peers { 325 | if p.UID() == uid { 326 | continue 327 | } 328 | 329 | if err := p.send(event); err != nil { 330 | log.Errorf("send data to peer(%s) error: %v", p.info.Uid, err) 331 | } 332 | } 333 | } 334 | 335 | func (r *Room) broadcastPeerEvent(event *room.PeerEvent) { 336 | log.Infof("event=%+v", event) 337 | peers := r.getPeers() 338 | r.update = time.Now() 339 | for _, p := range peers { 340 | if p.info.Uid == event.Peer.Uid { 341 | continue 342 | } 343 | if err := p.sendPeerEvent(event); err != nil { 344 | log.Errorf("send data to peer(%s) error: %v", p.info.Uid, err) 345 | } 346 | } 347 | } 348 | 349 | func (r *Room) sendMessage(msg *room.Message) { 350 | log.Infof("msg=%+v", msg) 351 | r.update = time.Now() 352 | from := msg.From 353 | to := msg.To 354 | dtype := msg.Type 355 | data := msg.Payload 356 | log.Debugf("Room.onMessage %v => %v, type: %v, data: %v", from, to, dtype, data) 357 | if to == "all" { 358 | r.broadcastRoomEvent( 359 | from, 360 | &room.Reply{ 361 | Payload: &room.Reply_Message{ 362 | Message: msg, 363 | }, 364 | }, 365 | ) 366 | return 367 | } 368 | 369 | peers := r.getPeers() 370 | for _, p := range peers { 371 | if to == p.info.Uid { 372 | if err := p.sendMessage(msg); err != nil { 373 | log.Errorf("send msg to peer(%s) error: %v", p.info.Uid, err) 374 | } 375 | } 376 | } 377 | } 378 | -------------------------------------------------------------------------------- /proto/debug/debug.pb.go: -------------------------------------------------------------------------------- 1 | // Code generated by protoc-gen-go. DO NOT EDIT. 2 | // versions: 3 | // protoc-gen-go v1.25.0 4 | // protoc v3.12.4 5 | // source: proto/debug/debug.proto 6 | 7 | package debug 8 | 9 | import ( 10 | proto "github.com/golang/protobuf/proto" 11 | protoreflect "google.golang.org/protobuf/reflect/protoreflect" 12 | protoimpl "google.golang.org/protobuf/runtime/protoimpl" 13 | reflect "reflect" 14 | sync "sync" 15 | ) 16 | 17 | const ( 18 | // Verify that this generated code is sufficiently up-to-date. 19 | _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) 20 | // Verify that runtime/protoimpl is sufficiently up-to-date. 21 | _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) 22 | ) 23 | 24 | // This is a compile-time assertion that a sufficiently up-to-date version 25 | // of the legacy proto package is being used. 26 | const _ = proto.ProtoPackageIsVersion4 27 | 28 | type Debugging struct { 29 | state protoimpl.MessageState 30 | sizeCache protoimpl.SizeCache 31 | unknownFields protoimpl.UnknownFields 32 | 33 | Nid string `protobuf:"bytes,1,opt,name=nid,proto3" json:"nid,omitempty"` 34 | Service string `protobuf:"bytes,2,opt,name=service,proto3" json:"service,omitempty"` 35 | File string `protobuf:"bytes,3,opt,name=file,proto3" json:"file,omitempty"` 36 | Line int32 `protobuf:"varint,4,opt,name=line,proto3" json:"line,omitempty"` 37 | Function string `protobuf:"bytes,5,opt,name=function,proto3" json:"function,omitempty"` 38 | } 39 | 40 | func (x *Debugging) Reset() { 41 | *x = Debugging{} 42 | if protoimpl.UnsafeEnabled { 43 | mi := &file_proto_debug_debug_proto_msgTypes[0] 44 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 45 | ms.StoreMessageInfo(mi) 46 | } 47 | } 48 | 49 | func (x *Debugging) String() string { 50 | return protoimpl.X.MessageStringOf(x) 51 | } 52 | 53 | func (*Debugging) ProtoMessage() {} 54 | 55 | func (x *Debugging) ProtoReflect() protoreflect.Message { 56 | mi := &file_proto_debug_debug_proto_msgTypes[0] 57 | if protoimpl.UnsafeEnabled && x != nil { 58 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 59 | if ms.LoadMessageInfo() == nil { 60 | ms.StoreMessageInfo(mi) 61 | } 62 | return ms 63 | } 64 | return mi.MessageOf(x) 65 | } 66 | 67 | // Deprecated: Use Debugging.ProtoReflect.Descriptor instead. 68 | func (*Debugging) Descriptor() ([]byte, []int) { 69 | return file_proto_debug_debug_proto_rawDescGZIP(), []int{0} 70 | } 71 | 72 | func (x *Debugging) GetNid() string { 73 | if x != nil { 74 | return x.Nid 75 | } 76 | return "" 77 | } 78 | 79 | func (x *Debugging) GetService() string { 80 | if x != nil { 81 | return x.Service 82 | } 83 | return "" 84 | } 85 | 86 | func (x *Debugging) GetFile() string { 87 | if x != nil { 88 | return x.File 89 | } 90 | return "" 91 | } 92 | 93 | func (x *Debugging) GetLine() int32 { 94 | if x != nil { 95 | return x.Line 96 | } 97 | return 0 98 | } 99 | 100 | func (x *Debugging) GetFunction() string { 101 | if x != nil { 102 | return x.Function 103 | } 104 | return "" 105 | } 106 | 107 | type IonError struct { 108 | state protoimpl.MessageState 109 | sizeCache protoimpl.SizeCache 110 | unknownFields protoimpl.UnknownFields 111 | 112 | ErrorCode int32 `protobuf:"varint,1,opt,name=errorCode,proto3" json:"errorCode,omitempty"` 113 | Description string `protobuf:"bytes,2,opt,name=description,proto3" json:"description,omitempty"` 114 | Debugging *Debugging `protobuf:"bytes,3,opt,name=debugging,proto3,oneof" json:"debugging,omitempty"` 115 | } 116 | 117 | func (x *IonError) Reset() { 118 | *x = IonError{} 119 | if protoimpl.UnsafeEnabled { 120 | mi := &file_proto_debug_debug_proto_msgTypes[1] 121 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 122 | ms.StoreMessageInfo(mi) 123 | } 124 | } 125 | 126 | func (x *IonError) String() string { 127 | return protoimpl.X.MessageStringOf(x) 128 | } 129 | 130 | func (*IonError) ProtoMessage() {} 131 | 132 | func (x *IonError) ProtoReflect() protoreflect.Message { 133 | mi := &file_proto_debug_debug_proto_msgTypes[1] 134 | if protoimpl.UnsafeEnabled && x != nil { 135 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 136 | if ms.LoadMessageInfo() == nil { 137 | ms.StoreMessageInfo(mi) 138 | } 139 | return ms 140 | } 141 | return mi.MessageOf(x) 142 | } 143 | 144 | // Deprecated: Use IonError.ProtoReflect.Descriptor instead. 145 | func (*IonError) Descriptor() ([]byte, []int) { 146 | return file_proto_debug_debug_proto_rawDescGZIP(), []int{1} 147 | } 148 | 149 | func (x *IonError) GetErrorCode() int32 { 150 | if x != nil { 151 | return x.ErrorCode 152 | } 153 | return 0 154 | } 155 | 156 | func (x *IonError) GetDescription() string { 157 | if x != nil { 158 | return x.Description 159 | } 160 | return "" 161 | } 162 | 163 | func (x *IonError) GetDebugging() *Debugging { 164 | if x != nil { 165 | return x.Debugging 166 | } 167 | return nil 168 | } 169 | 170 | var File_proto_debug_debug_proto protoreflect.FileDescriptor 171 | 172 | var file_proto_debug_debug_proto_rawDesc = []byte{ 173 | 0x0a, 0x17, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2f, 0x64, 0x65, 0x62, 0x75, 0x67, 0x2f, 0x64, 0x65, 174 | 0x62, 0x75, 0x67, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x05, 0x64, 0x65, 0x62, 0x75, 0x67, 175 | 0x22, 0x7b, 0x0a, 0x09, 0x44, 0x65, 0x62, 0x75, 0x67, 0x67, 0x69, 0x6e, 0x67, 0x12, 0x10, 0x0a, 176 | 0x03, 0x6e, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6e, 0x69, 0x64, 0x12, 177 | 0x18, 0x0a, 0x07, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 178 | 0x52, 0x07, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x66, 0x69, 0x6c, 179 | 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x66, 0x69, 0x6c, 0x65, 0x12, 0x12, 0x0a, 180 | 0x04, 0x6c, 0x69, 0x6e, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x05, 0x52, 0x04, 0x6c, 0x69, 0x6e, 181 | 0x65, 0x12, 0x1a, 0x0a, 0x08, 0x66, 0x75, 0x6e, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x05, 0x20, 182 | 0x01, 0x28, 0x09, 0x52, 0x08, 0x66, 0x75, 0x6e, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x22, 0x8d, 0x01, 183 | 0x0a, 0x08, 0x49, 0x6f, 0x6e, 0x45, 0x72, 0x72, 0x6f, 0x72, 0x12, 0x1c, 0x0a, 0x09, 0x65, 0x72, 184 | 0x72, 0x6f, 0x72, 0x43, 0x6f, 0x64, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x05, 0x52, 0x09, 0x65, 185 | 0x72, 0x72, 0x6f, 0x72, 0x43, 0x6f, 0x64, 0x65, 0x12, 0x20, 0x0a, 0x0b, 0x64, 0x65, 0x73, 0x63, 186 | 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x64, 187 | 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x33, 0x0a, 0x09, 0x64, 0x65, 188 | 0x62, 0x75, 0x67, 0x67, 0x69, 0x6e, 0x67, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x10, 0x2e, 189 | 0x64, 0x65, 0x62, 0x75, 0x67, 0x2e, 0x44, 0x65, 0x62, 0x75, 0x67, 0x67, 0x69, 0x6e, 0x67, 0x48, 190 | 0x00, 0x52, 0x09, 0x64, 0x65, 0x62, 0x75, 0x67, 0x67, 0x69, 0x6e, 0x67, 0x88, 0x01, 0x01, 0x42, 191 | 0x0c, 0x0a, 0x0a, 0x5f, 0x64, 0x65, 0x62, 0x75, 0x67, 0x67, 0x69, 0x6e, 0x67, 0x42, 0x21, 0x5a, 192 | 0x1f, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x70, 0x69, 0x6f, 0x6e, 193 | 0x2f, 0x69, 0x6f, 0x6e, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2f, 0x64, 0x65, 0x62, 0x75, 0x67, 194 | 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, 195 | } 196 | 197 | var ( 198 | file_proto_debug_debug_proto_rawDescOnce sync.Once 199 | file_proto_debug_debug_proto_rawDescData = file_proto_debug_debug_proto_rawDesc 200 | ) 201 | 202 | func file_proto_debug_debug_proto_rawDescGZIP() []byte { 203 | file_proto_debug_debug_proto_rawDescOnce.Do(func() { 204 | file_proto_debug_debug_proto_rawDescData = protoimpl.X.CompressGZIP(file_proto_debug_debug_proto_rawDescData) 205 | }) 206 | return file_proto_debug_debug_proto_rawDescData 207 | } 208 | 209 | var file_proto_debug_debug_proto_msgTypes = make([]protoimpl.MessageInfo, 2) 210 | var file_proto_debug_debug_proto_goTypes = []interface{}{ 211 | (*Debugging)(nil), // 0: debug.Debugging 212 | (*IonError)(nil), // 1: debug.IonError 213 | } 214 | var file_proto_debug_debug_proto_depIdxs = []int32{ 215 | 0, // 0: debug.IonError.debugging:type_name -> debug.Debugging 216 | 1, // [1:1] is the sub-list for method output_type 217 | 1, // [1:1] is the sub-list for method input_type 218 | 1, // [1:1] is the sub-list for extension type_name 219 | 1, // [1:1] is the sub-list for extension extendee 220 | 0, // [0:1] is the sub-list for field type_name 221 | } 222 | 223 | func init() { file_proto_debug_debug_proto_init() } 224 | func file_proto_debug_debug_proto_init() { 225 | if File_proto_debug_debug_proto != nil { 226 | return 227 | } 228 | if !protoimpl.UnsafeEnabled { 229 | file_proto_debug_debug_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} { 230 | switch v := v.(*Debugging); i { 231 | case 0: 232 | return &v.state 233 | case 1: 234 | return &v.sizeCache 235 | case 2: 236 | return &v.unknownFields 237 | default: 238 | return nil 239 | } 240 | } 241 | file_proto_debug_debug_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} { 242 | switch v := v.(*IonError); i { 243 | case 0: 244 | return &v.state 245 | case 1: 246 | return &v.sizeCache 247 | case 2: 248 | return &v.unknownFields 249 | default: 250 | return nil 251 | } 252 | } 253 | } 254 | file_proto_debug_debug_proto_msgTypes[1].OneofWrappers = []interface{}{} 255 | type x struct{} 256 | out := protoimpl.TypeBuilder{ 257 | File: protoimpl.DescBuilder{ 258 | GoPackagePath: reflect.TypeOf(x{}).PkgPath(), 259 | RawDescriptor: file_proto_debug_debug_proto_rawDesc, 260 | NumEnums: 0, 261 | NumMessages: 2, 262 | NumExtensions: 0, 263 | NumServices: 0, 264 | }, 265 | GoTypes: file_proto_debug_debug_proto_goTypes, 266 | DependencyIndexes: file_proto_debug_debug_proto_depIdxs, 267 | MessageInfos: file_proto_debug_debug_proto_msgTypes, 268 | }.Build() 269 | File_proto_debug_debug_proto = out.File 270 | file_proto_debug_debug_proto_rawDesc = nil 271 | file_proto_debug_debug_proto_goTypes = nil 272 | file_proto_debug_debug_proto_depIdxs = nil 273 | } 274 | -------------------------------------------------------------------------------- /examples/ion-pubsub/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 | 43 | room = new Ion.Room(connector); 44 | 45 | room.onjoin = function (result){ 46 | console.log('[onjoin]: success ' + result.success + ', room info: ' + JSON.stringify(result.room)); 47 | }; 48 | 49 | room.onleave = function (reason){ 50 | console.log('[onleave]: leave room, reason ' + reason); 51 | }; 52 | 53 | room.onmessage = function (msg){ 54 | console.log('[onmessage]: Received msg:', msg) 55 | const uint8Arr = new Uint8Array(msg.data); 56 | const decodedString = String.fromCharCode.apply(null, uint8Arr); 57 | const json = JSON.parse(decodedString); 58 | remoteData.innerHTML = remoteData.innerHTML + json.msg+ '\n'; 59 | }; 60 | 61 | room.onpeerevent = function (event){ 62 | switch(event.state) { 63 | case Ion.PeerState.JOIN: 64 | console.log('[onpeerevent]: Peer ' + event.peer.uid + ' joined'); 65 | break; 66 | case Ion.PeerState.LEAVE: 67 | console.log('[onpeerevent]: Peer ' + event.peer.uid + ' left'); 68 | break; 69 | case Ion.PeerState.UPDATE: 70 | console.log('[onpeerevent]: Peer ' + event.peer.uid + ' updated'); 71 | break; 72 | } 73 | }; 74 | 75 | room.onroominfo = function (info){ 76 | console.log('[onroominfo]: ' + JSON.stringify(info)); 77 | }; 78 | 79 | room.ondisconnect = function (dis){ 80 | console.log('[ondisconnect]: Disconnected from server ' + dis); 81 | }; 82 | 83 | const result = await room.join({ 84 | sid: sid, 85 | uid: uid, 86 | displayname: 'new peer', 87 | extrainfo: '', 88 | destination: 'webrtc://ion/peer1', 89 | role: Ion.Role.HOST, 90 | protocol: Ion.Protocol.WEBRTC , 91 | avatar: 'string', 92 | direction: Ion.Direction.INCOMING, 93 | vendor: 'string', 94 | }, '') 95 | .then((result) => { 96 | console.log('[join] result: success ' + result?.success + ', room info: ' + JSON.stringify(result?.room)); 97 | joinBtn.disabled = "true"; 98 | remoteData.innerHTML = remoteData.innerHTML + JSON.stringify(result) + '\n'; 99 | leaveBtn.removeAttribute('disabled'); 100 | publishBtn.removeAttribute('disabled'); 101 | publishSBtn.removeAttribute('disabled'); 102 | 103 | rtc = new Ion.RTC(connector); 104 | 105 | rtc.ontrack = (track, stream) => { 106 | console.log("got ", track.kind, " track", track.id, "for stream", stream.id); 107 | if (track.kind === "video") { 108 | track.onunmute = () => { 109 | if (!streams[stream.id]) { 110 | const remoteVideo = document.createElement("video"); 111 | remoteVideo.srcObject = stream; 112 | remoteVideo.autoplay = true; 113 | remoteVideo.muted = true; 114 | remoteVideo.addEventListener("loadedmetadata", function () { 115 | sizeTag.innerHTML = `${remoteVideo.videoWidth}x${remoteVideo.videoHeight}`; 116 | }); 117 | 118 | remoteVideo.onresize = function () { 119 | sizeTag.innerHTML = `${remoteVideo.videoWidth}x${remoteVideo.videoHeight}`; 120 | }; 121 | remotesDiv.appendChild(remoteVideo); 122 | streams[stream.id] = stream; 123 | stream.onremovetrack = () => { 124 | if (streams[stream.id]) { 125 | remotesDiv.removeChild(remoteVideo); 126 | streams[stream.id] = null; 127 | } 128 | }; 129 | getStats(); 130 | } 131 | }; 132 | } 133 | }; 134 | 135 | rtc.ontrackevent = function (ev) { 136 | console.log("ontrackevent: \nuid = ", ev.uid, " \nstate = ", ev.state, ", \ntracks = ", JSON.stringify(ev.tracks)); 137 | if (trackEvent === undefined) { 138 | console.log("store trackEvent=", ev) 139 | trackEvent = ev; 140 | } 141 | remoteSignal.innerHTML = remoteSignal.innerHTML + JSON.stringify(ev) + '\n'; 142 | }; 143 | 144 | rtc.join(sid, uid); 145 | 146 | const streams = {}; 147 | 148 | start = (sc) => { 149 | publishSBtn.disabled = "true"; 150 | publishBtn.disabled = "true"; 151 | 152 | let constraints = { 153 | resolution: resolutionBox.options[resolutionBox.selectedIndex].value, 154 | codec: codecBox.options[codecBox.selectedIndex].value, 155 | audio: true, 156 | simulcast: sc, 157 | } 158 | console.log("getUserMedia constraints=", constraints) 159 | Ion.LocalStream.getUserMedia(constraints) 160 | .then((media) => { 161 | localStream = media; 162 | localVideo.srcObject = media; 163 | localVideo.autoplay = true; 164 | localVideo.controls = true; 165 | localVideo.muted = true; 166 | 167 | rtc.publish(media); 168 | localDataChannel = rtc.createDataChannel(uid); 169 | }) 170 | .catch(console.error); 171 | }; 172 | 173 | }); 174 | } 175 | 176 | const send = () => { 177 | if (!room) { 178 | alert('join room first!', '', { 179 | confirmButtonText: 'OK', 180 | }); 181 | return 182 | } 183 | const payload = new Map(); 184 | payload.set('msg', localData.value); 185 | console.log("[send]: sid=", sid, "from=", 'sender', "to=", uid, "payload=", payload); 186 | room.message(sid, uid, "all", 'Map', payload); 187 | } 188 | 189 | 190 | const leave = () => { 191 | console.log("[leave]: sid=" + sid + " uid=", uid) 192 | room.leave(sid, uid); 193 | joinBtn.removeAttribute('disabled'); 194 | leaveBtn.disabled = "true"; 195 | publishBtn.disabled = "true"; 196 | publishSBtn.disabled = "true"; 197 | location.reload(); 198 | } 199 | 200 | const subscribe = () => { 201 | let layer = subscribeBox.value 202 | console.log("subscribe trackEvent=", trackEvent, "layer=", layer) 203 | var infos = []; 204 | trackEvent.tracks.forEach(t => { 205 | if (t.layer === layer && t.kind === "video"){ 206 | infos.push({ 207 | track_id: t.id, 208 | mute: t.muted, 209 | layer: t.layer, 210 | subscribe: true 211 | }); 212 | } 213 | 214 | if (t.kind === "audio"){ 215 | infos.push({ 216 | track_id: t.id, 217 | mute: t.muted, 218 | layer: t.layer, 219 | subscribe: true 220 | }); 221 | } 222 | }); 223 | console.log("subscribe infos=", infos) 224 | rtc.subscribe(infos); 225 | } 226 | 227 | 228 | 229 | const controlLocalVideo = (radio) => { 230 | if (radio.value === "false") { 231 | localStream.mute("video"); 232 | } else { 233 | localStream.unmute("video"); 234 | } 235 | }; 236 | 237 | const controlLocalAudio = (radio) => { 238 | if (radio.value === "false") { 239 | localStream.mute("audio"); 240 | } else { 241 | localStream.unmute("audio"); 242 | } 243 | }; 244 | 245 | const getStats = () => { 246 | let bytesPrev; 247 | let timestampPrev; 248 | setInterval(() => { 249 | rtc.getSubStats(null).then((results) => { 250 | results.forEach((report) => { 251 | const now = report.timestamp; 252 | 253 | let bitrate; 254 | if ( 255 | report.type === "inbound-rtp" && 256 | report.mediaType === "video" 257 | ) { 258 | const bytes = report.bytesReceived; 259 | if (timestampPrev) { 260 | bitrate = (8 * (bytes - bytesPrev)) / (now - timestampPrev); 261 | bitrate = Math.floor(bitrate); 262 | } 263 | bytesPrev = bytes; 264 | timestampPrev = now; 265 | } 266 | if (bitrate) { 267 | brTag.innerHTML = `${bitrate} kbps @ ${report.framesPerSecond} fps`; 268 | } 269 | }); 270 | }); 271 | }, 1000); 272 | }; 273 | 274 | function syntaxHighlight(json) { 275 | json = json 276 | .replace(/&/g, "&") 277 | .replace(//g, ">"); 279 | return json.replace( 280 | /("(\\u[a-zA-Z0-9]{4}|\\[^u]|[^\\"])*"(\s*:)?|\b(true|false|null)\b|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?)/g, 281 | function (match) { 282 | let cls = "number"; 283 | if (/^"/.test(match)) { 284 | if (/:$/.test(match)) { 285 | cls = "key"; 286 | } else { 287 | cls = "string"; 288 | } 289 | } else if (/true|false/.test(match)) { 290 | cls = "boolean"; 291 | } else if (/null/.test(match)) { 292 | cls = "null"; 293 | } 294 | return '' + match + ""; 295 | } 296 | ); 297 | } 298 | -------------------------------------------------------------------------------- /proto/ion/ion.pb.go: -------------------------------------------------------------------------------- 1 | // Code generated by protoc-gen-go. DO NOT EDIT. 2 | // versions: 3 | // protoc-gen-go v1.25.0 4 | // protoc v3.12.4 5 | // source: proto/ion/ion.proto 6 | 7 | package ion 8 | 9 | import ( 10 | proto "github.com/golang/protobuf/proto" 11 | protoreflect "google.golang.org/protobuf/reflect/protoreflect" 12 | protoimpl "google.golang.org/protobuf/runtime/protoimpl" 13 | reflect "reflect" 14 | sync "sync" 15 | ) 16 | 17 | const ( 18 | // Verify that this generated code is sufficiently up-to-date. 19 | _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) 20 | // Verify that runtime/protoimpl is sufficiently up-to-date. 21 | _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) 22 | ) 23 | 24 | // This is a compile-time assertion that a sufficiently up-to-date version 25 | // of the legacy proto package is being used. 26 | const _ = proto.ProtoPackageIsVersion4 27 | 28 | type Empty struct { 29 | state protoimpl.MessageState 30 | sizeCache protoimpl.SizeCache 31 | unknownFields protoimpl.UnknownFields 32 | } 33 | 34 | func (x *Empty) Reset() { 35 | *x = Empty{} 36 | if protoimpl.UnsafeEnabled { 37 | mi := &file_proto_ion_ion_proto_msgTypes[0] 38 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 39 | ms.StoreMessageInfo(mi) 40 | } 41 | } 42 | 43 | func (x *Empty) String() string { 44 | return protoimpl.X.MessageStringOf(x) 45 | } 46 | 47 | func (*Empty) ProtoMessage() {} 48 | 49 | func (x *Empty) ProtoReflect() protoreflect.Message { 50 | mi := &file_proto_ion_ion_proto_msgTypes[0] 51 | if protoimpl.UnsafeEnabled && x != nil { 52 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 53 | if ms.LoadMessageInfo() == nil { 54 | ms.StoreMessageInfo(mi) 55 | } 56 | return ms 57 | } 58 | return mi.MessageOf(x) 59 | } 60 | 61 | // Deprecated: Use Empty.ProtoReflect.Descriptor instead. 62 | func (*Empty) Descriptor() ([]byte, []int) { 63 | return file_proto_ion_ion_proto_rawDescGZIP(), []int{0} 64 | } 65 | 66 | type RPC struct { 67 | state protoimpl.MessageState 68 | sizeCache protoimpl.SizeCache 69 | unknownFields protoimpl.UnknownFields 70 | 71 | Protocol string `protobuf:"bytes,1,opt,name=protocol,proto3" json:"protocol,omitempty"` 72 | Addr string `protobuf:"bytes,2,opt,name=addr,proto3" json:"addr,omitempty"` 73 | Params map[string]string `protobuf:"bytes,3,rep,name=params,proto3" json:"params,omitempty" protobuf_key:"bytes,1,opt,name=key,proto3" protobuf_val:"bytes,2,opt,name=value,proto3"` 74 | } 75 | 76 | func (x *RPC) Reset() { 77 | *x = RPC{} 78 | if protoimpl.UnsafeEnabled { 79 | mi := &file_proto_ion_ion_proto_msgTypes[1] 80 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 81 | ms.StoreMessageInfo(mi) 82 | } 83 | } 84 | 85 | func (x *RPC) String() string { 86 | return protoimpl.X.MessageStringOf(x) 87 | } 88 | 89 | func (*RPC) ProtoMessage() {} 90 | 91 | func (x *RPC) ProtoReflect() protoreflect.Message { 92 | mi := &file_proto_ion_ion_proto_msgTypes[1] 93 | if protoimpl.UnsafeEnabled && x != nil { 94 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 95 | if ms.LoadMessageInfo() == nil { 96 | ms.StoreMessageInfo(mi) 97 | } 98 | return ms 99 | } 100 | return mi.MessageOf(x) 101 | } 102 | 103 | // Deprecated: Use RPC.ProtoReflect.Descriptor instead. 104 | func (*RPC) Descriptor() ([]byte, []int) { 105 | return file_proto_ion_ion_proto_rawDescGZIP(), []int{1} 106 | } 107 | 108 | func (x *RPC) GetProtocol() string { 109 | if x != nil { 110 | return x.Protocol 111 | } 112 | return "" 113 | } 114 | 115 | func (x *RPC) GetAddr() string { 116 | if x != nil { 117 | return x.Addr 118 | } 119 | return "" 120 | } 121 | 122 | func (x *RPC) GetParams() map[string]string { 123 | if x != nil { 124 | return x.Params 125 | } 126 | return nil 127 | } 128 | 129 | type Node struct { 130 | state protoimpl.MessageState 131 | sizeCache protoimpl.SizeCache 132 | unknownFields protoimpl.UnknownFields 133 | 134 | Dc string `protobuf:"bytes,1,opt,name=dc,proto3" json:"dc,omitempty"` 135 | Nid string `protobuf:"bytes,2,opt,name=nid,proto3" json:"nid,omitempty"` 136 | Service string `protobuf:"bytes,3,opt,name=service,proto3" json:"service,omitempty"` 137 | Rpc *RPC `protobuf:"bytes,4,opt,name=rpc,proto3" json:"rpc,omitempty"` 138 | } 139 | 140 | func (x *Node) Reset() { 141 | *x = Node{} 142 | if protoimpl.UnsafeEnabled { 143 | mi := &file_proto_ion_ion_proto_msgTypes[2] 144 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 145 | ms.StoreMessageInfo(mi) 146 | } 147 | } 148 | 149 | func (x *Node) String() string { 150 | return protoimpl.X.MessageStringOf(x) 151 | } 152 | 153 | func (*Node) ProtoMessage() {} 154 | 155 | func (x *Node) ProtoReflect() protoreflect.Message { 156 | mi := &file_proto_ion_ion_proto_msgTypes[2] 157 | if protoimpl.UnsafeEnabled && x != nil { 158 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 159 | if ms.LoadMessageInfo() == nil { 160 | ms.StoreMessageInfo(mi) 161 | } 162 | return ms 163 | } 164 | return mi.MessageOf(x) 165 | } 166 | 167 | // Deprecated: Use Node.ProtoReflect.Descriptor instead. 168 | func (*Node) Descriptor() ([]byte, []int) { 169 | return file_proto_ion_ion_proto_rawDescGZIP(), []int{2} 170 | } 171 | 172 | func (x *Node) GetDc() string { 173 | if x != nil { 174 | return x.Dc 175 | } 176 | return "" 177 | } 178 | 179 | func (x *Node) GetNid() string { 180 | if x != nil { 181 | return x.Nid 182 | } 183 | return "" 184 | } 185 | 186 | func (x *Node) GetService() string { 187 | if x != nil { 188 | return x.Service 189 | } 190 | return "" 191 | } 192 | 193 | func (x *Node) GetRpc() *RPC { 194 | if x != nil { 195 | return x.Rpc 196 | } 197 | return nil 198 | } 199 | 200 | var File_proto_ion_ion_proto protoreflect.FileDescriptor 201 | 202 | var file_proto_ion_ion_proto_rawDesc = []byte{ 203 | 0x0a, 0x13, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2f, 0x69, 0x6f, 0x6e, 0x2f, 0x69, 0x6f, 0x6e, 0x2e, 204 | 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x03, 0x69, 0x6f, 0x6e, 0x22, 0x07, 0x0a, 0x05, 0x45, 0x6d, 205 | 0x70, 0x74, 0x79, 0x22, 0x9e, 0x01, 0x0a, 0x03, 0x52, 0x50, 0x43, 0x12, 0x1a, 0x0a, 0x08, 0x70, 206 | 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x70, 207 | 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x12, 0x12, 0x0a, 0x04, 0x61, 0x64, 0x64, 0x72, 0x18, 208 | 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x61, 0x64, 0x64, 0x72, 0x12, 0x2c, 0x0a, 0x06, 0x70, 209 | 0x61, 0x72, 0x61, 0x6d, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x14, 0x2e, 0x69, 0x6f, 210 | 0x6e, 0x2e, 0x52, 0x50, 0x43, 0x2e, 0x50, 0x61, 0x72, 0x61, 0x6d, 0x73, 0x45, 0x6e, 0x74, 0x72, 211 | 0x79, 0x52, 0x06, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x73, 0x1a, 0x39, 0x0a, 0x0b, 0x50, 0x61, 0x72, 212 | 0x61, 0x6d, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 213 | 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 214 | 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 215 | 0x3a, 0x02, 0x38, 0x01, 0x22, 0x5e, 0x0a, 0x04, 0x4e, 0x6f, 0x64, 0x65, 0x12, 0x0e, 0x0a, 0x02, 216 | 0x64, 0x63, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x64, 0x63, 0x12, 0x10, 0x0a, 0x03, 217 | 0x6e, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6e, 0x69, 0x64, 0x12, 0x18, 218 | 0x0a, 0x07, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 219 | 0x07, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x1a, 0x0a, 0x03, 0x72, 0x70, 0x63, 0x18, 220 | 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x08, 0x2e, 0x69, 0x6f, 0x6e, 0x2e, 0x52, 0x50, 0x43, 0x52, 221 | 0x03, 0x72, 0x70, 0x63, 0x42, 0x1f, 0x5a, 0x1d, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 222 | 0x6f, 0x6d, 0x2f, 0x70, 0x69, 0x6f, 0x6e, 0x2f, 0x69, 0x6f, 0x6e, 0x2f, 0x70, 0x72, 0x6f, 0x74, 223 | 0x6f, 0x2f, 0x69, 0x6f, 0x6e, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, 224 | } 225 | 226 | var ( 227 | file_proto_ion_ion_proto_rawDescOnce sync.Once 228 | file_proto_ion_ion_proto_rawDescData = file_proto_ion_ion_proto_rawDesc 229 | ) 230 | 231 | func file_proto_ion_ion_proto_rawDescGZIP() []byte { 232 | file_proto_ion_ion_proto_rawDescOnce.Do(func() { 233 | file_proto_ion_ion_proto_rawDescData = protoimpl.X.CompressGZIP(file_proto_ion_ion_proto_rawDescData) 234 | }) 235 | return file_proto_ion_ion_proto_rawDescData 236 | } 237 | 238 | var file_proto_ion_ion_proto_msgTypes = make([]protoimpl.MessageInfo, 4) 239 | var file_proto_ion_ion_proto_goTypes = []interface{}{ 240 | (*Empty)(nil), // 0: ion.Empty 241 | (*RPC)(nil), // 1: ion.RPC 242 | (*Node)(nil), // 2: ion.Node 243 | nil, // 3: ion.RPC.ParamsEntry 244 | } 245 | var file_proto_ion_ion_proto_depIdxs = []int32{ 246 | 3, // 0: ion.RPC.params:type_name -> ion.RPC.ParamsEntry 247 | 1, // 1: ion.Node.rpc:type_name -> ion.RPC 248 | 2, // [2:2] is the sub-list for method output_type 249 | 2, // [2:2] is the sub-list for method input_type 250 | 2, // [2:2] is the sub-list for extension type_name 251 | 2, // [2:2] is the sub-list for extension extendee 252 | 0, // [0:2] is the sub-list for field type_name 253 | } 254 | 255 | func init() { file_proto_ion_ion_proto_init() } 256 | func file_proto_ion_ion_proto_init() { 257 | if File_proto_ion_ion_proto != nil { 258 | return 259 | } 260 | if !protoimpl.UnsafeEnabled { 261 | file_proto_ion_ion_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} { 262 | switch v := v.(*Empty); i { 263 | case 0: 264 | return &v.state 265 | case 1: 266 | return &v.sizeCache 267 | case 2: 268 | return &v.unknownFields 269 | default: 270 | return nil 271 | } 272 | } 273 | file_proto_ion_ion_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} { 274 | switch v := v.(*RPC); i { 275 | case 0: 276 | return &v.state 277 | case 1: 278 | return &v.sizeCache 279 | case 2: 280 | return &v.unknownFields 281 | default: 282 | return nil 283 | } 284 | } 285 | file_proto_ion_ion_proto_msgTypes[2].Exporter = func(v interface{}, i int) interface{} { 286 | switch v := v.(*Node); i { 287 | case 0: 288 | return &v.state 289 | case 1: 290 | return &v.sizeCache 291 | case 2: 292 | return &v.unknownFields 293 | default: 294 | return nil 295 | } 296 | } 297 | } 298 | type x struct{} 299 | out := protoimpl.TypeBuilder{ 300 | File: protoimpl.DescBuilder{ 301 | GoPackagePath: reflect.TypeOf(x{}).PkgPath(), 302 | RawDescriptor: file_proto_ion_ion_proto_rawDesc, 303 | NumEnums: 0, 304 | NumMessages: 4, 305 | NumExtensions: 0, 306 | NumServices: 0, 307 | }, 308 | GoTypes: file_proto_ion_ion_proto_goTypes, 309 | DependencyIndexes: file_proto_ion_ion_proto_depIdxs, 310 | MessageInfos: file_proto_ion_ion_proto_msgTypes, 311 | }.Build() 312 | File_proto_ion_ion_proto = out.File 313 | file_proto_ion_ion_proto_rawDesc = nil 314 | file_proto_ion_ion_proto_goTypes = nil 315 | file_proto_ion_ion_proto_depIdxs = nil 316 | } 317 | --------------------------------------------------------------------------------