5
0
Fork 0

Initial logic

pull/6/head
Daniel Lim 2019-06-24 13:05:36 +08:00
parent 9a75c31d97
commit 761d76df61
6 changed files with 434 additions and 76 deletions

1
.env Normal file
View File

@ -0,0 +1 @@
LISTEN=:80

87
.gitignore vendored
View File

@ -1,78 +1,15 @@
# ---> Node
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# Binaries for programs and plugins
*.exe
*.exe~
*.dll
*.so
*.dylib
# Runtime data
pids
*.pid
*.seed
*.pid.lock
# Test binary, built with `go test -c`
*.test
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
# nyc test coverage
.nyc_output
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# Bower dependency directory (https://bower.io/)
bower_components
# node-waf configuration
.lock-wscript
# Compiled binary addons (https://nodejs.org/api/addons.html)
build/Release
# Dependency directories
node_modules/
jspm_packages/
# TypeScript v1 declaration files
typings/
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# Yarn Integrity file
.yarn-integrity
# dotenv environment variables file
.env
# parcel-bundler cache (https://parceljs.org/)
.cache
# next.js build output
.next
# nuxt.js build output
.nuxt
# vuepress build output
.vuepress/dist
# Serverless directories
.serverless
# FuseBox cache
.fusebox/
# Output of the go coverage tool, specifically when used with LiteIDE
*.out
# Dependency directories (remove the comment below to include it)
# vendor/

115
README.md
View File

@ -1,3 +1,116 @@
# backend-webrtc
Beep backend handling WebRTC Selective Forwarding Units
Beep backend handling WebRTC Selective Forwarding Units (SFUs).
**The security of this service is handled by backend-auth called by traefik.**
## Environment variables
Supply environment variables by either exporting them or editing `.env`.
| ENV | Description | Default |
| --- | ----------- | ------- |
| LISTEN | Host and port to listen on | :80 |
## API
All endpoints require a populated `X-User-Claim` header from `backend-auth`.
| Contents |
| -------- |
| New Connection |
| Join Conversation |
---
### New Connection
```
GET /connect
```
Creates a new WebRTC peer connection with the server. Connection gets upgraded to a websocket through which signalling occurs before it is closed once the peer connection is established. Please supply the token via GET querystring since Websockets do not support Auth headers.
#### Example (Javascript)
```js
const wsuri = `wss:\/\/localhost:80/connect`;
let localSDP = '';
let remoteSDP = '';
const socket = new WebSocket(wsuri);
socket.onmessage = (e) => {
remoteSDP = e.data;
try {
pc.setRemoteDescription(new RTCSessionDescription({ type: 'answer', sdp: remoteSDP });
} catch(e) {
console.error(e);
}
};
const pc = new RTCPeerConnection({
iceServers: [
{
urls: 'stun:stun.l.google.com:19302',
},
],
});
pc.onicecandidate = (e) => {
if (e.candidate === null) {
localSDP = pc.localDescription.sdp;
socket.send(localSDP);
}
};
navigator.mediaDevices
.getUserMedia({ audio: true })
.then((stream) => {
for (const track of stream.getAudioTracks()) {
pc.addTrack(track);
}
pc.createOffer()
.then((d) => {
p.setLocalDescription(d);
})
.catch((e) => console.error(e));
})
.catch((e) => console.error(e));
pc.ontrack = (event) => {
// Do stuff with event.streams
};
```
#### Errors
| Code | Description |
| ---- | ----------- |
| 400 | Error parsing `X-User-Claims` header |
| 500 | Error establishing WebRTC connection |
---
### Join Conversation
```
POST /join/:conversationid
```
Signify a user's intention to join a conversation.
#### Params
| Name | Type | Description |
| ---- | ---- | ----------- |
| conversationid | String | ID of conversation to be joined |
#### Success (200 OK)
Empty body
#### Errors
| Code | Description |
| ---- | ----------- |
| 400 | Error parsing `X-User-Claims` header |

10
go.mod Normal file
View File

@ -0,0 +1,10 @@
module webrtc
go 1.12
require (
github.com/gorilla/websocket v1.4.0
github.com/joho/godotenv v1.3.0
github.com/julienschmidt/httprouter v1.2.0
github.com/pion/webrtc/v2 v2.0.23
)

79
go.sum Normal file
View File

