commit 4318395ad02fd7e3a8113d39707ee02ea863fd88 Author: Charles Iliya Krempeaux Date: Wed Jul 26 14:15:39 2023 -0700 initial commit - coding by Benyamin Azarkhazin (and not me, even though I am committing it) diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..2f0f515 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,3 @@ +.dockerignore +.gitignore +Dockerfile \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..6591483 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +.idea +goldgorilla diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..e69de29 diff --git a/README.md b/README.md new file mode 100644 index 0000000..568e24e --- /dev/null +++ b/README.md @@ -0,0 +1,19 @@ +# GoldGorilla + +**GoldGorilla** is bespoke **Selective Forwarding Unit** (**SFU**) designed to work with **GreatApe**. + +## Team + +The following is a list of the people who are actively working on GoldGorilla (in alphabetical order): + +| Name | Role | Online | +|--------------------------|----------------------------|------------------------------------------------------------------------------------| +| Atanaz Ostovar | qa | | +| Benyamin Azarkhazin | engineering (**primary**) | | +| Charles Iliya Krempeaux | product, engineering | [🐘](https://mastodon.social/@reiver) [πŸ•ΈοΈ](http://changelog.ca/) | +| Chris Trottier | community, marketing | [🐘](https://calckey.social/@atomicpoet) [πŸ“·](https://peerverse.space/atomicpoet) | +| Massoud Seifi | engineering | [🐘](https://mastodon.social/@accesstoken) | +| Mehrdad Mirsamie | engineering | [🐘](https://mastodon.social/@mmcomp) | +| Sepideh Farsi | qa | | + +🦍 diff --git a/app.go b/app.go new file mode 100644 index 0000000..e0fac51 --- /dev/null +++ b/app.go @@ -0,0 +1,62 @@ +package main + +import ( + "bytes" + "encoding/json" + "sourcecode.social/greatape/goldgorilla/controllers" + "sourcecode.social/greatape/goldgorilla/models" + "sourcecode.social/greatape/goldgorilla/repositories" + "sourcecode.social/greatape/goldgorilla/routers" + "io" + "net/http" + "time" +) + +type App struct { + conf *models.ConfigModel + router *routers.Router + src string +} + +func (a *App) Init(srcListenAddr string, logjamBaseUrl string, targetRoom string) { + a.src = srcListenAddr + a.conf = &models.ConfigModel{ + LogjamBaseUrl: logjamBaseUrl + "/auxiliary-node", + TargetRoom: targetRoom, + } + roomRepo := repositories.NewRoomRepository(a.conf) + a.router = &routers.Router{} + respHelper := controllers.NewResponseHelper() + roomCtrl := controllers.NewRoomController(respHelper, roomRepo, a.conf) + + err := a.router.RegisterRoutes(roomCtrl) + panicIfErr(err) +} + +func (a *App) Run() { + go func() { + start: + buffer, _ := json.Marshal(map[string]any{"roomId": a.conf.TargetRoom}) + body := bytes.NewReader(buffer) + res, err := http.Post(a.conf.LogjamBaseUrl+"/join", "application/json", body) + if err != nil { + println(err.Error()) + time.Sleep(4 * time.Second) + goto start + } + if res.StatusCode > 204 { + resbody, _ := io.ReadAll(res.Body) + println("get /join "+res.Status, string(resbody)) + time.Sleep(4 * time.Second) + goto start + } + }() + err := a.router.Serve(a.src) + panicIfErr(err) +} + +func panicIfErr(err error) { + if err != nil { + panic(err) + } +} diff --git a/controllers/helpers.go b/controllers/helpers.go new file mode 100644 index 0000000..0df7bd8 --- /dev/null +++ b/controllers/helpers.go @@ -0,0 +1,70 @@ +package controllers + +import ( + "github.com/gin-gonic/gin" + "sourcecode.social/greatape/goldgorilla/models" +) + +type ResponseHelper struct { +} + +func NewResponseHelper() *ResponseHelper { + return &ResponseHelper{} +} + +func (r *ResponseHelper) Response(ctx *gin.Context, body any, code int) { + if body == nil { + body = struct{}{} + } + ctx.JSON(code, body) +} + +func (r *ResponseHelper) ResponseError(ctx *gin.Context, errObj any, code *int) { + errCode := 500 + if code != nil { + errCode = *code + } else if bigErr, isBigErr := errObj.(models.BigError); isBigErr { + errCode = bigErr.ErrCode() + } + + var errResp models.MessageResponse + if err, isError := errObj.(error); isError { + errResp.Message = err.Error() + } else if err, isMessage := errObj.(models.MessageResponse); isMessage { + errResp = err + } else { + errResp.Message = "unhandled error, contact support" + } + ctx.JSON(errCode, errResp) +} + +func (r *ResponseHelper) ResponseBadReq(ctx *gin.Context) { + st := 400 + r.ResponseError(ctx, models.MessageResponse{Message: "bad request, invalid input"}, &st) +} + +func (r *ResponseHelper) ResponseUnprocessableEntity(ctx *gin.Context) { + st := 422 + r.ResponseError(ctx, models.MessageResponse{Message: "unprocessable entity, invalid input"}, &st) +} + +func (r *ResponseHelper) HandleIfErr(ctx *gin.Context, err error, status *int) bool { + if err == nil { + return false + } + var statusCode = 500 + if bigErr, extends := err.(models.BigError); extends { + if status != nil { + statusCode = *status + } else { + statusCode = bigErr.ErrCode() + } + r.ResponseError(ctx, models.MessageResponse{Message: bigErr.Error()}, &statusCode) + } else { + if status != nil { + statusCode = *status + } + r.ResponseError(ctx, models.MessageResponse{Message: err.Error()}, &statusCode) + } + return true +} diff --git a/controllers/room.go b/controllers/room.go new file mode 100644 index 0000000..e5f7c25 --- /dev/null +++ b/controllers/room.go @@ -0,0 +1,156 @@ +package controllers + +import ( + "bytes" + "encoding/json" + "github.com/gin-gonic/gin" + "sourcecode.social/greatape/goldgorilla/models" + "sourcecode.social/greatape/goldgorilla/models/dto" + "sourcecode.social/greatape/goldgorilla/repositories" + "net/http" +) + +type RoomController struct { + helper *ResponseHelper + repo *repositories.RoomRepository + conf *models.ConfigModel +} + +func NewRoomController(respHelper *ResponseHelper, repo *repositories.RoomRepository, conf *models.ConfigModel) *RoomController { + return &RoomController{ + helper: respHelper, + repo: repo, + conf: conf, + } +} + +func (c *RoomController) CreatePeer(ctx *gin.Context) { + var reqModel dto.CreatePeerReqModel + badReqSt := 400 + if err := ctx.ShouldBindJSON(&reqModel); c.helper.HandleIfErr(ctx, err, &badReqSt) { + return + } + if !reqModel.Validate() { + c.helper.ResponseUnprocessableEntity(ctx) + return + } + offer, err := c.repo.CreatePeer(reqModel.RoomId, reqModel.ID, reqModel.CanPublish, reqModel.IsCaller) + if c.helper.HandleIfErr(ctx, err, nil) { + return + } + c.helper.Response(ctx, struct{}{}, http.StatusNoContent) + + if offer != nil { + buffer, err := json.Marshal(dto.SetSDPReqModel{ + PeerDTO: dto.PeerDTO{ + RoomId: reqModel.RoomId, + ID: reqModel.ID, + }, + SDP: *offer, + }) + if err != nil { + println(err.Error()) + return + } + reader := bytes.NewReader(buffer) + resp, err := http.Post(c.conf.LogjamBaseUrl+"/offer", "application/json", reader) + if err != nil { + println(err.Error()) + return + } + if resp.StatusCode > 204 { + println(resp.Status) + } + } +} + +func (c *RoomController) AddICECandidate(ctx *gin.Context) { + var reqModel dto.AddPeerICECandidateReqModel + badReqSt := 400 + if err := ctx.ShouldBindJSON(&reqModel); c.helper.HandleIfErr(ctx, err, &badReqSt) { + return + } + if !reqModel.Validate() { + c.helper.ResponseUnprocessableEntity(ctx) + return + } + err := c.repo.AddPeerIceCandidate(reqModel.RoomId, reqModel.ID, reqModel.ICECandidate) + if c.helper.HandleIfErr(ctx, err, nil) { + return + } + c.helper.Response(ctx, struct{}{}, http.StatusNoContent) +} + +func (c *RoomController) Offer(ctx *gin.Context) { + var reqModel dto.SetSDPReqModel + badReqSt := 400 + if err := ctx.ShouldBindJSON(&reqModel); c.helper.HandleIfErr(ctx, err, &badReqSt) { + return + } + if !reqModel.Validate() { + c.helper.ResponseUnprocessableEntity(ctx) + return + } + answer, err := c.repo.SetPeerOffer(reqModel.RoomId, reqModel.ID, reqModel.SDP) + if c.helper.HandleIfErr(ctx, err, nil) { + println(err.Error()) + return + } + c.helper.Response(ctx, struct{}{}, http.StatusNoContent) + { + buffer, err := json.Marshal(dto.SetSDPReqModel{ + PeerDTO: dto.PeerDTO{ + RoomId: reqModel.RoomId, + ID: reqModel.ID, + }, + SDP: *answer, + }) + if err != nil { + println(err.Error()) + return + } + reader := bytes.NewReader(buffer) + resp, err := http.Post(c.conf.LogjamBaseUrl+"/answer", "application/json", reader) + if err != nil { + println(err.Error()) + return + } + if resp.StatusCode > 204 { + println(resp.Status) + } + } +} + +func (c *RoomController) Answer(ctx *gin.Context) { + var reqModel dto.SetSDPReqModel + badReqSt := 400 + if err := ctx.ShouldBindJSON(&reqModel); c.helper.HandleIfErr(ctx, err, &badReqSt) { + return + } + if !reqModel.Validate() { + c.helper.ResponseUnprocessableEntity(ctx) + return + } + err := c.repo.SetPeerAnswer(reqModel.RoomId, reqModel.ID, reqModel.SDP) + if c.helper.HandleIfErr(ctx, err, nil) { + return + } + c.helper.Response(ctx, struct{}{}, http.StatusNoContent) +} + +func (c *RoomController) ClosePeer(ctx *gin.Context) { + var reqModel dto.PeerDTO + badReqSt := 400 + if err := ctx.ShouldBindJSON(&reqModel); c.helper.HandleIfErr(ctx, err, &badReqSt) { + return + } + if !reqModel.Validate() { + c.helper.ResponseUnprocessableEntity(ctx) + return + } + err := c.repo.ClosePeer(reqModel.RoomId, reqModel.ID) + if c.helper.HandleIfErr(ctx, err, nil) { + return + } + c.helper.Response(ctx, struct{}{}, http.StatusNoContent) +} diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..e69de29 diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..c4ac04a --- /dev/null +++ b/go.mod @@ -0,0 +1,54 @@ +module sourcecode.social/greatape/goldgorilla + +go 1.20 + +require ( + github.com/gin-gonic/gin v1.9.1 + github.com/pion/rtcp v1.2.10 + github.com/pion/webrtc/v3 v3.2.12 +) + +require ( + github.com/bytedance/sonic v1.9.1 // indirect + github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 // indirect + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/gabriel-vasile/mimetype v1.4.2 // indirect + github.com/gin-contrib/sse v0.1.0 // indirect + github.com/go-playground/locales v0.14.1 // indirect + github.com/go-playground/universal-translator v0.18.1 // indirect + github.com/go-playground/validator/v10 v10.14.0 // indirect + github.com/goccy/go-json v0.10.2 // indirect + github.com/google/uuid v1.3.0 // indirect + github.com/json-iterator/go v1.1.12 // indirect + github.com/klauspost/cpuid/v2 v2.2.4 // indirect + github.com/leodido/go-urn v1.2.4 // indirect + github.com/mattn/go-isatty v0.0.19 // indirect + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect + github.com/modern-go/reflect2 v1.0.2 // indirect + github.com/pelletier/go-toml/v2 v2.0.8 // indirect + github.com/pion/datachannel v1.5.5 // indirect + github.com/pion/dtls/v2 v2.2.7 // indirect + github.com/pion/ice/v2 v2.3.9 // indirect + github.com/pion/interceptor v0.1.17 // indirect + github.com/pion/logging v0.2.2 // indirect + github.com/pion/mdns v0.0.7 // indirect + github.com/pion/randutil v0.1.0 // indirect + github.com/pion/rtp v1.7.13 // indirect + github.com/pion/sctp v1.8.7 // indirect + github.com/pion/sdp/v3 v3.0.6 // indirect + github.com/pion/srtp/v2 v2.0.15 // indirect + github.com/pion/stun v0.6.1 // indirect + github.com/pion/transport/v2 v2.2.1 // indirect + github.com/pion/turn/v2 v2.1.2 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/stretchr/testify v1.8.4 // indirect + github.com/twitchyliquid64/golang-asm v0.15.1 // indirect + github.com/ugorji/go/codec v1.2.11 // indirect + golang.org/x/arch v0.3.0 // indirect + golang.org/x/crypto v0.10.0 // indirect + golang.org/x/net v0.11.0 // indirect + golang.org/x/sys v0.9.0 // indirect + golang.org/x/text v0.10.0 // indirect + google.golang.org/protobuf v1.30.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..1abd186 --- /dev/null +++ b/go.sum @@ -0,0 +1,245 @@ +github.com/bytedance/sonic v1.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1O2AihPM= +github.com/bytedance/sonic v1.9.1 h1:6iJ6NqdoxCDr6mbY8h18oSO+cShGSMRGCEo7F2h0x8s= +github.com/bytedance/sonic v1.9.1/go.mod h1:i736AoUSYt75HyZLoJW9ERYxcy6eaN6h4BZXU064P/U= +github.com/chenzhuoyu/base64x v0.0.0-20211019084208-fb5309c8db06/go.mod h1:DH46F32mSOjUmXrMHnKwZdA8wcEefY7UVqBKYGjpdQY= +github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 h1:qSGYFH7+jGhDF8vLC+iwCD4WpbV1EBDSzWkJODFLams= +github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311/go.mod h1:b583jCggY9gE99b6G5LEC39OIiVsWj+R97kbl5odCEk= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= +github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= +github.com/gabriel-vasile/mimetype v1.4.2 h1:w5qFW6JKBz9Y393Y4q372O9A7cUSequkh1Q7OhCmWKU= +github.com/gabriel-vasile/mimetype v1.4.2/go.mod h1:zApsH/mKG4w07erKIaJPFiX0Tsq9BFQgN3qGY5GnNgA= +github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= +github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= +github.com/gin-gonic/gin v1.9.1 h1:4idEAncQnU5cB7BeOkPtxjfCSye0AAm1R0RVIqJ+Jmg= +github.com/gin-gonic/gin v1.9.1/go.mod h1:hPrL7YrpYKXt5YId3A/Tnip5kqbEAP+KLuI3SUcPTeU= +github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= +github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= +github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= +github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= +github.com/go-playground/validator/v10 v10.14.0 h1:vgvQWe3XCz3gIeFDm/HnTIbj6UGmg/+t63MyGU2n5js= +github.com/go-playground/validator/v10 v10.14.0/go.mod h1:9iXMNT7sEkjXb0I+enO7QXmzG6QCsPWY4zveKFVRSyU= +github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE= +github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU= +github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= +github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= +github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= +github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= +github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= +github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= +github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= +github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= +github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= +github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= +github.com/klauspost/cpuid/v2 v2.2.4 h1:acbojRNwl3o09bUq+yDCtZFc1aiwaAAxtcn8YkZXnvk= +github.com/klauspost/cpuid/v2 v2.2.4/go.mod h1:RVVoqg1df56z8g3pUjL/3lE5UfnlrJX8tyFgg4nqhuY= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/leodido/go-urn v1.2.4 h1:XlAE/cm/ms7TE/VMVoduSpNBoyc2dOxHs5MZSwAN63Q= +github.com/leodido/go-urn v1.2.4/go.mod h1:7ZrI8mTSeBSHl/UaRyKQW1qZeMgak41ANeCNaVckg+4= +github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA= +github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= +github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= +github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU= +github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= +github.com/onsi/ginkgo v1.16.4/go.mod h1:dX+/inL/fNMqNlz0e9LfyB9TswhZpCVdJM/Z6Vvnwo0= +github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU= +github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= +github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= +github.com/onsi/gomega v1.17.0/go.mod h1:HnhC7FXeEQY45zxNK3PPoIUhzk/80Xly9PcubAlGdZY= +github.com/pelletier/go-toml/v2 v2.0.8 h1:0ctb6s9mE31h0/lhu+J6OPmVeDxJn+kYnJc2jZR9tGQ= +github.com/pelletier/go-toml/v2 v2.0.8/go.mod h1:vuYfssBdrU2XDZ9bYydBu6t+6a6PYNcZljzZR9VXg+4= +github.com/pion/datachannel v1.5.5 h1:10ef4kwdjije+M9d7Xm9im2Y3O6A6ccQb0zcqZcJew8= +github.com/pion/datachannel v1.5.5/go.mod h1:iMz+lECmfdCMqFRhXhcA/219B0SQlbpoR2V118yimL0= +github.com/pion/dtls/v2 v2.2.7 h1:cSUBsETxepsCSFSxC3mc/aDo14qQLMSL+O6IjG28yV8= +github.com/pion/dtls/v2 v2.2.7/go.mod h1:8WiMkebSHFD0T+dIU+UeBaoV7kDhOW5oDCzZ7WZ/F9s= +github.com/pion/ice/v2 v2.3.9 h1:7yZpHf3PhPxJGT4JkMj1Y8Rl5cQ6fB709iz99aeMd/U= +github.com/pion/ice/v2 v2.3.9/go.mod h1:lT3kv5uUIlHfXHU/ZRD7uKD/ufM202+eTa3C/umgGf4= +github.com/pion/interceptor v0.1.17 h1:prJtgwFh/gB8zMqGZoOgJPHivOwVAp61i2aG61Du/1w= +github.com/pion/interceptor v0.1.17/go.mod h1:SY8kpmfVBvrbUzvj2bsXz7OJt5JvmVNZ+4Kjq7FcwrI= +github.com/pion/logging v0.2.2 h1:M9+AIj/+pxNsDfAT64+MAVgJO0rsyLnoJKCqf//DoeY= +github.com/pion/logging v0.2.2/go.mod h1:k0/tDVsRCX2Mb2ZEmTqNa7CWsQPc+YYCB7Q+5pahoms= +github.com/pion/mdns v0.0.7 h1:P0UB4Sr6xDWEox0kTVxF0LmQihtCbSAdW0H2nEgkA3U= +github.com/pion/mdns v0.0.7/go.mod h1:4iP2UbeFhLI/vWju/bw6ZfwjJzk0z8DNValjGxR/dD8= +github.com/pion/randutil v0.1.0 h1:CFG1UdESneORglEsnimhUjf33Rwjubwj6xfiOXBa3mA= +github.com/pion/randutil v0.1.0/go.mod h1:XcJrSMMbbMRhASFVOlj/5hQial/Y8oH/HVo7TBZq+j8= +github.com/pion/rtcp v1.2.10 h1:nkr3uj+8Sp97zyItdN60tE/S6vk4al5CPRR6Gejsdjc= +github.com/pion/rtcp v1.2.10/go.mod h1:ztfEwXZNLGyF1oQDttz/ZKIBaeeg/oWbRYqzBM9TL1I= +github.com/pion/rtp v1.7.13 h1:qcHwlmtiI50t1XivvoawdCGTP4Uiypzfrsap+bijcoA= +github.com/pion/rtp v1.7.13/go.mod h1:bDb5n+BFZxXx0Ea7E5qe+klMuqiBrP+w8XSjiWtCUko= +github.com/pion/sctp v1.8.5/go.mod h1:SUFFfDpViyKejTAdwD1d/HQsCu+V/40cCs2nZIvC3s0= +github.com/pion/sctp v1.8.7 h1:JnABvFakZueGAn4KU/4PSKg+GWbF6QWbKTWZOSGJjXw= +github.com/pion/sctp v1.8.7/go.mod h1:g1Ul+ARqZq5JEmoFy87Q/4CePtKnTJ1QCL9dBBdN6AU= +github.com/pion/sdp/v3 v3.0.6 h1:WuDLhtuFUUVpTfus9ILC4HRyHsW6TdugjEX/QY9OiUw= +github.com/pion/sdp/v3 v3.0.6/go.mod h1:iiFWFpQO8Fy3S5ldclBkpXqmWy02ns78NOKoLLL0YQw= +github.com/pion/srtp/v2 v2.0.15 h1:+tqRtXGsGwHC0G0IUIAzRmdkHvriF79IHVfZGfHrQoA= +github.com/pion/srtp/v2 v2.0.15/go.mod h1:b/pQOlDrbB0HEH5EUAQXzSYxikFbNcNuKmF8tM0hCtw= +github.com/pion/stun v0.6.1 h1:8lp6YejULeHBF8NmV8e2787BogQhduZugh5PdhDyyN4= +github.com/pion/stun v0.6.1/go.mod h1:/hO7APkX4hZKu/D0f2lHzNyvdkTGtIy3NDmLR7kSz/8= +github.com/pion/transport v0.14.1 h1:XSM6olwW+o8J4SCmOBb/BpwZypkHeyM0PGFCxNQBr40= +github.com/pion/transport v0.14.1/go.mod h1:4tGmbk00NeYA3rUa9+n+dzCCoKkcy3YlYb99Jn2fNnI= +github.com/pion/transport/v2 v2.0.0/go.mod h1:HS2MEBJTwD+1ZI2eSXSvHJx/HnzQqRy2/LXxt6eVMHc= +github.com/pion/transport/v2 v2.1.0/go.mod h1:AdSw4YBZVDkZm8fpoz+fclXyQwANWmZAlDuQdctTThQ= +github.com/pion/transport/v2 v2.2.0/go.mod h1:AdSw4YBZVDkZm8fpoz+fclXyQwANWmZAlDuQdctTThQ= +github.com/pion/transport/v2 v2.2.1 h1:7qYnCBlpgSJNYMbLCKuSY9KbQdBFoETvPNETv0y4N7c= +github.com/pion/transport/v2 v2.2.1/go.mod h1:cXXWavvCnFF6McHTft3DWS9iic2Mftcz1Aq29pGcU5g= +github.com/pion/turn/v2 v2.1.2 h1:wj0cAoGKltaZ790XEGW9HwoUewqjliwmhtxCuB2ApyM= +github.com/pion/turn/v2 v2.1.2/go.mod h1:1kjnPkBcex3dhCU2Am+AAmxDcGhLX3WnMfmkNpvSTQU= +github.com/pion/webrtc/v3 v3.2.12 h1:pVqz5NdtTqyhKIhMcXR8bPp709kCf9blyAhDjoVRLvA= +github.com/pion/webrtc/v3 v3.2.12/go.mod h1:/Oz6K95CGWaN+3No+Z0NYvgOPOr3aY8UyTlMm/dec3A= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/sclevine/agouti v3.0.0+incompatible/go.mod h1:b4WX9W9L1sfQKXeJf1mUTLZKJ48R1S7H23Ji7oFO5Bw= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= +github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= +github.com/ugorji/go/codec v1.2.11 h1:BMaWp1Bb6fHwEtbplGBGJ498wD+LKlNSl25MjdZY4dU= +github.com/ugorji/go/codec v1.2.11/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= +golang.org/x/arch v0.3.0 h1:02VY4/ZcO/gBOH6PUaoiptASxtXU10jazRCP865E97k= +golang.org/x/arch v0.3.0/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.8.0/go.mod h1:mRqEX+O9/h5TFCrQhkgjo2yKi0yYA+9ecGkdQoHrywE= +golang.org/x/crypto v0.9.0 h1:LF6fAI+IutBocDJ2OT0Q1g8plpYljMZ4+lty+dsqw3g= +golang.org/x/crypto v0.9.0/go.mod h1:yrmDGqONDYtNj3tH8X9dzUun2m2lzPa9ngI6/RUPGR0= +golang.org/x/crypto v0.10.0 h1:LKqV2xt9+kDzSTfOhx4FrkEBcMrAgHSYgzywV9zcGmM= +golang.org/x/crypto v0.10.0/go.mod h1:o4eNf7Ede1fv+hwOwZsTHl9EsPFO6q6ZvYR8vYfY45I= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco= +golang.org/x/net v0.5.0/go.mod h1:DivGGAXEgPSlEBzxGzZI+ZLohi+xUj054jfeKui00ws= +golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= +golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc= +golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns= +golang.org/x/net v0.10.0 h1:X2//UzNDwYmtCLn7To6G58Wr6f5ahEAQgKNzv9Y951M= +golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= +golang.org/x/net v0.11.0 h1:Gi2tvZIJyBtO9SDr1q9h5hEQCp/4L2RQ+ar0qjx2oNU= +golang.org/x/net v0.11.0/go.mod h1:2L/ixqYpgIVXmeoSA/4Lu7BzTG4KIyPIryS4IsOd1oQ= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220704084225-05e143d24a9e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.4.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.8.0 h1:EBmGv8NaZBZTWvrbjNoL6HVt+IVy3QDQpJs7VRIw3tU= +golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.9.0 h1:KS/R3tvhPqvJvwcKfnBHJwwthS11LRhmM5D59eEXa0s= +golang.org/x/sys v0.9.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.4.0/go.mod h1:9P2UbLfCdcvo3p/nzKvsmas4TnlujnuoV9hGgYzW1lQ= +golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= +golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U= +golang.org/x/term v0.7.0/go.mod h1:P32HKFT3hSsZrRxla30E9HqToFYAQPCMs/zFMBUFqPY= +golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= +golang.org/x/term v0.9.0/go.mod h1:M6DEAAIenWoTxdKrOltXcmDY3rSplQUkrvaDU5FcQyo= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.6.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= +golang.org/x/text v0.9.0 h1:2sjJmO8cDvYveuX97RDLsxlyUxLl+GHoLxBiRdHllBE= +golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= +golang.org/x/text v0.10.0 h1:UpjohKhiEgNc0CSauXmwYftY1+LlaC75SJwh0SgCX58= +golang.org/x/text v0.10.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= +google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= +google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= +google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= +google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= +google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= +google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= +google.golang.org/protobuf v1.30.0 h1:kPPoIgf3TsEvrm0PFe15JQ+570QVxYzEvvHqChK+cng= +google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4= diff --git a/main.go b/main.go new file mode 100644 index 0000000..b45e14d --- /dev/null +++ b/main.go @@ -0,0 +1,21 @@ +package main + +import ( + "flag" + "strings" +) + +func main() { + src := flag.String("src", ":8080", "listenhost:listenPort") + logjamBaseUrl := flag.String("logjam-base-url", "https://example.com", "logjam base url(shouldn't end with /)") + targetRoom := flag.String("targetRoom", "testyroom", "target room") + + flag.Parse() + + if strings.HasSuffix(*logjamBaseUrl, "/") { + panic("logjam-base-url shouldn't end with /") + } + app := App{} + app.Init(*src, *logjamBaseUrl, *targetRoom) + app.Run() +} diff --git a/models/common.go b/models/common.go new file mode 100644 index 0000000..2c6a532 --- /dev/null +++ b/models/common.go @@ -0,0 +1,5 @@ +package models + +type MessageResponse struct { + Message string `json:"message"` +} diff --git a/models/config.go b/models/config.go new file mode 100644 index 0000000..d687881 --- /dev/null +++ b/models/config.go @@ -0,0 +1,6 @@ +package models + +type ConfigModel struct { + LogjamBaseUrl string `json:"logjamBaseUrl"` + TargetRoom string `json:"targetRoom"` +} diff --git a/models/dto/room.go b/models/dto/room.go new file mode 100644 index 0000000..0f2f676 --- /dev/null +++ b/models/dto/room.go @@ -0,0 +1,36 @@ +package dto + +import "github.com/pion/webrtc/v3" + +type PeerDTO struct { + RoomId string `json:"roomId"` + ID uint64 `json:"id"` +} + +func (model *PeerDTO) Validate() bool { + return len(model.RoomId) > 0 +} + +type CreatePeerReqModel struct { + PeerDTO + CanPublish bool `json:"canPublish"` + IsCaller bool `json:"isCaller"` +} + +type AddPeerICECandidateReqModel struct { + PeerDTO + ICECandidate webrtc.ICECandidateInit `json:"iceCandidate"` +} + +func (model *AddPeerICECandidateReqModel) Validate() bool { + return model.PeerDTO.Validate() // && len(model.ICECandidate.Candidate) > 0 +} + +type SetSDPReqModel struct { + PeerDTO + SDP webrtc.SessionDescription `json:"sdp"` +} + +func (model *SetSDPReqModel) Validate() bool { + return model.PeerDTO.Validate() && len(model.SDP.SDP) > 0 +} diff --git a/models/error.go b/models/error.go new file mode 100644 index 0000000..497172a --- /dev/null +++ b/models/error.go @@ -0,0 +1,42 @@ +package models + +type BigError struct { + message string `json:"message"` + errCode int `json:"-"` + metaData any `json:"meta"` +} + +func (err BigError) Error() string { + return err.message +} + +func (err BigError) ErrCode() int { + return err.errCode +} + +func (err BigError) Meta() any { + return err.metaData +} + +func NewError(msg string, errCode int, meta any) BigError { + if meta == nil { + meta = struct { + }{} + } + return BigError{ + message: msg, + errCode: errCode, + metaData: meta, + } +} + +func (BigError) FromErr(err error, errCode int, meta any) BigError { + if meta == nil { + meta = err + } + return BigError{ + message: err.Error(), + errCode: errCode, + metaData: meta, + } +} diff --git a/repositories/room.go b/repositories/room.go new file mode 100644 index 0000000..68db5b2 --- /dev/null +++ b/repositories/room.go @@ -0,0 +1,412 @@ +package repositories + +import ( + "bytes" + "encoding/json" + "fmt" + "github.com/pion/rtcp" + "github.com/pion/webrtc/v3" + "sourcecode.social/greatape/goldgorilla/models" + "sourcecode.social/greatape/goldgorilla/models/dto" + "log" + "net/http" + "sync" + "time" +) + +type Track struct { + OwnerId uint64 + TrackLocal *webrtc.TrackLocalStaticRTP +} + +type Peer struct { + ID uint64 + Conn *webrtc.PeerConnection + CanPublish bool +} + +type Room struct { + *sync.Mutex + Peers map[uint64]*Peer + trackLock *sync.Mutex + Tracks map[string]*Track +} + +type RoomRepository struct { + Rooms map[string]*Room + conf *models.ConfigModel + *sync.Mutex +} + +func NewRoomRepository(conf *models.ConfigModel) *RoomRepository { + return &RoomRepository{ + Mutex: &sync.Mutex{}, + Rooms: make(map[string]*Room), + conf: conf, + } +} + +func (r *RoomRepository) doesRoomExists(id string) bool { + if _, exists := r.Rooms[id]; exists { + return true + } + return false +} + +func (r *RoomRepository) doesPeerExists(roomId string, id uint64) bool { + if !r.doesRoomExists(roomId) { + return false + } + if _, exists := r.Rooms[roomId].Peers[id]; exists { + return true + } + + return false +} + +func (r *RoomRepository) CreatePeer(roomId string, id uint64, canPublish, isCaller bool) (*webrtc.SessionDescription, error) { + r.Lock() + + if !r.doesRoomExists(roomId) { + room := &Room{ + Mutex: &sync.Mutex{}, + Peers: make(map[uint64]*Peer), + trackLock: &sync.Mutex{}, + Tracks: make(map[string]*Track), + } + r.Rooms[roomId] = room + go func() { + for range time.NewTicker(3 * time.Second).C { + room.Lock() + for _, peer := range room.Peers { + for _, receiver := range peer.Conn.GetReceivers() { + if receiver.Track() == nil { + continue + } + + go peer.Conn.WriteRTCP([]rtcp.Packet{ + &rtcp.PictureLossIndication{ + MediaSSRC: uint32(receiver.Track().SSRC()), + }, + }) + } + + } + room.Unlock() + } + }() + } + + r.Unlock() + room := r.Rooms[roomId] + room.Lock() + defer room.Unlock() + + peerConn, err := webrtc.NewPeerConnection(webrtc.Configuration{}) + if err != nil { + return nil, models.NewError("can't create peer connection", 500, models.MessageResponse{Message: err.Error()}) + } + for _, typ := range []webrtc.RTPCodecType{webrtc.RTPCodecTypeVideo, webrtc.RTPCodecTypeAudio} { + if _, err := peerConn.AddTransceiverFromKind(typ, webrtc.RTPTransceiverInit{ + Direction: webrtc.RTPTransceiverDirectionSendrecv, + }); err != nil { + return nil, models.NewError("unhandled error, contact support #1313", 500, err) + } + } + + peerConn.OnICECandidate(func(ic *webrtc.ICECandidate) { + r.onPeerICECandidate(roomId, id, ic) + }) + peerConn.OnConnectionStateChange(func(state webrtc.PeerConnectionState) { + r.onPeerConnectionStateChange(roomId, id, state) + }) + peerConn.OnTrack(func(remote *webrtc.TrackRemote, receiver *webrtc.RTPReceiver) { + r.onPeerTrack(roomId, id, remote, receiver) + }) + + var sdp *webrtc.SessionDescription + if !isCaller { + offer, err := peerConn.CreateOffer(nil) + if err != nil { + println(err.Error()) + peerConn.Close() + return nil, models.NewError("unhandled error, contact support #1314", 500, err) + } + err = peerConn.SetLocalDescription(offer) + if err != nil { + println(err.Error()) + peerConn.Close() + return nil, models.NewError("unhandled error, contact support #1315", 500, err) + } + sdp = &offer + } + + room.Peers[id] = &Peer{ + ID: id, + Conn: peerConn, + } + + return sdp, nil +} + +func (r *RoomRepository) onPeerICECandidate(roomId string, id uint64, ic *webrtc.ICECandidate) { + if ic == nil { + return + } + reqModel := dto.AddPeerICECandidateReqModel{ + PeerDTO: dto.PeerDTO{ + RoomId: roomId, + ID: id, + }, + ICECandidate: ic.ToJSON(), + } + serializedReqBody, err := json.Marshal(reqModel) + if err != nil { + println(err.Error()) + return + } + resp, err := http.Post(r.conf.LogjamBaseUrl+"/ice", "application/json", bytes.NewReader(serializedReqBody)) + if err != nil { + println(err.Error()) + return + } + if resp.StatusCode > 204 { + println("POST /ice", resp.StatusCode) + return + } +} + +func (r *RoomRepository) onPeerConnectionStateChange(roomId string, id uint64, newState webrtc.PeerConnectionState) { + r.Lock() + + if !r.doesRoomExists(roomId) { + r.Unlock() + return + } + r.Unlock() + room := r.Rooms[roomId] + room.Lock() + defer room.Unlock() + + switch newState { + case webrtc.PeerConnectionStateFailed: + if err := room.Peers[id].Conn.Close(); err != nil { + log.Print(err) + } + case webrtc.PeerConnectionStateClosed: + delete(room.Peers, id) + } +} + +func (r *RoomRepository) onPeerTrack(roomId string, id uint64, remote *webrtc.TrackRemote, receiver *webrtc.RTPReceiver) { + fmt.Println("got a track!", remote.ID(), remote.StreamID(), remote.Kind().String()) + r.Lock() + if !r.doesRoomExists(roomId) { + r.Unlock() + return + } + room := r.Rooms[roomId] + r.Unlock() + + trackLocal, err := webrtc.NewTrackLocalStaticRTP(remote.Codec().RTPCodecCapability, remote.ID(), remote.StreamID()) + if err != nil { + panic(err) + } + room.trackLock.Lock() + room.Tracks[remote.ID()] = &Track{ + OwnerId: id, + TrackLocal: trackLocal, + } + room.trackLock.Unlock() + + defer func() { + room.trackLock.Lock() + delete(room.Tracks, remote.ID()) + room.trackLock.Unlock() + r.updatePCTracks(roomId) + }() + go r.updatePCTracks(roomId) + buffer := make([]byte, 1500) + for { + n, _, err := remote.Read(buffer) + if err != nil { + println(err.Error()) + return + } + if _, err = trackLocal.Write(buffer[:n]); err != nil { + println(err.Error()) + return + } + } +} + +func (r *RoomRepository) updatePCTracks(roomId string) { + r.Lock() + if !r.doesRoomExists(roomId) { + r.Unlock() + return + } + room := r.Rooms[roomId] + defer r.Unlock() + room.Lock() + defer room.Unlock() + for _, peer := range room.Peers { + if peer.Conn == nil { + continue + } + alreadySentTracks := map[string]*webrtc.RTPSender{} + receivingPeerTracks := map[string]*webrtc.RTPReceiver{} + for _, rtpSender := range peer.Conn.GetSenders() { + if rtpSender.Track() == nil { + continue + } + track := rtpSender.Track() + alreadySentTracks[track.ID()] = rtpSender + } + for _, rtpReceiver := range peer.Conn.GetReceivers() { + if rtpReceiver.Track() == nil { + continue + } + track := rtpReceiver.Track() + receivingPeerTracks[track.ID()] = rtpReceiver + } + room.trackLock.Lock() + for id, track := range room.Tracks { + _, alreadySend := alreadySentTracks[id] + _, alreadyReceiver := receivingPeerTracks[id] + if track.OwnerId != peer.ID && (!alreadySend && !alreadyReceiver) { + go func(peer *Peer, track *Track) { + println("add track") + _, err := peer.Conn.AddTrack(track.TrackLocal) + if err != nil { + println(err.Error()) + } + }(peer, track) + } + } + room.trackLock.Unlock() + + /*room.trackLock.Lock() + for trackId, _ := range alreadySentTracks { + if _, exists := room.Tracks[trackId]; !exists { + go func(peer *Peer, rtpSender *webrtc.RTPSender) { + err := peer.Conn.RemoveTrack(rtpSender) + if err != nil { + println(err.Error()) + } + }(peer, alreadySentTracks[trackId]) + } + } + room.trackLock.Unlock()*/ + } +} + +func (r *RoomRepository) AddPeerIceCandidate(roomId string, id uint64, ic webrtc.ICECandidateInit) error { + r.Lock() + if !r.doesRoomExists(roomId) { + r.Unlock() + return models.NewError("room doesn't exists", 403, map[string]any{"roomId": roomId}) + } + r.Unlock() + room := r.Rooms[roomId] + room.Lock() + defer room.Unlock() + + if !r.doesPeerExists(roomId, id) { + return models.NewError("no such a peer with this id in this room", 403, map[string]any{"roomId": roomId, "peerId": id}) + } + + err := room.Peers[id].Conn.AddICECandidate(ic) + if err != nil { + return models.NewError(err.Error(), 500, models.MessageResponse{Message: err.Error()}) + } + return nil +} + +func (r *RoomRepository) SetPeerAnswer(roomId string, id uint64, answer webrtc.SessionDescription) error { + r.Lock() + if !r.doesRoomExists(roomId) { + r.Unlock() + return models.NewError("room doesn't exists", 403, map[string]any{"roomId": roomId}) + } + r.Unlock() + room := r.Rooms[roomId] + room.Lock() + defer room.Unlock() + + if !r.doesPeerExists(roomId, id) { + return models.NewError("no such a peer with this id in this room", 403, map[string]any{"roomId": roomId, "peerId": id}) + } + + err := room.Peers[id].Conn.SetRemoteDescription(answer) + if err != nil { + return models.NewError(err.Error(), 500, models.MessageResponse{Message: err.Error()}) + } + return nil +} +func (r *RoomRepository) SetPeerOffer(roomId string, id uint64, offer webrtc.SessionDescription) (sdpAnswer *webrtc.SessionDescription, err error) { + r.Lock() + if !r.doesRoomExists(roomId) { + r.Unlock() + return nil, models.NewError("room doesn't exists", 403, map[string]any{"roomId": roomId}) + } + r.Unlock() + room := r.Rooms[roomId] + room.Lock() + defer room.Unlock() + + if !r.doesPeerExists(roomId, id) { + return nil, models.NewError("no such a peer with this id in this room", 403, map[string]any{"roomId": roomId, "peerId": id}) + } + + err = room.Peers[id].Conn.SetRemoteDescription(offer) + if err != nil { + return nil, models.NewError(err.Error(), 500, models.MessageResponse{Message: err.Error()}) + } + answer, err := room.Peers[id].Conn.CreateAnswer(nil) + if err != nil { + return nil, models.NewError(err.Error(), 500, models.MessageResponse{Message: err.Error()}) + } + err = room.Peers[id].Conn.SetLocalDescription(answer) + if err != nil { + return nil, models.NewError(err.Error(), 500, models.MessageResponse{Message: err.Error()}) + } + + return &answer, nil +} + +func (r *RoomRepository) AllowPublish(roomId string, id uint64) error { + r.Lock() + if !r.doesRoomExists(roomId) { + r.Unlock() + return models.NewError("room doesn't exists", 403, map[string]any{"roomId": roomId}) + } + r.Unlock() + room := r.Rooms[roomId] + room.Lock() + defer room.Unlock() + + if !r.doesPeerExists(roomId, id) { + return models.NewError("no such a peer with this id in this room", 403, map[string]any{"roomId": roomId, "peerId": id}) + } + + room.Peers[id].CanPublish = true + return nil +} + +func (r *RoomRepository) ClosePeer(roomId string, id uint64) error { + r.Lock() + if !r.doesRoomExists(roomId) { + r.Unlock() + return models.NewError("room doesn't exists", 403, map[string]any{"roomId": roomId}) + } + r.Unlock() + room := r.Rooms[roomId] + room.Lock() + defer room.Unlock() + + if !r.doesPeerExists(roomId, id) { + return models.NewError("no such a peer with this id in this room", 403, map[string]any{"roomId": roomId, "peerId": id}) + } + return room.Peers[id].Conn.Close() +} diff --git a/routers/room.go b/routers/room.go new file mode 100644 index 0000000..de0224e --- /dev/null +++ b/routers/room.go @@ -0,0 +1,16 @@ +package routers + +import ( + "github.com/gin-gonic/gin" + "sourcecode.social/greatape/goldgorilla/controllers" +) + +func registerRoomRoutes(rg *gin.RouterGroup, ctrl *controllers.RoomController) { + + rg.POST("/peer", ctrl.CreatePeer) + rg.DELETE("/peer", ctrl.ClosePeer) + rg.POST("/ice", ctrl.AddICECandidate) + rg.POST("/answer", ctrl.Answer) + rg.POST("/offer", ctrl.Offer) + +} diff --git a/routers/router.go b/routers/router.go new file mode 100644 index 0000000..4ab7413 --- /dev/null +++ b/routers/router.go @@ -0,0 +1,23 @@ +package routers + +import ( + "github.com/gin-gonic/gin" + "sourcecode.social/greatape/goldgorilla/controllers" +) + +type Router struct { + router *gin.Engine +} + +func (r *Router) RegisterRoutes(rCtrl *controllers.RoomController) error { + gin.SetMode(gin.ReleaseMode) + r.router = gin.Default() + registerRoomRoutes(r.router.Group("/room"), rCtrl) + + return nil +} + +func (r *Router) Serve(addr string) error { + println("serving on ", addr) + return r.router.Run(addr) +}