From 7f84951ccf28105193bfa623ae22d2f789857c54 Mon Sep 17 00:00:00 2001 From: Trevor Slocum Date: Mon, 9 Dec 2019 16:16:24 -0800 Subject: [PATCH] Establish multiple WebRTC connections --- HOSTING.md | 5 + README.md | 17 ++ go.mod | 7 +- go.sum | 33 +--- pkg/audio/audio.go | 7 + pkg/web/client.go | 110 ++++++++++-- pkg/web/message.go | 1 + pkg/web/public/assets/js/harmony.js | 262 +++++++++++++++++++--------- pkg/web/public/index.html | 9 +- pkg/web/user.go | 21 +++ pkg/web/web.go | 192 ++++++++++++++------ 11 files changed, 475 insertions(+), 189 deletions(-) create mode 100644 HOSTING.md create mode 100644 pkg/audio/audio.go create mode 100644 pkg/web/user.go diff --git a/HOSTING.md b/HOSTING.md new file mode 100644 index 0000000..40f5cf8 --- /dev/null +++ b/HOSTING.md @@ -0,0 +1,5 @@ +This document covers how to host a harmony server. + +# Requirements + +- WIP \ No newline at end of file diff --git a/README.md b/README.md index 27a55ee..0bfe703 100644 --- a/README.md +++ b/README.md @@ -10,6 +10,23 @@ Voice and text communications platform - Low-latency voice chat using [Opus](https://en.wikipedia.org/wiki/Opus_%28audio_format%29) - Rich text chat using [Markdown](https://en.wikipedia.org/wiki/Markdown) +## Web client + +The only client currently implemented is a web interface (located in pkg/web) hosted by the server. + +## harmony-server + +The server (located in cmd/harmony-server) passes voice and text communications between clients. + +See [HOSTING.md](https://man.sr.ht/~tslocum/harmony/HOSTING.md) for more information on hosting a server. + ## Support Please share suggestions/issues [here](https://todo.sr.ht/~tslocum/harmony). + +## Libraries + +The following libraries are used to build netris: + +* [webrtc](https://github.com/pion/webrtc) - WebRTC connections and audio +* [websocket](https://github.com/gorilla/websocket) - WebSocket connections diff --git a/go.mod b/go.mod index 8c042fc..4638671 100644 --- a/go.mod +++ b/go.mod @@ -4,15 +4,10 @@ go 1.13 require ( github.com/GeertJohan/go.rice v1.0.0 - github.com/daaku/go.zipexe v1.0.1 // indirect - github.com/golang/protobuf v1.3.2 // indirect github.com/gorilla/mux v1.7.3 github.com/gorilla/websocket v1.4.1 - github.com/lucas-clemente/quic-go v0.13.1 // indirect + github.com/pion/ice v0.7.4 // indirect github.com/pion/rtp v1.1.4 github.com/pion/webrtc/v2 v2.1.16 github.com/pkg/errors v0.8.1 - golang.org/x/crypto v0.0.0-20191202143827-86a70503ff7e // indirect - golang.org/x/net v0.0.0-20191126235420-ef20fe5d7933 // indirect - golang.org/x/sys v0.0.0-20191128015809-6d18c012aee9 // indirect ) diff --git a/go.sum b/go.sum index f0a4235..00734ab 100644 --- a/go.sum +++ b/go.sum @@ -2,13 +2,10 @@ github.com/GeertJohan/go.incremental v1.0.0/go.mod h1:6fAjUhbVuX1KcMD3c8TEgVUqmo github.com/GeertJohan/go.rice v1.0.0 h1:KkI6O9uMaQU3VEKaj01ulavtF7o1fWT7+pk/4voiMLQ= github.com/GeertJohan/go.rice v1.0.0/go.mod h1:eH6gbSOAUv07dQuZVnBmoDP8mgsM1rtixis4Tib9if0= github.com/akavel/rsrc v0.8.0/go.mod h1:uLoCtb9J+EyAqh+26kdrTgmzRBFPGOolLWKpdxkKq+c= -github.com/alangpierce/go-forceexport v0.0.0-20160317203124-8f1d6941cd75 h1:3ILjVyslFbc4jl1w5TWuvvslFD/nDfR2H8tVaMVLrEY= -github.com/alangpierce/go-forceexport v0.0.0-20160317203124-8f1d6941cd75/go.mod h1:uAXEEpARkRhCZfEvy/y0Jcc888f9tHCc1W7/UeEtreE= github.com/cheekybits/genny v1.0.0 h1:uGGa4nei+j20rOSeDeP5Of12XVm7TGUd4dJA9RDitfE= github.com/cheekybits/genny v1.0.0/go.mod h1:+tQajlRqAUrPI7DOSpB0XAqZYtQakVtB7wXkRAgjxjQ= +github.com/daaku/go.zipexe v1.0.0 h1:VSOgZtH418pH9L16hC/JrgSNJbbAL26pj7lmD1+CGdY= github.com/daaku/go.zipexe v1.0.0/go.mod h1:z8IiR6TsVLEYKwXAoE/I+8ys/sDkgTzSL0CLnGVd57E= -github.com/daaku/go.zipexe v1.0.1 h1:wV4zMsDOI2SZ2m7Tdz1Ps96Zrx+TzaK15VbUaGozw0M= -github.com/daaku/go.zipexe v1.0.1/go.mod h1:5xWogtqlYnfBXkSB1o9xysukNP9GTvaNkqzUZbt3Bw8= 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= @@ -16,11 +13,8 @@ github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= github.com/golang/mock v1.2.0 h1:28o5sBqPkBsMGnC6b4MvE2TzSr5/AT4c/1fLqVGIwlk= github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/protobuf v1.2.0 h1:P3YflyNX/ehuJFLhxviNdFxQPkGK5cDcApsge1SqnvM= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/golang/protobuf v1.3.0 h1:kbxbvI4Un1LUWKxufD+BiE6AEExYYgkQLQmLFqA1LFk= -github.com/golang/protobuf v1.3.0/go.mod h1:Qd/q+1AKNOZr9uGQzbzCmRO6sUih6GTPZv6a1/R87v0= -github.com/golang/protobuf v1.3.2 h1:6nsPYzhq5kReh6QImI3k5qWzO4PEbvbIW2cwSfR/6xs= -github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/gorilla/mux v1.7.3 h1:gnP5JzjVOuiZD07fKKToCAOjS0yOpj/qPETTXCCS6hw= github.com/gorilla/mux v1.7.3/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= github.com/gorilla/websocket v1.4.1 h1:q7AeDBpnBk8AogcD4DSag/Ukw/KV+YhzLj2bP5HvKCM= @@ -33,15 +27,10 @@ github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORN github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/lucas-clemente/quic-go v0.7.1-0.20190401152353-907071221cf9 h1:tbuodUh2vuhOVZAdW3NEUvosFHUMJwUNl7jk/VSEiwc= github.com/lucas-clemente/quic-go v0.7.1-0.20190401152353-907071221cf9/go.mod h1:PpMmPfPKO9nKJ/psF49ESTAGQSdfXxlg1otPbEB2nOw= -github.com/lucas-clemente/quic-go v0.13.1 h1:CxtJTXQIh2aboCPk0M6vf530XOov6DZjVBiSE3nSj8s= -github.com/lucas-clemente/quic-go v0.13.1/go.mod h1:Vn3/Fb0/77b02SGhQk36KzOUmXgVpFfizUfW5WMaqyU= -github.com/marten-seemann/chacha20 v0.2.0 h1:f40vqzzx+3GdOmzQoItkLX5WLvHgPgyYqFFIO5Gh4hQ= -github.com/marten-seemann/chacha20 v0.2.0/go.mod h1:HSdjFau7GzYRj+ahFNwsO3ouVJr1HFkWoEwNDb4TMtE= -github.com/marten-seemann/qpack v0.1.0/go.mod h1:LFt1NU/Ptjip0C2CPkhimBz5CGE3WGDAUWqna+CNTrI= +github.com/marten-seemann/qtls v0.2.3 h1:0yWJ43C62LsZt08vuQJDK1uC1czUc3FJeCLPoNAI4vA= github.com/marten-seemann/qtls v0.2.3/go.mod h1:xzjG7avBwGGbdZ8dTGxlBnLArsVKLvwmjgmPuiQEcYk= -github.com/marten-seemann/qtls v0.4.1 h1:YlT8QP3WCCvvok7MGEZkMldXbyqgr8oFg5/n8Gtbkks= -github.com/marten-seemann/qtls v0.4.1/go.mod h1:pxVXcHHw1pNIt8Qo0pwSYQEoZ8yYOOPXTCZLQQunvRc= github.com/nkovacs/streamquote v0.0.0-20170412213628-49af9bddb229/go.mod h1:0aYXnNPJ8l7uZxf45rWW1a/uME32OF0rhiYGNQ2oF2E= github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/ginkgo v1.7.0 h1:WSHQ+IS43OoUrWtD1/bbclrwK8TTH5hzp+umCiuxHgs= @@ -52,8 +41,9 @@ github.com/pion/datachannel v1.4.13 h1:ezTn3AtUtXvKemRRjRdUgao/T8bH4ZJwrpOqU8Iz3 github.com/pion/datachannel v1.4.13/go.mod h1:+rBUwEDonA63KXx994DP/ofyyGVAm6AIMvOqQZxjWRU= github.com/pion/dtls/v2 v2.0.0-rc.3 h1:u9utI+EDJOjOWfrkGQsD8WNssPcTwfYIanFB6oI8K+4= github.com/pion/dtls/v2 v2.0.0-rc.3/go.mod h1:x0XH+cN5z+l/+/4nYL8r4sB8g6+0d1Zp2Pfkcoz8BKY= -github.com/pion/ice v0.7.2 h1:b+QxnpJ7AVyFDXBOMnEypNXS+fZM8+4+itNInwrrI6U= github.com/pion/ice v0.7.2/go.mod h1:xLKf+788DA/ZubtdBfiDT3vnEmIdiF5eDqjs4rzUAg8= +github.com/pion/ice v0.7.4 h1:cwbduOII1hRb7ntoDAyXvEqGxALBVFk7PO4IbC9i/9A= +github.com/pion/ice v0.7.4/go.mod h1:V+ILxpsY3qtzJnvg9W3mLgQ37FkYfzM4ri0wX3VLzpM= github.com/pion/logging v0.2.1/go.mod h1:k0/tDVsRCX2Mb2ZEmTqNa7CWsQPc+YYCB7Q+5pahoms= 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= @@ -94,30 +84,25 @@ github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyC github.com/valyala/fasttemplate v1.0.1/go.mod h1:UQGH1tvbgY+Nz5t2n7tXsz52dQxojPUpymEIMZ47gx8= 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-20190829043050-9756ffdc2472/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20191029031824-8986dd9e96cf/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20191122220453-ac88ee75c92c h1:/nJuwDLoL/zrqY6gf57vxC+Pi+pZ8bfhpPkicO5H7W4= golang.org/x/crypto v0.0.0-20191122220453-ac88ee75c92c/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.0.0-20191202143827-86a70503ff7e h1:egKlR8l7Nu9vHGWbcUV8lqR4987UfUbBd7GbhqGzNYU= -golang.org/x/crypto v0.0.0-20191202143827-86a70503ff7e/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20190228165749-92fc7df08ae7/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-20190619014844-b5b0513f8c1b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20191028085509-fe3aa8a45271/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20191126235420-ef20fe5d7933 h1:e6HwijUxhDe+hPNjZQQn9bA5PW3vNmnN64U2ZW759Lk= golang.org/x/net v0.0.0-20191126235420-ef20fe5d7933/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f h1:wMNYb4v58l5UBM7MYRLPG6ZhfOqbKu7X5eyFl8ZhKvA= 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-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-20191029155521-f43be2a4598c h1:S/FtSvpNLtFBgjTqcKsRpsa6aVsI6iztaz1bQd9BJwE= golang.org/x/sys v0.0.0-20191029155521-f43be2a4598c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191128015809-6d18c012aee9 h1:ZBzSG/7F4eNKz2L3GE9o300RX0Az1Bw5HF7PDraD+qU= -golang.org/x/sys v0.0.0-20191128015809-6d18c012aee9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -google.golang.org/genproto v0.0.0-20180831171423-11092d34479b/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/pkg/audio/audio.go b/pkg/audio/audio.go new file mode 100644 index 0000000..43a2a1e --- /dev/null +++ b/pkg/audio/audio.go @@ -0,0 +1,7 @@ +package audio + +const ( + ClockRate = 48 // KHz + FrameTime = 20 // ms + Samples = ClockRate * FrameTime +) diff --git a/pkg/web/client.go b/pkg/web/client.go index 19e40d1..a47926c 100644 --- a/pkg/web/client.go +++ b/pkg/web/client.go @@ -4,30 +4,44 @@ import ( "encoding/json" "log" "strconv" + "sync" "time" "github.com/gorilla/websocket" + "github.com/pion/rtp" "github.com/pion/webrtc/v2" "github.com/pkg/errors" ) type Client struct { - ID int - Name string - Status int - Conn *websocket.Conn - PeerConn *webrtc.PeerConnection - In chan *Message - Out chan *Message + ID int + Name string + Status int + Conn *websocket.Conn - AudioTrack *webrtc.Track + PeerConns map[int]*webrtc.PeerConnection + PeerConnLock *sync.Mutex + + In chan *Message + Out chan *Message + + AudioTracks map[int]*webrtc.Track SequenceNumber uint16 + VoiceIn chan []int16 + + VoiceOut []chan *rtp.Packet + VoiceOutClient []int + VoiceOutActive []time.Time + VoiceOutLock *sync.Mutex + Terminated chan bool } func NewClient(conn *websocket.Conn) *Client { - c := Client{Conn: conn, Name: "Anonymous", In: make(chan *Message, 10), Out: make(chan *Message, 10), SequenceNumber: 1, Terminated: make(chan bool)} + c := Client{Conn: conn, Name: "Anonymous", PeerConns: make(map[int]*webrtc.PeerConnection), PeerConnLock: new(sync.Mutex), In: make(chan *Message, 10), Out: make(chan *Message, 10), SequenceNumber: 1, AudioTracks: make(map[int]*webrtc.Track), VoiceIn: make(chan []int16, 10), Terminated: make(chan bool)} + + c.VoiceOutLock = new(sync.Mutex) go c.handleRead() go c.handleWrite() @@ -98,11 +112,9 @@ func (c *Client) Close() { return } c.Status = -1 - c.AudioTrack = nil - if c.PeerConn != nil { - c.PeerConn.Close() - } + c.CloseAudio() + if c.Conn != nil { c.Conn.Close() } @@ -118,13 +130,73 @@ func (c *Client) Close() { }() } -func (c *Client) ClosePeerConn() { - if c.PeerConn == nil { +func (c *Client) InitAudio() { + c.VoiceOutLock.Lock() + defer c.VoiceOutLock.Unlock() + + if len(c.VoiceOut) > 0 { return } - c.PeerConn.Close() - c.AudioTrack = nil - c.PeerConn = nil - c.SequenceNumber = 1 + for i := 0; i < 3; i++ { + c.VoiceOut = append(c.VoiceOut, make(chan *rtp.Packet, 10)) + c.VoiceOutClient = append(c.VoiceOutClient, 0) + c.VoiceOutActive = append(c.VoiceOutActive, time.Time{}) + } +} + +func (c *Client) WriteAudio(p *rtp.Packet, source int) { + c.VoiceOutLock.Lock() + for i := range c.VoiceOut { + if c.VoiceOutClient[i] == 0 || c.VoiceOutClient[i] == source || time.Since(c.VoiceOutActive[i]) >= 50*time.Millisecond { + select { + case c.VoiceOut[i] <- p: + default: + log.Printf("client %d warning: filled voice out buffer when writing from %d", c.ID, source) + continue + } + c.VoiceOutActive[i] = time.Now() + c.VoiceOutClient[i] = source + + c.VoiceOutLock.Unlock() + return + } + } + c.VoiceOutLock.Unlock() +} + +func (c *Client) CloseAudio() { + c.ClosePeerConns() + + c.PeerConns = make(map[int]*webrtc.PeerConnection) + c.VoiceOut = nil + c.VoiceOutClient = nil + c.VoiceOutActive = nil +} + +func (c *Client) ClosePeerConns() { + for id := range c.PeerConns { + c.ClosePeerConn(id) + } +} + +func (c *Client) ClosePeerConn(id int) { + c.VoiceOutLock.Lock() + defer c.VoiceOutLock.Unlock() + + pc := c.PeerConns[id] + if pc == nil { + return + } + + select { + case c.VoiceOut[id] <- nil: + default: + log.Println("failed to close channel for client " + strconv.Itoa(c.ID)) + } + + pc.Close() + pc = nil + + c.AudioTracks[id] = nil } diff --git a/pkg/web/message.go b/pkg/web/message.go index 9d248e9..4c28273 100644 --- a/pkg/web/message.go +++ b/pkg/web/message.go @@ -15,6 +15,7 @@ const ( MessageAction MessageType = 115 MessageDisconnect MessageType = 119 MessageChat MessageType = 120 + MessageUsers MessageType = 121 ) func (t MessageType) String() string { diff --git a/pkg/web/public/assets/js/harmony.js b/pkg/web/public/assets/js/harmony.js index 53cc9c5..c1ff0d6 100644 --- a/pkg/web/public/assets/js/harmony.js +++ b/pkg/web/public/assets/js/harmony.js @@ -1,3 +1,5 @@ +var printStats = false; + var socket = null; var ReconnectDelay = 0; var reconnectTimeout; @@ -6,14 +8,15 @@ var connected; var chatprefix = ""; var voice = false; var ptt = false; -var printStats = false; +var nickname = "Anonymous"; -var pc; +var numConnections = 3; +var peerConnections = []; var RTCConstraints = { audio: { - autoGainControl: true, - echoCancellation: true, - noiseSuppression: true, + autoGainControl: false, + echoCancellation: false, + noiseSuppression: false, }, video: false }; @@ -28,6 +31,8 @@ var audioTrack; var shownPTTHelp = false; var muteOnMouseUp = true; +var userListStatus = ''; + var MessageBinary = 2; var MessagePing = 100; var MessageCall = 101; @@ -40,6 +45,7 @@ var MessageTopic = 114; var MessageAction = 115; var MessageDisconnect = 119; var MessageChat = 120; +var MessageUsers = 121; var tagsToReplace = { '&': '&', @@ -75,55 +81,14 @@ function HandleInput(e) { $(document).ready(function () { $("#voiceButtonJoin").on("click touchstart", function () { - pc = new RTCPeerConnection({ - iceServers: RTCICEServers - }); - pc.onicecandidate = event => { - if (event.candidate === null) { + if (peerConnections.length > 0 || voice) { + return; + } - } - }; - navigator.mediaDevices.getUserMedia(RTCConstraints).then(localStream => { - audioTracks = localStream.getAudioTracks(); - if (audioTracks.length > 0) { - console.log(`Using Audio device: ${audioTracks[0].label} - tracks available: ${audioTracks}`); - } - - audioTrack = audioTracks[0]; - - pc.addTrack(audioTrack); - pc.getSenders()[0].replaceTrack(null); - - pc.ontrack = function (event) { - console.log(`onTrack ${event.streams.length} ${event.streams[0].getAudioTracks().length}`); - - pc.addTransceiver(event.streams[0].getAudioTracks()[0], {'direction': 'sendrecv'}); - - var el = document.getElementById('audioplayer'); - el.autoplay = true; - el.srcObject = event.streams[0]; - - voice = true; - - if (!shownPTTHelp) { - shownPTTHelp = true; - - Log("* Note: Push-to-talk is bound to <F8>"); - } - - $('#voicepttcontainer').css('display', 'table-row'); - $('#voiceinactiveleft').css('display', 'none'); - $('#voiceactiveleft').css('display', 'inline-block'); - $('#voiceinactiveright').css('display', 'none'); - $('#voiceactiveright').css('display', 'inline-block'); - $('#voiceButton').html('Quit voice chat'); - - updateVoiceStatus(); - }; - - pc.createOffer(RTCOfferOptions) - .then(onRTCDescription, onRTCDescriptionError); - }).catch(Log); + var i; + for (i = 0; i < numConnections; i++) { + peerConnections.push(createPeerConnection(i)); + } }); $("#voiceptt").on("touchstart", function (e) { @@ -165,10 +130,18 @@ $(document).ready(function () { }); $("#voiceButtonQuit").on("click touchstart", function () { - pc.close(); - w(MessageQuit, ""); + if (!voice) { + return; + } voice = false; + w(MessageQuit, ""); + + peerConnections.forEach(function (pc) { + pc.close(); + }); + peerConnections = []; + $('#voicepttcontainer').css('display', 'none'); $('#voiceinactiveleft').css('display', 'inline-block'); $('#voiceactiveleft').css('display', 'none'); @@ -179,8 +152,6 @@ $(document).ready(function () { updateVoiceStatus(); }); - Connect(); - window.setInterval(() => { if (!webSocketReady()) { return; @@ -191,6 +162,8 @@ $(document).ready(function () { if (printStats) { window.setInterval(() => { + // TODO Fix + if (!pc) { return; } @@ -223,24 +196,103 @@ $(document).ready(function () { }); }, 1000); } + + nickname = prompt("What is your name?", nickname); + Connect(); }); -function onRTCDescriptionError(error) { - console.log(`Failed to create/set session description: ${error.toString()}`); +function createPeerConnection(id) { + var pc = new RTCPeerConnection({ + iceServers: RTCICEServers + }); + pc.onicecandidate = event => { + if (event.candidate === null) { + + } + }; + + if (id > 0) { + pc.addTransceiver('audio', {'direction': 'recvonly'}); + } + + pc.ontrack = function (event) { + console.log(`PC ${id} onTrack ${event.streams.length} ${event.streams[0].id} ${event.streams[0].getAudioTracks().length}`); + + if (id > 0) { + pc.addTransceiver(event.streams[0].getAudioTracks()[0], {'direction': 'sendrecv'}); + } + + $("#hidden").append(''); + + var el = document.getElementById('audiostream' + id); + el.autoplay = true; + el.srcObject = event.streams[0]; + + voice = true; + + if (id == 0 && !shownPTTHelp) { + shownPTTHelp = true; + + Log("* Note: Push-to-talk is bound to <F8>"); + } + + $('#voicepttcontainer').css('display', 'table-row'); + $('#voiceinactiveleft').css('display', 'none'); + $('#voiceactiveleft').css('display', 'inline-block'); + $('#voiceinactiveright').css('display', 'none'); + $('#voiceactiveright').css('display', 'inline-block'); + $('#voiceButton').html('Quit voice chat'); + + updateVoiceStatus(); + }; + + if (id == 0) { + navigator.mediaDevices.getUserMedia(RTCConstraints).then(localStream => { + audioTracks = localStream.getAudioTracks(); + if (audioTracks.length > 0) { + console.log(`PC ${id} Using Audio device: ${audioTracks[0].label} - tracks available: ${audioTracks}`); + } + + audioTrack = audioTracks[0]; + + pc.addTrack(audioTrack); + pc.getSenders()[0].replaceTrack(null); + + pc.createOffer(RTCOfferOptions) + .then(onRTCDescription(id), onRTCDescriptionError(id)); + }).catch(Log); + } else { + pc.createOffer(RTCOfferOptions) + .then(onRTCDescription(id), onRTCDescriptionError(id)); + } + + return pc; } -function onRTCDescription(desc) { - console.log(`Offer from pc\n${desc.sdp}`); - pc.setLocalDescription(desc) - .then(() => { - desc.sdp = maybePreferCodec(desc.sdp, 'audio', 'send', 'opus'); - desc.sdp = maybePreferCodec(desc.sdp, 'audio', 'receive', 'opus'); +function onRTCDescriptionError(id) { + return function (error) { + console.log(`Failed to create/set session description: ${error.toString()}`); + } +} - console.log("onRTCDescription"); - console.log(desc); +function onRTCDescription(id) { + return function (desc) { + console.log(`PC ${id} Offer received \n${desc.sdp}`); - w(MessageCall, desc.sdp); - }, onRTCDescriptionError); + if (peerConnections.length < id) { + return; + } + + peerConnections[id].setLocalDescription(desc) + .then(() => { + desc.sdp = maybePreferCodec(desc.sdp, 'audio', 'send', 'opus'); + desc.sdp = maybePreferCodec(desc.sdp, 'audio', 'receive', 'opus'); + + console.log(`PC ${id} onRTCDescription`); + + wpc(id, MessageCall, desc.sdp); + }, onRTCDescriptionError); + } } function Connect() { @@ -275,6 +327,8 @@ function Connect() { clearTimeout(reconnectTimeout); } + w(MessageNick, nickname); + updateVoiceStatus(); }; socket.onmessage = function (e) { @@ -290,17 +344,59 @@ function Connect() { } if (p.T == MessageAnswer) { + if (p.PC === undefined || p.PC > peerConnections.length) { + return; + } + + var pc = peerConnections[p.PC]; pc.setRemoteDescription(new RTCSessionDescription({type: 'answer', sdp: p.M})); } else if (p.T == MessageConnect) { - Log("* " + p.M + " connected"); + if (p.N === undefined) { + return; + } + + Log("* " + escapeEntities(p.N) + " connected"); } else if (p.T == MessageJoin) { - Log("* " + p.M + " joined #lobby voice chat"); + if (p.N === undefined) { + return; + } + + Log("* " + escapeEntities(p.N) + " joined #lobby voice chat"); } else if (p.T == MessageQuit) { - Log("* " + p.M + " quit #lobby voice chat"); + if (p.N === undefined) { + return; + } + + Log("* " + escapeEntities(p.N) + " quit #lobby voice chat"); } else if (p.T == MessageDisconnect) { - Log("* " + p.M + " disconnected"); + if (p.N === undefined) { + return; + } + + Log("* " + escapeEntities(p.N) + " disconnected"); } else if (p.T == MessageChat) { - Log("<Anonymous> " + escapeEntities(p.M)); + if (p.N === undefined) { + return; + } + + Log("<" + escapeEntities(p.N) + "> " + escapeEntities(p.M)); + } else if (p.T == MessageUsers) { + var usersconnected = 0; + var usersvoice = 0; + + var u = JSON.parse(p.M); + for (let i = 0; i < u.length; i++) { + usersconnected++; + + if (u[i].V) { + usersvoice++; + } + } + + userListStatus = "Users: " + usersconnected + " - Voice chatting: " + usersvoice; + + $("#voiceinactiveright").html(userListStatus); + updateVoiceStatus(); } } else { // TODO Binary data @@ -354,26 +450,26 @@ function Log(msg) { } function StartPTT() { - if (ptt) { + if (ptt || !voice || peerConnections.length == 0) { return; } ptt = true; - var sender = pc.getSenders()[0]; + var sender = peerConnections[0].getSenders()[0]; sender.replaceTrack(audioTrack); updateVoiceStatus(); } function StopPTT() { - if (!ptt) { + if (!ptt || !voice || peerConnections.length == 0) { return; } ptt = false; - var sender = pc.getSenders()[0]; + var sender = peerConnections[0].getSenders()[0]; sender.replaceTrack(null); updateVoiceStatus(); @@ -383,7 +479,7 @@ function updateVoiceStatus() { if (ptt) { $('#voiceactiveleft').html('Transmitting'); } else { - $('#voiceactiveleft').html('# users voice chatting'); + $('#voiceactiveleft').html(userListStatus); } } @@ -395,6 +491,14 @@ function w(t, m) { socket.send(JSON.stringify({T: t, M: btoa(m)})); } +function wpc(pc, t, m) { + if (!webSocketReady()) { + return; + } + + socket.send(JSON.stringify({PC: pc, T: t, M: btoa(m)})); +} + function escapeEntitiesCallback(tag) { return tagsToReplace[tag] || tag; } diff --git a/pkg/web/public/index.html b/pkg/web/public/index.html index 11d5b99..e22ba4a 100644 --- a/pkg/web/public/index.html +++ b/pkg/web/public/index.html @@ -11,6 +11,7 @@
+
@@ -36,11 +37,11 @@
- +
- # users voice chatting +