├── turn-server ├── build.sh ├── go.mod ├── turn-server.go └── go.sum ├── shared └── message-types.js ├── client ├── user-media.js ├── styles.css ├── ice-servers.js ├── test.js ├── signaling-server-connection.js ├── index.html ├── webrtc-util.js ├── template-util.js └── video-chat-main.js ├── signaling-server ├── go.mod ├── go.sum ├── signaling │ ├── message.go │ └── user.go ├── example │ ├── client.go │ └── server.go ├── signaling-server.js └── main.go ├── ssl-config.js ├── client-dev-server.js ├── package.json ├── LICENSE ├── .gitignore └── readme.md /turn-server/build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | go build -o ./build/turn-server 4 | -------------------------------------------------------------------------------- /shared/message-types.js: -------------------------------------------------------------------------------- 1 | export const messageTypes = { 2 | signalServerConnected: 'signal-server-connected', 3 | join: 'join', 4 | userList: 'user-list', 5 | offer: 'offer', 6 | answer: 'answer', 7 | iceCandidate: 'ice-candidate', 8 | } 9 | -------------------------------------------------------------------------------- /client/user-media.js: -------------------------------------------------------------------------------- 1 | export function getUserMedia() { 2 | // TODO: IMPLEMENT 3 | return navigator.mediaDevices 4 | .getUserMedia({ 5 | audio: true, 6 | video: { 7 | facingMode: { ideal: ['user', 'environment'] }, 8 | height: { ideal: 250 } 9 | } 10 | }) 11 | } -------------------------------------------------------------------------------- /signaling-server/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/haydenbr/sigserver 2 | 3 | go 1.20 4 | 5 | require ( 6 | github.com/google/uuid v1.3.0 7 | github.com/gorilla/websocket v1.5.0 8 | github.com/samber/lo v1.38.1 9 | ) 10 | 11 | require golang.org/x/exp v0.0.0-20220303212507-bbda1eaf7a17 // indirect 12 | -------------------------------------------------------------------------------- /turn-server/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/haydenbr/webrtc-video-chat/turn-server 2 | 3 | go 1.20 4 | 5 | require github.com/pion/turn/v2 v2.0.5 6 | 7 | require ( 8 | github.com/pion/logging v0.2.2 // indirect 9 | github.com/pion/randutil v0.1.0 // indirect 10 | github.com/pion/stun v0.3.5 // indirect 11 | github.com/pion/transport v0.10.1 // indirect 12 | ) 13 | -------------------------------------------------------------------------------- /client/styles.css: -------------------------------------------------------------------------------- 1 | .video-container { 2 | background-color: black; 3 | height: 250px; 4 | width: 300px; 5 | position: relative; 6 | overflow: hidden; 7 | display: flex; 8 | justify-content: center; 9 | flex-shrink: 0; 10 | margin: 4px; 11 | } 12 | 13 | .video-label { 14 | position: absolute; 15 | bottom: 0; 16 | left: 0; 17 | color: white; 18 | background-color: rgba(0,0,0,0.5); 19 | padding-left: 8px; 20 | padding-right: 8px; 21 | } 22 | 23 | #peer-video-container { 24 | display: flex; 25 | flex-wrap: wrap; 26 | } -------------------------------------------------------------------------------- /ssl-config.js: -------------------------------------------------------------------------------- 1 | import { readFileSync } from 'fs' 2 | 3 | const isProd = process.argv.includes('--prod') 4 | 5 | let certIndex = process.argv.findIndex(arg => arg === '--ssl-cert-path') 6 | let keyIndex = process.argv.findIndex(arg => arg === '--ssl-key-path') 7 | const envCertPath = isProd ? process.argv[certIndex + 1] : '' 8 | const envKeyPath = isProd ? process.argv[keyIndex + 1] : '' 9 | 10 | export const sslConfig = { 11 | cert: envCertPath && readFileSync(envCertPath), 12 | key: envKeyPath && readFileSync(envKeyPath), 13 | }; 14 | -------------------------------------------------------------------------------- /client-dev-server.js: -------------------------------------------------------------------------------- 1 | import liveServer from 'live-server' 2 | import { sslConfig } from './ssl-config.js' 3 | 4 | const prod = !!process.argv.includes('--prod') 5 | const port = prod ? 443 : 5500 6 | 7 | let devServerConfig = { 8 | port, 9 | host: '0.0.0.0', 10 | root: './client', 11 | open: false, 12 | mount: [['/shared', './shared']], 13 | logLevel: prod ? 0 : 2, 14 | cors: true, 15 | watch: prod ? undefined : './client', 16 | } 17 | 18 | if (prod) { 19 | devServerConfig.https = sslConfig 20 | } 21 | 22 | liveServer.start(devServerConfig); 23 | -------------------------------------------------------------------------------- /client/ice-servers.js: -------------------------------------------------------------------------------- 1 | import { 2 | getCallSettings, 3 | } from './template-util.js' 4 | 5 | export function getIceServers() { 6 | let iceServers = [] 7 | let callSettings = getCallSettings() 8 | 9 | if (callSettings.stunServer) { 10 | iceServers.push({ urls: callSettings.stunServer }) 11 | } 12 | 13 | if (callSettings.turnServer) { 14 | iceServers.push({ 15 | urls: callSettings.turnServer, 16 | credentialType: 'password', 17 | username: callSettings.turnUserName, 18 | credential: callSettings.turnPassword 19 | }) 20 | } 21 | 22 | return iceServers 23 | } 24 | -------------------------------------------------------------------------------- /client/test.js: -------------------------------------------------------------------------------- 1 | run() 2 | 3 | async function run() { 4 | let peerConnection = new RTCPeerConnection() 5 | 6 | peerConnection.onicecandidate = (event) => console.log(event) 7 | peerConnection.oniceconnectionstatechange = (event) => console.log(event) 8 | peerConnection.onsignalingstatechange = (event) => console.log(event) 9 | peerConnection.onnegotiationneeded = (event) => console.log(event) 10 | peerConnection.ontrack = (event) => console.log(event) 11 | 12 | let mediaStream = await navigator.mediaDevices.getUserMedia({ video: true }) 13 | 14 | mediaStream 15 | .getTracks() 16 | .forEach(track => peerConnection.addTransceiver(track, { streams: [mediaStream] })) 17 | } 18 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "webrtc-video-chat", 3 | "version": "0.0.0", 4 | "scripts": { 5 | "start": "npm run client:dev & npm run signaling-server:dev", 6 | "client:dev": "node ./client-dev-server.js", 7 | "client:prod": "node ./client-dev-server.js --prod", 8 | "signaling-server:dev": "nodemon -w ./signaling-server/signaling-server.js ./signaling-server/signaling-server.js", 9 | "signaling-server:prod": "node ./signaling-server/signaling-server.js --prod" 10 | }, 11 | "dependencies": { 12 | "live-server": "^1.2.1", 13 | "nodemon": "^2.0.6", 14 | "queue": "^6.0.2", 15 | "uuid": "^8.3.1", 16 | "ws": "^7.4.0" 17 | }, 18 | "type": "module" 19 | } 20 | -------------------------------------------------------------------------------- /client/signaling-server-connection.js: -------------------------------------------------------------------------------- 1 | let signalingServer 2 | 3 | export function connectToSignalingServer(serverUrl, messageHandlers) { 4 | return new Promise((resolve) => { 5 | signalingServer = new WebSocket(serverUrl, "json"); 6 | 7 | signalingServer.onopen = () => { 8 | resolve() 9 | signalingServer.onopen = undefined 10 | } 11 | 12 | signalingServer.onmessage = (event) => { 13 | let message = JSON.parse(event.data); 14 | let messageHandler = messageHandlers[message.type] 15 | 16 | if (messageHandler) { 17 | messageHandler(message) 18 | } 19 | }; 20 | }) 21 | } 22 | 23 | export function sendSignalMessage(message) { 24 | signalingServer.send(JSON.stringify(message)) 25 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2020 Hayden R. Braxton 2 | 3 | Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted, provided that the above copyright notice and this permission notice appear in all copies. 4 | 5 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 6 | -------------------------------------------------------------------------------- /signaling-server/go.sum: -------------------------------------------------------------------------------- 1 | github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= 2 | github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 3 | github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc= 4 | github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= 5 | github.com/samber/lo v1.38.1 h1:j2XEAqXKb09Am4ebOg31SpvzUTTs6EN3VfgeLUhPdXM= 6 | github.com/samber/lo v1.38.1/go.mod h1:+m/ZKRl6ClXCE2Lgf3MsQlWfh4bn1bz6CXEOxnEXnEA= 7 | golang.org/x/exp v0.0.0-20220303212507-bbda1eaf7a17 h1:3MTrJm4PyNL9NBqvYDSj3DHl46qQakyfqfWo4jgfaEM= 8 | golang.org/x/exp v0.0.0-20220303212507-bbda1eaf7a17/go.mod h1:lgLbSvA5ygNOMpwM/9anMpWVlVJ7Z+cHWq/eFuinpGE= 9 | -------------------------------------------------------------------------------- /signaling-server/signaling/message.go: -------------------------------------------------------------------------------- 1 | package signaling 2 | 3 | type MessageType int 4 | 5 | const ( 6 | ServerAck MessageType = iota 7 | ClientAck 8 | UserList 9 | UserJoin 10 | UserUpdate 11 | UserLeave 12 | ) 13 | 14 | type Message struct { 15 | MessageType MessageType 16 | Payload interface{} 17 | } 18 | 19 | type ServerAckPayload struct { 20 | UserId string 21 | } 22 | 23 | type ClientAckPayload struct { 24 | UserId string 25 | UserName string 26 | } 27 | 28 | type UserLeavePayload struct { 29 | UserId string 30 | } 31 | 32 | type UserListPayload struct { 33 | Users []User 34 | } 35 | 36 | func newMessage() 37 | 38 | func NewServerAckMessage(userId string) *Message { 39 | return &Message{ 40 | MessageType: ServerAck, 41 | Payload: ServerAckPayload{ 42 | UserId: userId, 43 | }, 44 | } 45 | } 46 | 47 | func NewUserLeaveMessage(userId string) *Message { 48 | return &Message{ 49 | MessageType: UserLeave, 50 | Payload: UserLeavePayload{ 51 | UserId: userId, 52 | }, 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /signaling-server/signaling/user.go: -------------------------------------------------------------------------------- 1 | package signaling 2 | 3 | import ( 4 | "sync" 5 | 6 | "github.com/google/uuid" 7 | "github.com/samber/lo" 8 | ) 9 | 10 | type User struct { 11 | Id string 12 | UserName string 13 | Acked bool 14 | } 15 | 16 | var userMap = make(map[string]*User) 17 | var userList = make([]*User, 0) 18 | var userLock = sync.RWMutex{} 19 | 20 | func CreateNewUser() *User { 21 | newUser := User{Id: uuid.NewString()} 22 | 23 | persistUser(&newUser) 24 | 25 | return &newUser 26 | } 27 | 28 | func persistUser(user *User) { 29 | userLock.Lock() 30 | defer userLock.Unlock() 31 | 32 | userMap[user.Id] = user 33 | userList = append(userList, user) 34 | } 35 | 36 | func AckUser(userId string, userName string) { 37 | userLock.Lock() 38 | defer userLock.Unlock() 39 | 40 | if user, ok := userMap[userId]; ok { 41 | user.Acked = true 42 | user.UserName = userName 43 | } 44 | } 45 | 46 | func GetAckedUsers() []*User { 47 | userLock.RLock() 48 | defer userLock.RUnlock() 49 | 50 | return lo.Filter(userList, func(user *User, _ int) bool { 51 | return user.Acked 52 | }) 53 | } 54 | 55 | func RemoveUser(userId string) { 56 | userLock.Lock() 57 | defer userLock.Unlock() 58 | 59 | user := userMap[userId] 60 | delete(userMap, userId) 61 | userList = lo.Without(userList, user) 62 | } 63 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ### Node ### 2 | 3 | # Logs 4 | logs 5 | npm-debug.log* 6 | yarn-debug.log* 7 | yarn-error.log* 8 | 9 | # private keys 10 | *.pem 11 | *.cert 12 | *.key 13 | 14 | # Optional npm cache directory 15 | .npm 16 | 17 | # Dependency directories 18 | /node_modules 19 | /jspm_packages 20 | /bower_components 21 | 22 | # Yarn Integrity file 23 | .yarn-integrity 24 | 25 | # Optional eslint cache 26 | .eslintcache 27 | 28 | # dotenv environment variables file(s) 29 | .env 30 | .env.* 31 | 32 | #Build generated 33 | dist/ 34 | build/ 35 | 36 | 37 | ### SublimeText ### 38 | # cache files for sublime text 39 | *.tmlanguage.cache 40 | *.tmPreferences.cache 41 | *.stTheme.cache 42 | 43 | # workspace files are user-specific 44 | *.sublime-workspace 45 | 46 | # project files should be checked into the repository, unless a significant 47 | # proportion of contributors will probably not be using SublimeText 48 | # *.sublime-project 49 | 50 | 51 | ### VisualStudioCode ### 52 | .vscode/* 53 | !.vscode/settings.json 54 | !.vscode/tasks.json 55 | !.vscode/launch.json 56 | !.vscode/extensions.json 57 | 58 | ### WebStorm/IntelliJ ### 59 | /.idea 60 | modules.xml 61 | *.ipr 62 | 63 | 64 | ### System Files ### 65 | .DS_Store 66 | 67 | # Windows thumbnail cache files 68 | Thumbs.db 69 | ehthumbs.db 70 | ehthumbs_vista.db 71 | 72 | # Folder config file 73 | Desktop.ini 74 | 75 | # Recycle Bin used on file shares 76 | $RECYCLE.BIN/ 77 | 78 | # Thumbnails 79 | ._* 80 | 81 | # Files that might appear in the root of a volume 82 | .DocumentRevisions-V100 83 | .fseventsd 84 | .Spotlight-V100 85 | .TemporaryItems 86 | .Trashes 87 | .VolumeIcon.icns 88 | .com.apple.timemachine.donotpresent -------------------------------------------------------------------------------- /turn-server/turn-server.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "log" 6 | "net" 7 | "os" 8 | "os/signal" 9 | "regexp" 10 | "strconv" 11 | "syscall" 12 | 13 | "github.com/pion/turn/v2" 14 | ) 15 | 16 | func main() { 17 | publicIP := flag.String("public-ip", "", "IP Address that TURN can be contacted by.") 18 | port := flag.Int("port", 3478, "Listening port.") 19 | users := flag.String("users", "turn=pion", "List of username and password (e.g. \"user=pass,user=pass\")") 20 | realm := flag.String("realm", "localhost", "Realm") 21 | flag.Parse() 22 | 23 | udpListener, err := net.ListenPacket("udp4", "0.0.0.0:"+strconv.Itoa(*port)) 24 | if err != nil { 25 | log.Panicf("Failed to create TURN server listener: %s", err) 26 | } 27 | 28 | usersMap := map[string][]byte{} 29 | for _, kv := range regexp.MustCompile(`(\w+)=(\w+)`).FindAllStringSubmatch(*users, -1) { 30 | usersMap[kv[1]] = turn.GenerateAuthKey(kv[1], *realm, kv[2]) 31 | } 32 | 33 | s, err := turn.NewServer(turn.ServerConfig{ 34 | Realm: *realm, 35 | AuthHandler: func(username string, realm string, srcAddr net.Addr) ([]byte, bool) { 36 | if key, ok := usersMap[username]; ok { 37 | return key, true 38 | } 39 | return nil, false 40 | }, 41 | PacketConnConfigs: []turn.PacketConnConfig{ 42 | { 43 | PacketConn: udpListener, 44 | RelayAddressGenerator: &turn.RelayAddressGeneratorStatic{ 45 | RelayAddress: net.ParseIP(*publicIP), 46 | Address: "0.0.0.0", 47 | }, 48 | }, 49 | }, 50 | }) 51 | 52 | if err != nil { 53 | log.Panic(err) 54 | } 55 | 56 | sigs := make(chan os.Signal, 1) 57 | signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM) 58 | <-sigs 59 | 60 | if err = s.Close(); err != nil { 61 | log.Panic(err) 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /client/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | WebRTC Video Chat 5 | 6 | 7 | 8 | 9 |

WebRTC Video Chat

10 | 11 | 17 | 18 |
19 |
20 | 21 | 22 |
23 | 24 | 25 |
26 | 27 | 28 |
29 | 30 | 31 |
32 | 33 | 34 |
35 | 36 | 37 |
38 | 39 |
40 | 41 | 42 |
43 | 44 |
45 | 46 |
47 | 48 |
49 | 50 | 51 | 52 | 53 | 54 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # WebRTC Video Chat 2 | 3 | Very basic and somewhat buggy reference implementation of multi-user, WebRTC video chat in the browser. This was developed for a meetup. I don't find slides very helpful in general, but if you want them, they are [here](https://slides.com/haydenbraxton/webrtc-video-chat). The last page of the slides has some links to some helpful learning resources, so do check that out. 4 | 5 | To start the signaling server and client server, run `npm start`. You can open the page at `http://localhost:5500/`. 6 | 7 | For the turn-server, 8 | 9 | ```bash 10 | cd ./turn-server 11 | ./build.sh 12 | ./build/turn-server --public-ip= 13 | ``` 14 | 15 | When running locally, you can test by opening the page in different browser tabs. You can also test connecting from different devices on the same local network if you're using https. 16 | 17 | command args: 18 | 19 | - `--ssl-cert-path` path to ssl cert (not required when running locally) 20 | - `--ssl-key-path` path to ssl key (not required when running locally) 21 | - run node commands with `--prod` to use proper port numbers 22 | - run turn server with `--public-ip ` (set to your local ip address when running locally) 23 | 24 | ## Some ideas for enhancements if you want to tinker: 25 | 26 | - mute/unmute 27 | - turn camera on/off 28 | - switch camera source 29 | - allow participants to rename themselves 30 | - share screen (hint: https://developer.mozilla.org/en-US/docs/Web/API/MediaDevices/getDisplayMedia) 31 | - live chat (not really WebRTC, but could still be fun to try if you're new to web sockets) 32 | - try forcing the peers to use a specific ICE candidate pair by filtering what ice candidates you send over the signaling server. 33 | - share files or other data over WebRTC (hint: https://developer.mozilla.org/en-US/docs/Web/API/RTCPeerConnection/createDataChannel) 34 | - experiment with different signaling server implementations. I built the simplest possible thing that would work, but you could use any technology or messaging system you want. -------------------------------------------------------------------------------- /signaling-server/example/client.go: -------------------------------------------------------------------------------- 1 | // Copyright 2015 The Gorilla WebSocket Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | //go:build ignore 6 | // +build ignore 7 | 8 | package example 9 | 10 | import ( 11 | "flag" 12 | "log" 13 | "net/url" 14 | "os" 15 | "os/signal" 16 | "time" 17 | 18 | "github.com/gorilla/websocket" 19 | ) 20 | 21 | var addr = flag.String("addr", "localhost:8080", "http service address") 22 | 23 | func main() { 24 | flag.Parse() 25 | log.SetFlags(0) 26 | 27 | interrupt := make(chan os.Signal, 1) 28 | signal.Notify(interrupt, os.Interrupt) 29 | 30 | u := url.URL{Scheme: "ws", Host: *addr, Path: "/echo"} 31 | log.Printf("connecting to %s", u.String()) 32 | 33 | c, _, err := websocket.DefaultDialer.Dial(u.String(), nil) 34 | if err != nil { 35 | log.Fatal("dial:", err) 36 | } 37 | defer c.Close() 38 | 39 | done := make(chan struct{}) 40 | 41 | go func() { 42 | defer close(done) 43 | for { 44 | _, message, err := c.ReadMessage() 45 | if err != nil { 46 | log.Println("read:", err) 47 | return 48 | } 49 | log.Printf("recv: %s", message) 50 | } 51 | }() 52 | 53 | ticker := time.NewTicker(time.Second) 54 | defer ticker.Stop() 55 | 56 | for { 57 | select { 58 | case <-done: 59 | return 60 | case t := <-ticker.C: 61 | err := c.WriteMessage(websocket.TextMessage, []byte(t.String())) 62 | if err != nil { 63 | log.Println("write:", err) 64 | return 65 | } 66 | case <-interrupt: 67 | log.Println("interrupt") 68 | 69 | // Cleanly close the connection by sending a close message and then 70 | // waiting (with timeout) for the server to close the connection. 71 | err := c.WriteMessage(websocket.CloseMessage, websocket.FormatCloseMessage(websocket.CloseNormalClosure, "")) 72 | if err != nil { 73 | log.Println("write close:", err) 74 | return 75 | } 76 | select { 77 | case <-done: 78 | case <-time.After(time.Second): 79 | } 80 | return 81 | } 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /client/webrtc-util.js: -------------------------------------------------------------------------------- 1 | export function closePeerConnection(event, peerConnection) { 2 | if (!peerConnection) { 3 | return 4 | } 5 | 6 | // Disconnect all our event listeners; we don't want stray events 7 | // to interfere with the hangup while it's ongoing. 8 | peerConnection.ontrack = null; 9 | peerConnection.onnicecandidate = null; 10 | peerConnection.oniceconnectionstatechange = null; 11 | peerConnection.onsignalingstatechange = null; 12 | peerConnection.onicegatheringstatechange = null; 13 | peerConnection.onnotificationneeded = null; 14 | 15 | // Stop all transceivers on the connection 16 | peerConnection.getTransceivers().forEach(transceiver => { 17 | transceiver.stop(); 18 | }); 19 | 20 | peerConnection.close() 21 | } 22 | 23 | export function createPeerConnection(config = { 24 | peer: undefined, 25 | localMediaStream: undefined, 26 | onicecandidate: (event, peerContext) => {}, 27 | oniceconnectionstatechange: (event, peerContext) => {}, 28 | onsignalingstatechange: (event, peerContext) => {}, 29 | onnegotiationneeded: (event, peerContext) => {}, 30 | ontrack: (event, peerContext) => { }, 31 | iceServers: [] 32 | }) { 33 | // TODO: IMPLEMENT START 34 | let peerConnection = new RTCPeerConnection({ iceServers: config.iceServers }) 35 | 36 | config.localMediaStream 37 | .getTracks() 38 | .forEach(track => peerConnection.addTransceiver(track, { streams: [config.localMediaStream] })) 39 | 40 | const peerContext = { 41 | peerConnection, 42 | peer: config.peer 43 | } 44 | // TODO: IMPLEMENT END 45 | 46 | peerConnection.onicecandidate = withPeerContext(config.onicecandidate, peerContext); 47 | peerConnection.oniceconnectionstatechange = withPeerContext(config.oniceconnectionstatechange, peerContext); 48 | peerConnection.onsignalingstatechange = withPeerContext(config.onsignalingstatechange, peerContext); 49 | peerConnection.onnegotiationneeded = withPeerContext(config.onnegotiationneeded, peerContext); 50 | peerConnection.ontrack = withPeerContext(config.ontrack, peerContext); 51 | 52 | return peerConnection 53 | } 54 | 55 | function withPeerContext(callback, peerContext) { 56 | return (event) => callback(event, peerContext) 57 | } 58 | -------------------------------------------------------------------------------- /signaling-server/signaling-server.js: -------------------------------------------------------------------------------- 1 | import WebSocket from 'ws' 2 | import * as uuid from 'uuid'; 3 | import { createServer as createHttpServer } from 'http' 4 | import { createServer as createHttpsServer } from 'https' 5 | import queue from 'queue' 6 | import { messageTypes } from '../shared/message-types.js' 7 | import { sslConfig } from '../ssl-config.js' 8 | 9 | const isProd = !!process.argv.includes('--prod') 10 | let httpServer = isProd 11 | ? createHttpsServer(sslConfig) 12 | : createHttpServer() 13 | 14 | const webSocketServer = new WebSocket.Server({ server: httpServer }); 15 | 16 | const users = {} 17 | const joinRequestQueue = queue({ concurrency: 1, autostart: true }) 18 | 19 | webSocketServer.on('connection', (connection) => { 20 | let user = { 21 | connection, 22 | userId: uuid.v4() 23 | } 24 | 25 | users[user.userId] = user 26 | 27 | sendToUser(user, { type: messageTypes.signalServerConnected, userId: user.userId }) 28 | 29 | connection.onmessage = (event) => { 30 | let message = JSON.parse(event.data) 31 | handleMessage(message) 32 | } 33 | 34 | connection.onclose = () => { 35 | delete users[user.userId] 36 | sendUpdatedUserList() 37 | } 38 | }); 39 | 40 | function handleMessage(message) { 41 | if (message.type === messageTypes.join) { 42 | joinRequestQueue.push(() => handleJoin(message)) 43 | } else if (message.senderId && message.recipientId) { 44 | let recipient = users[message.recipientId] 45 | sendToUser(recipient, message) 46 | } 47 | } 48 | 49 | function handleJoin(message) { 50 | let user = users[message.senderId] 51 | 52 | users[message.senderId] = { 53 | ...user, 54 | userName: message.userName 55 | } 56 | 57 | return sendUpdatedUserList() 58 | } 59 | 60 | function sendUpdatedUserList() { 61 | return sendToAllUsers({ 62 | type: messageTypes.userList, 63 | users: getUserList() 64 | }) 65 | } 66 | 67 | function getUserList() { 68 | return Object.values(users).map(user => ({ 69 | userName: user.userName, 70 | userId: user.userId 71 | })) 72 | } 73 | 74 | function sendToUser(user, message) { 75 | return new Promise((resolve, reject) => 76 | user.connection.send( 77 | JSON.stringify(message), 78 | (error) => error ? reject(error) : resolve() 79 | ) 80 | ) 81 | } 82 | 83 | function sendToAllUsers(message) { 84 | return Promise.all( 85 | Object 86 | .values(users) 87 | .map(user => 88 | sendToUser(user, message) 89 | .catch(error => console.log(error)) 90 | ) 91 | ) 92 | } 93 | 94 | httpServer.listen({ 95 | host: '0.0.0.0', 96 | port: isProd ? 444 : 5501 97 | }) -------------------------------------------------------------------------------- /turn-server/go.sum: -------------------------------------------------------------------------------- 1 | github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= 2 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 3 | github.com/pion/logging v0.2.2 h1:M9+AIj/+pxNsDfAT64+MAVgJO0rsyLnoJKCqf//DoeY= 4 | github.com/pion/logging v0.2.2/go.mod h1:k0/tDVsRCX2Mb2ZEmTqNa7CWsQPc+YYCB7Q+5pahoms= 5 | github.com/pion/randutil v0.1.0 h1:CFG1UdESneORglEsnimhUjf33Rwjubwj6xfiOXBa3mA= 6 | github.com/pion/randutil v0.1.0/go.mod h1:XcJrSMMbbMRhASFVOlj/5hQial/Y8oH/HVo7TBZq+j8= 7 | github.com/pion/stun v0.3.5 h1:uLUCBCkQby4S1cf6CGuR9QrVOKcvUwFeemaC865QHDg= 8 | github.com/pion/stun v0.3.5/go.mod h1:gDMim+47EeEtfWogA37n6qXZS88L5V6LqFcf+DZA2UA= 9 | github.com/pion/transport v0.10.1 h1:2W+yJT+0mOQ160ThZYUx5Zp2skzshiNgxrNE9GUfhJM= 10 | github.com/pion/transport v0.10.1/go.mod h1:PBis1stIILMiis0PewDw91WJeLJkyIMcEk+DwKOzf4A= 11 | github.com/pion/turn/v2 v2.0.5 h1:iwMHqDfPEDEOFzwWKT56eFmh6DYC6o/+xnLAEzgISbA= 12 | github.com/pion/turn/v2 v2.0.5/go.mod h1:APg43CFyt/14Uy7heYUOGWdkem/Wu4PhCO/bjyrTqMw= 13 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 14 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 15 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 16 | github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0= 17 | github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 18 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 19 | golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 20 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 21 | golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= 22 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 23 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 24 | golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 25 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 26 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 27 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= 28 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 29 | -------------------------------------------------------------------------------- /signaling-server/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/json" 5 | "flag" 6 | "fmt" 7 | "log" 8 | "net/http" 9 | 10 | "github.com/gorilla/websocket" 11 | 12 | "github.com/haydenbr/sigserver/signaling" 13 | ) 14 | 15 | var host = flag.String("host", "0.0.0.0", "http host") 16 | var port = flag.String("port", "8080", "http port") 17 | var upgrader = websocket.Upgrader{} // use default options 18 | 19 | func handleConnection(c *websocket.Conn) string { 20 | newUser := signaling.CreateNewUser() 21 | 22 | sendServerAck(c, newUser) 23 | sendUserList(c) 24 | 25 | return newUser.Id 26 | } 27 | 28 | func sendServerAck(c *websocket.Conn, user *signaling.User) { 29 | wsWriteErr := c.WriteJSON(signaling.NewServerAckMessage(user.Id)) 30 | 31 | if wsWriteErr != nil { 32 | fmt.Println("error sending server ack message:", wsWriteErr) 33 | } 34 | } 35 | 36 | func sendUserList(c *websocket.Conn) { 37 | c.WriteJSON(signaling.GetAckedUsers()) 38 | } 39 | 40 | func sendNewUserNotification() { 41 | // notify all other users (acked and not acked) that there's a new user 42 | // we need to do this in a way that doesn't require us to save a pointer to each user's ws connection on the user object 43 | } 44 | 45 | func handleClientAck(c *websocket.Conn, clientAck *signaling.ClientAckPayload) { 46 | signaling.AckUser(clientAck.UserId, clientAck.UserName) 47 | sendNewUserNotification() 48 | } 49 | 50 | func sendUserLeave(c *websocket.Conn, userId string) { 51 | wsWriteErr := c.WriteJSON(signaling.Message{ 52 | MessageType: signaling.ServerAck, 53 | Payload: signaling.ServerAckPayload{ 54 | UserId: userId, 55 | }, 56 | }) 57 | 58 | if wsWriteErr != nil { 59 | fmt.Println("error sending server ack message:", wsWriteErr) 60 | } 61 | } 62 | 63 | func signal(w http.ResponseWriter, r *http.Request) { 64 | c, err := upgrader.Upgrade(w, r, nil) 65 | if err != nil { 66 | log.Print("upgrade:", err) 67 | return 68 | } 69 | defer c.Close() 70 | 71 | userId := handleConnection(c) 72 | 73 | for { 74 | messageType, message, readErr := c.ReadMessage() 75 | 76 | if readErr != nil { 77 | log.Println("read:", readErr) 78 | break 79 | } 80 | 81 | if messageType == websocket.CloseMessage { 82 | signaling.RemoveUser(userId) 83 | } 84 | 85 | if messageType == websocket.TextMessage { 86 | messageJson := new(signaling.Message) 87 | jsonErr := json.Unmarshal(message, &messageJson) 88 | 89 | if jsonErr != nil { 90 | log.Println("json parse error:", jsonErr) 91 | } 92 | 93 | // TODO: handle message, actually 94 | } 95 | 96 | log.Printf("unhandled message type: %d %v\n", messageType, message) 97 | } 98 | } 99 | 100 | func main() { 101 | flag.Parse() 102 | log.SetFlags(0) 103 | 104 | http.HandleFunc("/", signal) 105 | 106 | addr := *host + *port 107 | log.Fatal(http.ListenAndServe(addr, nil)) 108 | } 109 | -------------------------------------------------------------------------------- /signaling-server/example/server.go: -------------------------------------------------------------------------------- 1 | // Copyright 2015 The Gorilla WebSocket Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | //go:build ignore 6 | // +build ignore 7 | 8 | package example 9 | 10 | import ( 11 | "flag" 12 | "html/template" 13 | "log" 14 | "net/http" 15 | 16 | "github.com/gorilla/websocket" 17 | ) 18 | 19 | var addr = flag.String("addr", "localhost:8080", "http service address") 20 | 21 | var upgrader = websocket.Upgrader{} // use default options 22 | 23 | func echo(w http.ResponseWriter, r *http.Request) { 24 | c, err := upgrader.Upgrade(w, r, nil) 25 | if err != nil { 26 | log.Print("upgrade:", err) 27 | return 28 | } 29 | defer c.Close() 30 | for { 31 | mt, message, err := c.ReadMessage() 32 | if err != nil { 33 | log.Println("read:", err) 34 | break 35 | } 36 | log.Printf("recv: %s", message) 37 | err = c.WriteMessage(mt, message) 38 | if err != nil { 39 | log.Println("write:", err) 40 | break 41 | } 42 | } 43 | } 44 | 45 | func home(w http.ResponseWriter, r *http.Request) { 46 | homeTemplate.Execute(w, "ws://"+r.Host+"/echo") 47 | } 48 | 49 | func main() { 50 | flag.Parse() 51 | log.SetFlags(0) 52 | http.HandleFunc("/echo", echo) 53 | http.HandleFunc("/", home) 54 | log.Fatal(http.ListenAndServe(*addr, nil)) 55 | } 56 | 57 | var homeTemplate = template.Must(template.New("").Parse(` 58 | 59 | 60 | 61 | 62 | 116 | 117 | 118 | 119 |
120 |

Click "Open" to create a connection to the server, 121 | "Send" to send a message to the server and "Close" to close the connection. 122 | You can change the message and send multiple times. 123 |

124 |

125 | 126 | 127 |

128 | 129 |

130 |
131 |
132 |
133 | 134 | 135 | `)) 136 | -------------------------------------------------------------------------------- /client/template-util.js: -------------------------------------------------------------------------------- 1 | export function insertVideoTemplate(config = { 2 | label: '', 3 | mediaStream: undefined, 4 | parent: undefined, 5 | muted: false, 6 | videoId: '' 7 | }) { 8 | let newVideoTemplate = getNewVideoTemplate() 9 | 10 | if (config.videoId) { 11 | newVideoTemplate.firstElementChild.setAttribute('id', getVideoId(config.videoId)) 12 | } 13 | 14 | let newVideo = newVideoTemplate.querySelector('video') 15 | 16 | if (config.mediaStream) { 17 | newVideo.srcObject = config.mediaStream; 18 | } 19 | 20 | if (config.muted) { 21 | newVideo.muted = true 22 | } 23 | 24 | let videoLabel = newVideoTemplate.querySelector('.video-label') 25 | 26 | if (config.label) { 27 | videoLabel.textContent = config.label 28 | } 29 | 30 | config.parent.appendChild(newVideoTemplate); 31 | 32 | return newVideoTemplate 33 | } 34 | 35 | export function setPeerVideoMediaStream(videoId, mediaStream) { 36 | if (!videoId || !mediaStream) { 37 | return 38 | } 39 | 40 | let peerVideoTemplate = getPeerVideoTemplate(videoId) 41 | let videoEl = peerVideoTemplate.querySelector('video') 42 | videoEl.srcObject = mediaStream 43 | } 44 | 45 | export function removePeerVideoTemplate(videoId = '') { 46 | let videoTemplate = getPeerVideoTemplate(videoId) 47 | 48 | if (videoTemplate) { 49 | videoTemplate.srcObject = null 50 | videoTemplate.remove(); 51 | } 52 | } 53 | 54 | export function initSettingsForm(config = { onSubmit: () => { } }) { 55 | let form = getCallSettingsForm() 56 | let formElements = form.elements 57 | 58 | form.onsubmit = () => { 59 | config.onSubmit() 60 | form.onsubmit = undefined 61 | } 62 | 63 | let localSettingsButton = document.querySelector('#local-settings') 64 | let prodSettingsButton = document.querySelector('#prod-settings') 65 | 66 | localSettingsButton.onclick = () => { 67 | formElements['signaling-server'].value = 'ws://localhost:5501' 68 | formElements['stun-server'].value = '' 69 | formElements['turn-server'].value = '' 70 | formElements['turn-user-name'].value = '' 71 | formElements['turn-password'].value = '' 72 | } 73 | 74 | prodSettingsButton.onclick = () => { 75 | formElements['signaling-server'].value = 'wss://webrtc.haydenbraxton.com:444' 76 | formElements['stun-server'].value = 'stun:webrtc.haydenbraxton.com:3478' 77 | formElements['turn-server'].value = 'turn:webrtc.haydenbraxton.com:3478' 78 | formElements['turn-user-name'].value = 'turn' 79 | formElements['turn-password'].value = 'pion' 80 | } 81 | } 82 | 83 | export function hideCallSettings() { 84 | hideElement(getCallSettingsContainer()) 85 | document.querySelector('#local-settings').onclick = null 86 | document.querySelector('#prod-settings').onclick = null 87 | } 88 | 89 | export function getCallSettings() { 90 | let formElements = getCallSettingsForm().elements 91 | return { 92 | signalingServer: formElements['signaling-server'].value, 93 | userName: formElements['user-name'].value, 94 | stunServer: formElements['stun-server'].value, 95 | turnServer: formElements['turn-server'].value, 96 | turnUserName: formElements['turn-user-name'].value, 97 | turnPassword: formElements['turn-password'].value, 98 | } 99 | } 100 | 101 | export function getLocalVideoContainer() { 102 | return document.body.querySelector('#local-video-container') 103 | } 104 | 105 | export function getPeerVideoContainer() { 106 | return document.body.querySelector('#peer-video-container') 107 | } 108 | 109 | function getNewVideoTemplate() { 110 | const videoTemplate = document.querySelector('#video-template'); 111 | return videoTemplate.content.cloneNode(true) 112 | } 113 | 114 | function getPeerVideoTemplate(videoId = '') { 115 | return document.querySelector('#' + getVideoId(videoId)) 116 | } 117 | 118 | function getCallSettingsForm() { 119 | return document.querySelector('#call-settings form'); 120 | } 121 | 122 | function getCallSettingsContainer() { 123 | return document.querySelector('#call-settings'); 124 | } 125 | 126 | function hideElement(element) { 127 | element.style.setProperty('display', 'none') 128 | } 129 | 130 | function getVideoId(userId) { 131 | return `user_${userId}` 132 | } 133 | -------------------------------------------------------------------------------- /client/video-chat-main.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import { messageTypes } from '/shared/message-types.js' 4 | import { 5 | getCallSettings, 6 | insertVideoTemplate, 7 | getLocalVideoContainer, 8 | getPeerVideoContainer, 9 | setPeerVideoMediaStream, 10 | removePeerVideoTemplate, 11 | initSettingsForm, 12 | hideCallSettings, 13 | } from './template-util.js' 14 | import { 15 | connectToSignalingServer, 16 | sendSignalMessage as _sendSignalMessage, 17 | } from './signaling-server-connection.js' 18 | import { getUserMedia } from './user-media.js' 19 | import { closePeerConnection, createPeerConnection } from './webrtc-util.js' 20 | import { getIceServers } from './ice-servers.js'; 21 | 22 | let state = { 23 | localMediaStream: undefined, 24 | peers: {}, // {[userId]: { userName: '', userId: '' }} 25 | currentUser: { 26 | userName: '', 27 | userId: '' 28 | }, 29 | isNewUser: true, 30 | } 31 | 32 | document.addEventListener('DOMContentLoaded', () => initSettingsForm({ onSubmit: () => joinCall() })) 33 | 34 | async function joinCall() { 35 | await connectToSignalingServer(getCallSettings().signalingServer, messageHandlers) 36 | 37 | state.currentUser.userName = getCallSettings().userName 38 | hideCallSettings() 39 | 40 | await initLocalVideo(`${state.currentUser.userName} (Me)`) 41 | sendJoinMessage() 42 | } 43 | 44 | async function initLocalVideo(label) { 45 | // TODO: IMPLEMENT 46 | state.localMediaStream = await getUserMedia() 47 | 48 | insertVideoTemplate({ 49 | label: label, 50 | mediaStream: state.localMediaStream, 51 | muted: true, 52 | parent: getLocalVideoContainer() 53 | }) 54 | } 55 | 56 | function sendJoinMessage() { 57 | sendSignalMessage({ 58 | type: messageTypes.join, 59 | userName: state.currentUser.userName 60 | }) 61 | } 62 | 63 | const messageHandlers = { 64 | [messageTypes.signalServerConnected]: saveUserId, 65 | [messageTypes.userList]: updateUserList, 66 | [messageTypes.offer]: respondToOffer, 67 | [messageTypes.answer]: saveSdpAnswer, 68 | [messageTypes.iceCandidate]: addIceCandidate 69 | } 70 | 71 | function saveUserId(message = { userId: '' }) { 72 | state.currentUser.userId = message.userId 73 | } 74 | 75 | function updateUserList(message = { users: [{ userId: '', userName: '' }] }) { 76 | message.users.forEach(u => { 77 | if (u.userId !== state.currentUser.userId && !state.peers[u.userId]) { 78 | state.peers[u.userId] = u 79 | } 80 | }) 81 | 82 | if (state.isNewUser) { 83 | state.isNewUser = false 84 | callPeers() 85 | } 86 | } 87 | 88 | function callPeers() { 89 | Object.values(state.peers).forEach(peer => initPeerConnection(peer)) 90 | } 91 | 92 | function initPeerConnection(peer) { 93 | // TODO: IMPLEMENT 94 | insertVideoTemplate({ 95 | label: peer.userName, 96 | parent: getPeerVideoContainer(), 97 | videoId: peer.userId 98 | }) 99 | let peerConnection = createPeerConnection({ 100 | peer, 101 | localMediaStream: state.localMediaStream, 102 | // when our local ICE agent finds a candidate 103 | onicecandidate: sendIceCandidateToPeer, 104 | oniceconnectionstatechange: handleICEConnectionStateChangeEvent, 105 | onsignalingstatechange: handleSignalingStateChangeEvent, 106 | // this starts the calling process 107 | // this event is triggered when you add a tranceiver 108 | onnegotiationneeded: createOffer, 109 | // we get peer media here 110 | ontrack: displayPeerMedia, 111 | iceServers: getIceServers() 112 | }) 113 | 114 | state.peers[peer.userId] = { 115 | ...peer, 116 | peerConnection 117 | } 118 | 119 | return peerConnection 120 | } 121 | 122 | function sendIceCandidateToPeer(event, peerContext) { 123 | // TODO: IMPLEMENT 124 | if (event.candidate) { 125 | sendSignalMessage({ 126 | type: messageTypes.iceCandidate, 127 | recipientId: peerContext.peer.userId, 128 | candidate: event.candidate 129 | }); 130 | } 131 | } 132 | 133 | function handleICEConnectionStateChangeEvent(event, peerContext) { 134 | if (['closed', 'failed', 'disconnected'].includes(peerContext.peerConnection.iceConnectionState)) { 135 | disposePeerConnection(peerContext) 136 | } 137 | } 138 | 139 | function handleSignalingStateChangeEvent(event, peerContext) { 140 | if (peerContext.peerConnection.signalingState === 'closed') { 141 | disposePeerConnection(peerContext) 142 | } 143 | } 144 | 145 | function disposePeerConnection(peerContext) { 146 | removePeerVideoTemplate(peerContext.peer.userId); 147 | closePeerConnection(peerContext.peerConnection); 148 | } 149 | 150 | async function createOffer(event, peerContext) { 151 | // TODO: IMPLEMENT 152 | let { peerConnection, peer } = peerContext 153 | const offer = await peerConnection.createOffer(); 154 | 155 | // if signaling state is not 'stable', then it means 156 | // we're already in the process of resolving local/remote SDPs 157 | // we don't want to create another offer in this case 158 | if (peerConnection.signalingState !== 'stable') { 159 | return; 160 | } 161 | 162 | await peerConnection.setLocalDescription(offer); 163 | 164 | sendSignalMessage({ 165 | recipientId: peer.userId, 166 | type: messageTypes.offer, 167 | sdp: peerConnection.localDescription 168 | }); 169 | } 170 | 171 | function displayPeerMedia(event, peerContext) { 172 | // TODO: IMPLEMENT 173 | setPeerVideoMediaStream(peerContext.peer.userId, event.streams[0]) 174 | } 175 | 176 | async function respondToOffer(message = { 177 | senderId: '', 178 | sdp: '' 179 | }) { 180 | // TODO: IMPLEMENT 181 | let peer = state.peers[message.senderId]; 182 | let peerConnection = peer.peerConnection || initPeerConnection(peer); 183 | 184 | let remoteSdp = new RTCSessionDescription(message.sdp); 185 | 186 | if (peerConnection.signalingState !== 'stable') { 187 | await Promise.all([ 188 | peerConnection.setLocalDescription({type: 'rollback'}), 189 | peerConnection.setRemoteDescription(remoteSdp) 190 | ]); 191 | return; 192 | } else { 193 | await peerConnection.setRemoteDescription(remoteSdp); 194 | } 195 | 196 | await peerConnection.setLocalDescription(await peerConnection.createAnswer()); 197 | 198 | sendSignalMessage({ 199 | recipientId: message.senderId, 200 | type: messageTypes.answer, 201 | sdp: peerConnection.localDescription 202 | }); 203 | } 204 | 205 | async function saveSdpAnswer(message = { 206 | senderId: '', 207 | sdp: '' 208 | }) { 209 | // TODO: IMPLEMENT 210 | let { peerConnection } = state.peers[message.senderId]; 211 | let remoteSdp = new RTCSessionDescription(message.sdp); 212 | 213 | await peerConnection.setRemoteDescription(remoteSdp); 214 | } 215 | 216 | async function addIceCandidate(message = { 217 | senderId: '', 218 | candidate: '' 219 | }) { 220 | // TODO: IMPLEMENT 221 | let { peerConnection } = state.peers[message.senderId]; 222 | let candidate = new RTCIceCandidate(message.candidate); 223 | 224 | await peerConnection.addIceCandidate(candidate) 225 | } 226 | 227 | function sendSignalMessage(message = { 228 | recipientId: '', 229 | type: '' 230 | }) { 231 | _sendSignalMessage({ 232 | senderId: state.currentUser.userId, 233 | ...message 234 | }) 235 | } --------------------------------------------------------------------------------