@ -0,0 +1,79 @@
github.com/cheekybits/genny v1.0.0/go.mod h1:+tQajlRqAUrPI7DOSpB0XAqZYtQakVtB7wXkRAgjxjQ=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
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/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/gorilla/websocket v1.4.0 h1:WDFjx/TMzVgy9VdMMQi2K2Emtwi2QcUQsztZ/zLaH/Q=
github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ=
github.com/gortc/turn v0.7.1/go.mod h1:3FZ+LvCZKCKu6YYgwuYPqEi3FqCtdjfSFnFqVQNwfjk=
github.com/gortc/turn v0.7.3 h1:CE72C79erbcsfa6L/QDhKztcl2kDq1UK20ImrJWDt/w=
github.com/gortc/turn v0.7.3/go.mod h1:gvguwaGAFyv5/9KrcW9MkCgHALYD+e99mSM7pSCYYho=
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
github.com/joho/godotenv v1.3.0 h1:Zjp+RcGpHhGlrMbJzXTrZZPrWj+1vfm90La1wgB6Bhc=
github.com/joho/godotenv v1.3.0/go.mod h1:7hK45KPybAkOC6peb+G5yklZfMxEjkZhHbwpqxOKXbg=
github.com/julienschmidt/httprouter v1.2.0 h1:TDTW5Yz1mjftljbcKqRcrYhd4XeOoI98t+9HbQbYf7g=
github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
github.com/lucas-clemente/quic-go v0.7.1-0.20190401152353-907071221cf9/go.mod h1:PpMmPfPKO9nKJ/psF49ESTAGQSdfXxlg1otPbEB2nOw=
github.com/marten-seemann/qtls v0.2.3/go.mod h1:xzjG7avBwGGbdZ8dTGxlBnLArsVKLvwmjgmPuiQEcYk=
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
github.com/pion/datachannel v1.4.3 h1:tqS6YiqqAiFCxGGhvn1K7fHEzemK9Aov025dE/isGFo=
github.com/pion/datachannel v1.4.3/go.mod h1:SpMJbuu8v+qbA94m6lWQwSdCf8JKQvgmdSHDNtcbe+w=
github.com/pion/dtls v1.3.5 h1:mBioifvh6JSE9pD4FtJh5WoizygoqkOJNJyS5Ns+y1U=
github.com/pion/dtls v1.3.5/go.mod h1:CjlPLfQdsTg3G4AEXjJp8FY5bRweBlxHrgoFrN+fQsk=
github.com/pion/ice v0.4.0 h1:BdTXHTjzdsJHGi9yMFnj9ffgr+Kg2oHVv1qk4B0mQ8A=
github.com/pion/ice v0.4.0/go.mod h1:/gw3aFmD/pBG8UM3TcEHs6HuaOEMSd/v1As3TodE7Ss=
github.com/pion/logging v0.2.1 h1:LwASkBKZ+2ysGJ+jLv1E/9H1ge0k1nTfi1X+5zirkDk=
github.com/pion/logging v0.2.1/go.mod h1:k0/tDVsRCX2Mb2ZEmTqNa7CWsQPc+YYCB7Q+5pahoms=
github.com/pion/mdns v0.0.2 h1:T22Gg4dSuYVYsZ21oRFh9z7twzAm27+5PEKiABbjCvM=
github.com/pion/mdns v0.0.2/go.mod h1:VrN3wefVgtfL8QgpEblPUC46ag1reLIfpqekCnKunLE=
github.com/pion/quic v0.1.1/go.mod h1:zEU51v7ru8Mp4AUBJvj6psrSth5eEFNnVQK5K48oV3k=
github.com/pion/rtcp v1.2.0 h1:rT2FptW5YHIern+4XlbGYnnsT26XGxurnkNLnzhtDXg=
github.com/pion/rtcp v1.2.0/go.mod h1:a5dj2d6BKIKHl43EnAOIrCczcjESrtPuMgfmL6/K6QM=
github.com/pion/rtp v1.1.2 h1:ERNugzYHW9F2ldpwoARbeFGKRoq1REe5Jxdjvm/rOx8=
github.com/pion/rtp v1.1.2/go.mod h1:/l4cvcKd0D3u9JLs2xSVI95YkfXW87a3br3nqmVtSlE=
github.com/pion/sctp v1.6.3 h1:SC4vKOjcddK8tXiTNj05a+0/GyPpCmuNfeBA/rzNFqs=
github.com/pion/sctp v1.6.3/go.mod h1:cCqpLdYvgEUdl715+qbWtgT439CuQrAgy8BZTp0aEfA=
github.com/pion/sdp/v2 v2.2.0 h1:JiixCEU8g6LbSsh1Bg5SOk0TPnJrn2HBOA1yJ+mRYhI=
github.com/pion/sdp/v2 v2.2.0/go.mod h1:idSlWxhfWQDtTy9J05cgxpHBu/POwXN2VDRGYxT/EjU=
github.com/pion/srtp v1.2.4 h1:wwGKC5ewuBukkZ+i+pZ8aO33+t6z2y/XRiYtyP0Xpv0=
github.com/pion/srtp v1.2.4/go.mod h1:52qiP0g3FVMG/5NL6Ko8Vr2qirevKH+ukYbNS/4EX40=
github.com/pion/stun v0.3.0/go.mod h1:xrCld6XM+6GWDZdvjPlLMsTU21rNxnO6UO8XsAvHr/M=
github.com/pion/stun v0.3.1 h1:d09JJzOmOS8ZzIp8NppCMgrxGZpJ4Ix8qirfNYyI3BA=
github.com/pion/stun v0.3.1/go.mod h1:xrCld6XM+6GWDZdvjPlLMsTU21rNxnO6UO8XsAvHr/M=
github.com/pion/transport v0.6.0/go.mod h1:iWZ07doqOosSLMhZ+FXUTq+TamDoXSllxpbGcfkCmbE=
github.com/pion/transport v0.7.0/go.mod h1:iWZ07doqOosSLMhZ+FXUTq+TamDoXSllxpbGcfkCmbE=
github.com/pion/transport v0.8.0 h1:YHZnWBBrBuMqkuvMFUHeAETXS+LgfwW1IsVd2K2cyW8=
github.com/pion/transport v0.8.0/go.mod h1:nAmRRnn+ArVtsoNuwktvAD+jrjSD7pA+H3iRmZwdUno=
github.com/pion/turn v1.1.4/go.mod h1:2O2GFDGO6+hJ5gsyExDhoNHtVcacPB1NOyc81gkq0WA=
github.com/pion/turnc v0.0.6 h1:FHsmwYvdJ8mhT1/ZtWWer9L0unEb7AyRgrymfWy6mEY=
github.com/pion/turnc v0.0.6/go.mod h1:4MSFv5i0v3MRkDLdo5eF9cD/xJtj1pxSphHNnxKL2W8=
github.com/pion/webrtc/v2 v2.0.23 h1:v/tDKsP4zB6Sj+Wx861fLsaNmbwWbxacciHUhetH288=
github.com/pion/webrtc/v2 v2.0.23/go.mod h1:AgremGibyNcHWIEkDbXt4ujKzKBO3tMuoYXybVRa8zo=
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
golang.org/x/crypto v0.0.0-20190228161510-8dd112bcdc25/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190404164418-38d8ce5564a5 h1:bselrhR0Or1vomJZC8ZIjWtbDmn9OYFLX5Ik9alpJpE=
golang.org/x/crypto v0.0.0-20190404164418-38d8ce5564a5/go.mod h1:WFFai1msRO1wXaEeE5yQxYXgSfI8pQAWXbQop6sCtWE=
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190403144856-b630fd6fe46b/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190619014844-b5b0513f8c1b h1:lkjdUzSyJ5P1+eal9fxXX9Xg2BTfswsonKUse48C0uE=
golang.org/x/net v0.0.0-20190619014844-b5b0513f8c1b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/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-20190228124157-a34e9553db1e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190403152447-81d4e9dc473e h1:nFYrTHrdrAOpShe27kaFHjsqYSEQ0KWqdWLu3xuZJts=
golang.org/x/sys v0.0.0-20190403152447-81d4e9dc473e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/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.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=

218
main.go Normal file
View File

@ -0,0 +1,218 @@
package main
import (
"context"
"encoding/json"
"io"
"log"
"net/http"
"os"
"github.com/joho/godotenv"
"github.com/julienschmidt/httprouter"
"github.com/pion/webrtc/v2"
"github.com/gorilla/websocket"
)
// Peer config
var peerConnectionConfig webrtc.Configuration
var listen string
var upgrader websocket.Upgrader
var mediaEngine webrtc.MediaEngine
var webrtcApi *webrtc.API
var userTracks map[string] map[string] *webrtc.Track // userid + clientid
var conversationUsers map[string] []string
var userConversation map[string] string
func main() {
// Load .env
err := godotenv.Load()
if err != nil {
log.Fatal("Error loading .env file")
}
listen = os.Getenv("LISTEN")
upgrader = websocket.Upgrader{}
mediaEngine = webrtc.MediaEngine{}
mediaEngine.RegisterCodec(webrtc.NewRTPOpusCodec(webrtc.DefaultPayloadTypeOpus, 48000))
webrtcApi = webrtc.NewAPI(webrtc.WithMediaEngine(mediaEngine))
userTracks = make(map[string] map[string] *webrtc.Track)
conversationUsers = make(map[string] []string)
userConversation = make(map[string] string)
// Routes
router := httprouter.New()
router.GET("/connect", GetAuth(NewConnection))
router.POST("/join/:conversationid", GetAuth(JoinConversation))
// Start server
log.Printf("starting server on %s", listen)
log.Fatal(http.ListenAndServe(listen, router))
}
type RawClient struct {
UserId string `json:"userid"`
ClientId string `json:"clientid"`
}
func GetAuth(next httprouter.Handle) httprouter.Handle {
return func(w http.ResponseWriter, r *http.Request, p httprouter.Params) {
ua := r.Header.Get("X-User-Claim")
if ua == "" {
http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)
return
}
var client RawClient
err := json.Unmarshal([]byte(ua), &client)
if err != nil {
http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)
return
}
context := context.WithValue(r.Context(), "user", client)
next(w, r.WithContext(context), p)
}
}
func NewConnection(w http.ResponseWriter, r *http.Request, p httprouter.Params) {
// Get user id
user := r.Context().Value("user").(RawClient)
// Websocket client
c, err := upgrader.Upgrade(w, r, nil)
if err != nil {
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
return
}
defer c.Close()
// Read SDP from websocket
mt, msg, err := c.ReadMessage()
if err != nil {
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
return
}
// Establish connection (it's a publisher)
clientReceiver, err := webrtcApi.NewPeerConnection(peerConnectionConfig)
if err != nil {
log.Printf("%s", err)
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
return
}
_, err = clientReceiver.AddTransceiver(webrtc.RTPCodecTypeAudio)
if err != nil {
log.Printf("%s", err)
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
return
}
// Handle OnTrack
clientReceiver.OnTrack(func(remoteTrack *webrtc.Track, receiver *webrtc.RTPReceiver) {
rtpBuf := make([]byte, 1400)
for {
i, err := remoteTrack.Read(rtpBuf)
if err != nil {
log.Printf("%s", err)
break
}
if conversationId, ok := userConversation[user.UserId]; ok {
if users, ok2 := conversationUsers[conversationId]; ok2 {
for _, u := range users {
if clients, ok3 := userTracks[u]; ok3 {
for client, track := range clients {
if !(u == user.UserId && client == user.ClientId) {
_, err = track.Write(rtpBuf[:i])
if !(err == io.ErrClosedPipe || err == nil) {
log.Printf("%s", err)
break
}
}
}
}
}
}
}
}
})
// Add sending track
var track *webrtc.Track = &webrtc.Track{}
_, err = clientReceiver.AddTrack(track)
if err != nil {
log.Printf("%s", err)
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
return
}
userTracks[user.UserId][user.ClientId] = track
// Do signalling things
err = clientReceiver.SetRemoteDescription(
webrtc.SessionDescription {
SDP: string(msg),
Type: webrtc.SDPTypeOffer,
})
if err != nil {
log.Printf("%s", err)
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
return
}
answer, err := clientReceiver.CreateAnswer(nil)
if err != nil {
log.Printf("%s", err)
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
return
}
err = clientReceiver.SetLocalDescription(answer)
if err != nil {
log.Printf("%s", err)
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
return
}
err = c.WriteMessage(mt, []byte(answer.SDP))
if err != nil {
log.Printf("%s", err)
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
return
}
}
func JoinConversation(w http.ResponseWriter, r *http.Request, p httprouter.Params) {
// Get user id
user := r.Context().Value("user").(RawClient)
conversationId := p.ByName("conversationid")
// Remove user from existing conversation
if oldConversation, ok := userConversation[user.UserId]; ok {
if users, ok2 := conversationUsers[oldConversation]; ok2 {
var lastIndex int
for i, u := range users {
if u == user.UserId {
lastIndex = i
break;
}
}
users[lastIndex] = users[len(users) - 1]
conversationUsers[oldConversation] = users[:len(users)-1]
}
}
// Populate new values
userConversation[user.UserId] = conversationId
if _, ok := conversationUsers[conversationId]; !ok {
conversationUsers[conversationId] = make([]string, 0)
}
conversationUsers[conversationId] = append(conversationUsers[conversationId], user.UserId)
w.WriteHeader(200)
}