├── .gitattributes ├── .gitignore ├── .travis.yml ├── LICENSE ├── Makefile ├── README.md ├── conf └── test.config ├── examples ├── multi │ ├── .gitignore │ ├── Makefile │ ├── README.md │ ├── conf │ │ └── sys.config │ ├── priv │ │ ├── css │ │ │ └── main.css │ │ ├── index.html │ │ └── js │ │ │ ├── adapter-latest.js │ │ │ └── main.js │ ├── rebar.config │ ├── rebar.lock │ └── src │ │ ├── callbacks.erl │ │ ├── example.app.src │ │ ├── example_app.erl │ │ └── example_sup.erl └── simple │ ├── .gitignore │ ├── Makefile │ ├── README.md │ ├── conf │ └── sys.config │ ├── priv │ ├── css │ │ └── main.css │ ├── index.html │ └── js │ │ ├── adapter-latest.js │ │ └── main.js │ ├── rebar.config │ ├── rebar.lock │ └── src │ ├── callbacks.erl │ ├── example.app.src │ ├── example_app.erl │ └── example_sup.erl ├── priv └── certs │ ├── certificate.pem │ └── key.pem ├── rebar.config ├── rebar.lock ├── rebar3 ├── src ├── webrtc_server.app.src ├── webrtc_server.erl ├── webrtc_server_app.erl ├── webrtc_server_sup.erl ├── webrtc_utils.erl └── webrtc_ws_handler.erl └── test ├── webrtc_ws_handler_SUITE.erl └── ws_client.erl /.gitattributes: -------------------------------------------------------------------------------- 1 | example/* linguist-vendored 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .rebar3 2 | _* 3 | .eunit 4 | *.o 5 | *.beam 6 | *.plt 7 | *.swp 8 | *.swo 9 | .erlang.cookie 10 | ebin 11 | log 12 | erl_crash.dump 13 | .rebar 14 | logs 15 | _build 16 | .idea 17 | rebar3.crashdump 18 | ct_log 19 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: erlang 2 | otp_release: 3 | - 20.0 4 | 5 | script: 6 | - make test 7 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 LambdaClass 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: dev test 2 | 3 | test: 4 | ./rebar3 ct 5 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # webrtc-server [![Build Status](https://travis-ci.org/lambdaclass/webrtc-server.svg?branch=master)](https://travis-ci.org/lambdaclass/webrtc-server) 2 | 3 | An Erlang/OTP application that provides all the server side components to make video calls using [WebRTC](https://webrtc.org/). 4 | ## Usage 5 | 6 | Add `webrtc_server` as a dependency, for example using rebar3: 7 | 8 | ``` erlang 9 | {deps, [ 10 | {cowboy, "2.0.0"}, 11 | {webrtc_server, {git, "https://github.com/lambdaclass/webrtc-server", {ref, "56bce3"}}} 12 | ]} 13 | ``` 14 | 15 | `webrtc_server` provides a single Cowboy WebSocket handler 16 | `webrtc_ws_handler` that acts as the Signaling Server to pass data 17 | between peers. Add the handler to your cowboy 2.0 application: 18 | 19 | ``` erlang 20 | Dispatch = cowboy_router:compile([{'_', [{"/websocket/:room", webrtc_ws_handler, []}]}]), 21 | {ok, _} = cowboy:start_tls(my_http_listener, 22 | [{port, config(port)}, 23 | {certfile, config(certfile)}, 24 | {keyfile, config(certkey)}], 25 | #{env => #{dispatch => Dispatch}}). 26 | ``` 27 | 28 | Note the handler expects a `:room` parameter to group connecting 29 | clients into different rooms. 30 | 31 | In addition to the Signaling server, `webrtc_server` starts a 32 | STUN/TURN server on port 3478 33 | using [processone/stun](https://github.com/processone/stun), which can 34 | be used as ICE servers by the WebRTC peers. A browser client can use it like: 35 | 36 | ``` javascript 37 | var pc = new RTCPeerConnection({ 38 | iceServers: [{ 39 | urls: "stun:example.com:3478" 40 | },{ 41 | urls: "turn:example.com:3478", 42 | username: "username", 43 | credential: "password" 44 | }] 45 | }); 46 | ``` 47 | 48 | The [examples/simple](https://github.com/lambdaclass/webrtc-server/tree/master/examples/simple) 49 | directory contains a full Cowboy application using `webrtc_server` and a 50 | browser client that establishes a WebRTC connection using the 51 | Signaling and ICE servers. 52 | 53 | ## Configuration 54 | ### authentication 55 | 56 | An authentication function needs to be provided to the app 57 | environment to authenticate both the websocket 58 | connections to the signaling server and the TURN connections. 59 | 60 | Example: 61 | 62 | ``` erlang 63 | {auth_fun, {module, function}} 64 | ``` 65 | 66 | This function will ba called like `module:function(Username)`, and 67 | should return the expected password for the given Username. The 68 | password will be compared to the one sent by the client. Authentication will be 69 | considered failed if the result value is not a binary or the function 70 | throws an error. 71 | 72 | The implementation of the function will depend on how the webrtc 73 | application is expected to be deployed. It could just return a fixed 74 | password from configuration, encode the username using a secret shared 75 | between the webtrtc and application servers, lookup the password on a common 76 | datastore, etc. 77 | 78 | ### callbacks 79 | webrtc_server allows to define callback functions that will be 80 | triggered when users enter or leave a room. This can be useful to 81 | track conversation state (such as when a call starts or ends), without 82 | needing extra work from the clients. 83 | 84 | ``` erlang 85 | {join_callback, {module, function}} 86 | {leave_callback, {module, function}} 87 | ``` 88 | 89 | Both callbacks receive the same arguments: 90 | 91 | * Room: name of the room used to connect. 92 | * Username: username provided by the client executing the action. 93 | * OtherUsers = [{Username, PeerId}]: list of the rest of the usernames currently in the room. 94 | 95 | ### server configuration 96 | 97 | * certfile: path to the certificate file for the STUN server. 98 | * keyfile: path to the key file for for the STUN server. 99 | * hostname: webrtc server hostname. Will be used as the `auth_realm` 100 | for TURN and to lookup the `turn_ip` if it's not provided. 101 | * turn_ip: IP of the webrtc server. If not provided, will default to 102 | the first result of `inet_res:lookup(Hostname, in, a)`. 103 | * idle_timeout: [Cowboy option](https://ninenines.eu/docs/en/cowboy/2.0/manual/cowboy_websocket/#_opts) for the websocket 104 | connections. By default will disconnect idle sockets after a 105 | minute (thus requiring the clients to periodically send a ping message). Use 106 | `infinity` to disable idle timeouts. 107 | 108 | ## Signaling API reference 109 | 110 | The signaling API is used by web socket clients to exchange the 111 | necessary information to establish a WebRTC peer connection. See 112 | the 113 | [examples](https://github.com/lambdaclass/webrtc-server/tree/master/examples) 114 | for context on how this API is used. 115 | 116 | ### Authentication 117 | After connection, an authentication JSON message should be sent: 118 | 119 | ``` json 120 | { 121 | "event": "authenticate", 122 | "data": { 123 | "username": "john", 124 | "password": "s3cr3t!" 125 | } 126 | } 127 | ``` 128 | 129 | The server will assign a `peer_id` to the client and reply: 130 | 131 | ``` json 132 | { 133 | "event": "authenticated", 134 | "data": { 135 | "peer_id": "bxCBrwyL3Ar7Nw==" 136 | } 137 | } 138 | ``` 139 | 140 | The rest of the peers in the room will receive a `joined` event: 141 | 142 | ``` json 143 | { 144 | "event": "joined", 145 | "data": { 146 | "username": "john", 147 | "peer_id": "bxCBrwyL3Ar7Nw==" 148 | } 149 | } 150 | ``` 151 | 152 | Similarly, when a client leaves the room, the rest of the peers will 153 | receive a `left` event: 154 | 155 | ``` json 156 | { 157 | "event": "left", 158 | "data": { 159 | "username": "john", 160 | "peer_id": "bxCBrwyL3Ar7Nw==" 161 | } 162 | } 163 | ``` 164 | 165 | ### Signaling messages 166 | 167 | After authentication, all messages sent by the client should be 168 | signaling messages addressed to a specific peer in the room, including a `to` 169 | field. The event and data are opaque to the server (they can be 170 | ice candidates, session descriptions, or whatever clients need to 171 | exchange). For example: 172 | 173 | ``` json 174 | { 175 | "event": "candidate", 176 | "to": "458/53WAkeu+tQ==", 177 | "data": { 178 | ... 179 | } 180 | } 181 | ``` 182 | 183 | The addressed peer will receive this payload: 184 | 185 | ``` json 186 | { 187 | "event": "candidate", 188 | "from": "bxCBrwyL3Ar7Nw==", 189 | "data": { 190 | ... 191 | } 192 | } 193 | ``` 194 | 195 | ### Ping 196 | To send a keepalive message to prevent idle connections to be droped 197 | by the server, send a plain text frame of value `ping`. The server 198 | will respond with `pong`. 199 | 200 | ## Server API 201 | 202 | The `webrtc_server` module provides a few functions to interact with 203 | connected peers from the server: 204 | 205 | * `webrtc_server:peers(Room)`: return a list of `{PeerId, Username}` 206 | for the peers connected to `Room`. 207 | * `webrtc_server:publish(Room, Event, Data)`: send a JSON message to 208 | all connected peers in `Room`. 209 | * `webrtc_server:send(PeerId, Event, Data)`: send a JSON message to 210 | the peers identified by `PeerId`. 211 | 212 | 213 | ## Troubleshooting 214 | ### openssl error during compilation 215 | 216 | ``` 217 | _build/default/lib/fast_tls/c_src/fast_tls.c:21:10: fatal error: 'openssl/err.h' file not found 218 | ``` 219 | 220 | On debian it's solved by installing libssl-dev: 221 | 222 | ``` 223 | sudo apt-get install libssl-dev 224 | ``` 225 | 226 | On macOS it's solved by exporting the following openssl flags: 227 | 228 | ``` 229 | export LDFLAGS="-L/usr/local/opt/openssl/lib" 230 | export CFLAGS="-I/usr/local/opt/openssl/include/" 231 | export CPPFLAGS="-I/usr/local/opt/openssl/include/" 232 | ``` 233 | 234 | ### Firewall setup for STUN/TURN 235 | 236 | ``` 237 | iptables -A INPUT -p tcp --dport 3478 -j ACCEPT 238 | iptables -A INPUT -p udp --dport 3478 -j ACCEPT 239 | iptables -A INPUT -p tcp --dport 5349 -j ACCEPT 240 | iptables -A INPUT -p udp --dport 5349 -j ACCEPT 241 | iptables -A INPUT -p udp --dport 49152:65535 -j ACCEPT 242 | ``` 243 | -------------------------------------------------------------------------------- /conf/test.config: -------------------------------------------------------------------------------- 1 | [{webrtc_server, [ 2 | {port, 8443}, 3 | {hostname, <<"localhost">>}, 4 | {certfile, <<"../../lib/webrtc_server/priv/certs/certificate.pem">>}, 5 | {keyfile, <<"../../lib/webrtc_server/priv/certs/key.pem">>} 6 | ]}, 7 | {lager, [ 8 | {handlers, [ 9 | {lager_console_backend, [{level, debug}]}, 10 | {lager_file_backend, [{file, "log/error.log"}, {level, error}]}, 11 | {lager_file_backend, [{file, "log/console.log"}, {level, debug}]}]} 12 | ]} 13 | 14 | ]. 15 | -------------------------------------------------------------------------------- /examples/multi/.gitignore: -------------------------------------------------------------------------------- 1 | .rebar3 2 | _* 3 | .eunit 4 | *.o 5 | *.beam 6 | *.plt 7 | *.swp 8 | *.swo 9 | .erlang.cookie 10 | ebin 11 | log 12 | erl_crash.dump 13 | .rebar 14 | logs 15 | _build 16 | .idea 17 | *.iml 18 | rebar3.crashdump 19 | -------------------------------------------------------------------------------- /examples/multi/Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: dev 2 | 3 | dev: 4 | ../../rebar3 compile && ../../rebar3 shell 5 | 6 | release: 7 | ../../rebar3 as prod release 8 | -------------------------------------------------------------------------------- /examples/multi/README.md: -------------------------------------------------------------------------------- 1 | # WebRTC multi-party example 2 | 3 | Cowboy application that serves an HTML client for WebRTC. 4 | An unlimited number of peers can join a conversation by entering the 5 | same room URL. 6 | A mesh topology is used, meaning that if there are N peers, each one 7 | will maintain N - 1 RTC peer connections. This is CPU intensive and 8 | will work with a low number of peers. 9 | 10 | ## Run in development 11 | 12 | make dev 13 | 14 | The example app will run on `https://localhost:8443/:room` 15 | 16 | ## Run in production 17 | 18 | To run the example app stand alone in a production server, update the 19 | relevant configuration in `conf/sys.config` (port, certs, host, etc.) 20 | and run: 21 | 22 | make release 23 | 24 | Unpack the generated tar and run `bin/webrtc_server start`. 25 | -------------------------------------------------------------------------------- /examples/multi/conf/sys.config: -------------------------------------------------------------------------------- 1 | [{example, [ 2 | {port, 8443}, 3 | {certfile, <<"../../priv/certs/certificate.pem">>}, 4 | {keyfile, <<"../../priv/certs/key.pem">>}, 5 | {example_password, <<"password">>} 6 | ]}, 7 | {webrtc_server, [ 8 | {hostname, <<"localhost">>}, 9 | {certfile, <<"../priv/certs/certificate.pem">>}, 10 | {keyfile, <<"../priv/certs/key.pem">>}, 11 | {auth_fun, {callbacks, authenticate}}, 12 | {create_callback, {callbacks, create}}, 13 | {join_callback, {callbacks, join}}, 14 | {leave_callback, {callbacks, leave}}, 15 | {idle_timeout, infinity} 16 | ]}, 17 | {lager, [ 18 | {handlers, [ 19 | {lager_console_backend, [{level, debug}]}, 20 | {lager_file_backend, [{file, "log/error.log"}, {level, error}]}, 21 | {lager_file_backend, [{file, "log/console.log"}, {level, info}]}]} 22 | ]} 23 | 24 | ]. 25 | -------------------------------------------------------------------------------- /examples/multi/priv/css/main.css: -------------------------------------------------------------------------------- 1 | body { 2 | font-family: sans-serif; 3 | } 4 | 5 | video { 6 | max-width: 100%; 7 | width: 320px; 8 | } 9 | -------------------------------------------------------------------------------- /examples/multi/priv/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Realtime communication with WebRTC 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 |

Realtime communication with WebRTC

15 | 16 |
17 | 18 |
19 | 20 | 21 | 22 | 23 | 26 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /examples/multi/priv/js/main.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | let localStream; 4 | let peerId; 5 | const peers = {}; 6 | 7 | var username = 'username'; 8 | var password = 'password'; 9 | 10 | var localVideo = document.querySelector('#localVideo'); 11 | 12 | const stunUrl = 'stun:' + window.location.hostname + ':3478'; 13 | const turnUrl = 'turn:' + window.location.hostname + ':3478'; 14 | var pcConfig = { 15 | iceServers: [{ 16 | urls: turnUrl, 17 | username: username, 18 | credential: password 19 | },{ 20 | urls: stunUrl 21 | }] 22 | }; 23 | 24 | /** 25 | * General algorithm: 26 | * 1. get local stream 27 | * 2. connect to signaling server 28 | * 3. authenticate and wait for other client to connect 29 | * 4. when both peers are connected (socker receives joined message), 30 | * start webRTC peer connection 31 | */ 32 | getStream() 33 | .then(() => connectSocket()) 34 | .catch(console.log); 35 | 36 | function getStream() { 37 | return navigator.mediaDevices 38 | .getUserMedia({ 39 | audio: true, 40 | video: true 41 | }) 42 | .then(function (stream) { 43 | console.log('Adding local stream.'); 44 | localVideo.srcObject = stream; 45 | localStream = stream; 46 | }) 47 | .catch(function(e) { 48 | console.log(e.stack); 49 | alert('getUserMedia() error: ' + e.name); 50 | }); 51 | } 52 | 53 | ///////////////////////////////////////////// 54 | 55 | const room = window.location.pathname.split(/\//g)[1] ; 56 | console.log("room is", room); 57 | const wsUrl = 'wss://' + window.location.host + '/websocket/' + room; 58 | var socket; 59 | 60 | // helper to send ws messages with {event, data} structure 61 | function sendMessage(event, message, toPeer) { 62 | const payload = { 63 | event, 64 | data: message 65 | }; 66 | if (toPeer) { 67 | payload.to = toPeer; 68 | } 69 | console.log('Client sending message: ', event, message); 70 | socket.send(JSON.stringify(payload)); 71 | } 72 | 73 | //// SOCKET EVENT LISTENERS 74 | 75 | function authenticated (data) { 76 | peerId = data.peer_id; 77 | console.log('authenticated:', peerId); 78 | } 79 | 80 | function joined (data) { 81 | console.log('peer joined', data.peer_id); 82 | // start RTC as initiator with newly joined peer 83 | startRTC(data.peer_id, true); 84 | } 85 | 86 | function offer (data, fromPeer) { 87 | // received an offer, need to initiate rtc as receiver before answering 88 | startRTC(fromPeer, false); 89 | 90 | const connection = peers[fromPeer].connection; 91 | connection.setRemoteDescription(new RTCSessionDescription(data)); 92 | console.log('Sending answer to peer.'); 93 | connection.createAnswer().then( 94 | function(sessionDescription) { 95 | connection.setLocalDescription(sessionDescription); 96 | sendMessage('answer', sessionDescription, fromPeer); 97 | }, 98 | logEvent('Failed to create session description:') 99 | ); 100 | } 101 | 102 | function candidate(data, fromPeer) { 103 | var candidate = new RTCIceCandidate({ 104 | sdpMLineIndex: data.label, 105 | candidate: data.candidate 106 | }); 107 | peers[fromPeer].connection.addIceCandidate(candidate); 108 | } 109 | 110 | function answer (data, fromPeer) { 111 | peers[fromPeer].connection.setRemoteDescription(new RTCSessionDescription(data)); 112 | } 113 | 114 | function left (data) { 115 | console.log('Session terminated.'); 116 | const otherPeer = data.peer_id; 117 | peers[otherPeer].connection.close(); 118 | 119 | // remove dom element 120 | const element = document.getElementById(peers[otherPeer].element); 121 | element.srcObject = undefined; 122 | element.parentNode.removeChild(element); 123 | 124 | delete peer[otherPeer]; 125 | } 126 | 127 | /* 128 | * Connect the socket and set up its listeners. 129 | * Will return a promise that resolves once both clients are connected. 130 | */ 131 | function connectSocket() { 132 | // setting global var, sorry 133 | socket = new WebSocket(wsUrl); 134 | 135 | socket.onopen = function(event) { 136 | console.log('socket connected'); 137 | sendMessage('authenticate', {username, password}); 138 | }; 139 | 140 | socket.onclose = function(event) { 141 | console.log('socket was closed', event); 142 | }; 143 | 144 | const listeners = { 145 | authenticated, 146 | joined, 147 | left, 148 | candidate, 149 | offer, 150 | answer 151 | }; 152 | 153 | socket.onmessage = function(e) { 154 | const data = JSON.parse(e.data); 155 | console.log('Client received message:', data); 156 | const listener = listeners[data.event]; 157 | if (listener) { 158 | listener(data.data, data.from); 159 | } else { 160 | console.log('no listener for message', data.event); 161 | } 162 | }; 163 | 164 | } 165 | 166 | //////////////////////////////////////////////////// 167 | 168 | function startRTC(peerId, isInitiator) { 169 | console.log('>>>>>> creating peer connection'); 170 | 171 | try { 172 | const connection = new RTCPeerConnection(pcConfig); 173 | 174 | connection.onicecandidate = getHandleIceCandidate(peerId); 175 | connection.ontrack = getHandleRemoteStream(peerId); 176 | connection.onremovestream = logEvent('Remote stream removed,'); 177 | 178 | connection.addStream(localStream); 179 | 180 | peers[peerId] = {connection}; 181 | 182 | console.log('Created RTCPeerConnnection for', peerId); 183 | 184 | if (isInitiator) { 185 | createOffer(peerId); 186 | } 187 | } catch (e) { 188 | console.log('Failed to create PeerConnection, exception: ' + e.message); 189 | alert('Cannot create RTCPeerConnection object.'); 190 | return; 191 | } 192 | } 193 | 194 | //// PeerConnection handlers 195 | 196 | function getHandleIceCandidate(peerId) { 197 | return function(event) { 198 | console.log('icecandidate event: ', event); 199 | if (event.candidate) { 200 | sendMessage('candidate', { 201 | label: event.candidate.sdpMLineIndex, 202 | id: event.candidate.sdpMid, 203 | candidate: event.candidate.candidate 204 | }, peerId); 205 | } else { 206 | console.log('End of candidates.'); 207 | } 208 | }; 209 | } 210 | 211 | function getHandleRemoteStream(peerId) { 212 | return function(event) { 213 | console.log('Remote stream added for peer', peerId); 214 | const elementId = "video-" + peerId; 215 | 216 | // this handler can be called multiple times per stream, only 217 | // add a new video element once 218 | if (!peers[peerId].element) { 219 | const t = document.querySelector('#video-template'); 220 | t.content.querySelector('video').id = elementId; 221 | const clone = document.importNode(t.content, true); 222 | document.getElementById("videos").appendChild(clone); 223 | peers[peerId].element = elementId; 224 | } 225 | // always set the srcObject to the latest stream 226 | document.getElementById(elementId).srcObject = event.streams[0]; 227 | }; 228 | } 229 | 230 | function createOffer(peerId) { 231 | console.log('Sending offer to peer'); 232 | const connection = peers[peerId].connection; 233 | connection.createOffer(function(sessionDescription) { 234 | connection.setLocalDescription(sessionDescription); 235 | sendMessage('offer', sessionDescription, peerId); 236 | }, logEvent('createOffer() error:')); 237 | } 238 | 239 | // event/error logger 240 | function logEvent(text) { 241 | return function (data) { 242 | console.log(text, data); 243 | }; 244 | } 245 | -------------------------------------------------------------------------------- /examples/multi/rebar.config: -------------------------------------------------------------------------------- 1 | {erl_opts, [debug_info, {parse_transform, lager_transform}]}. 2 | 3 | {deps, [ 4 | {cowboy, "2.0.0"}, 5 | {webrtc_server, {git, "https://github.com/lambdaclass/webrtc-server", {ref, "fabb07a"}}} 6 | ]}. 7 | 8 | {relx, [{release, {example, "0.1.0"}, 9 | [example]}, 10 | {dev_mode, true}, 11 | {include_erts, false}, 12 | {extended_start_script, false}, 13 | {sys_config, "conf/sys.config"} 14 | ]}. 15 | 16 | {profiles, [{prod, [{relx, [{dev_mode, false}, 17 | {include_erts, true}, 18 | {sys_config, "./conf/prod.config"}, 19 | {extended_start_script, true}]}]}]}. 20 | -------------------------------------------------------------------------------- /examples/multi/rebar.lock: -------------------------------------------------------------------------------- 1 | {"1.1.0", 2 | [{<<"cowboy">>,{pkg,<<"cowboy">>,<<"2.0.0">>},0}, 3 | {<<"cowlib">>,{pkg,<<"cowlib">>,<<"2.0.1">>},1}, 4 | {<<"fast_tls">>,{pkg,<<"fast_tls">>,<<"1.0.20">>},2}, 5 | {<<"goldrush">>,{pkg,<<"goldrush">>,<<"0.1.9">>},2}, 6 | {<<"jsx">>,{pkg,<<"jsx">>,<<"2.8.2">>},1}, 7 | {<<"lager">>,{pkg,<<"lager">>,<<"3.5.1">>},1}, 8 | {<<"p1_utils">>,{pkg,<<"p1_utils">>,<<"1.0.10">>},2}, 9 | {<<"ranch">>,{pkg,<<"ranch">>,<<"1.4.0">>},1}, 10 | {<<"stun">>,{pkg,<<"stun">>,<<"1.0.20">>},1}, 11 | {<<"syn">>,{pkg,<<"syn">>,<<"1.6.1">>},1}, 12 | {<<"webrtc_server">>, 13 | {git,"https://github.com/lambdaclass/webrtc-server", 14 | {ref,"fabb07aac0cd2f64c80fb9c9224bce78e3ff6741"}}, 15 | 0}]}. 16 | [ 17 | {pkg_hash,[ 18 | {<<"cowboy">>, <<"A3B680BCC1156C6FBCB398CC56ADC35177037012D7DC28D8F7E7926D6C243561">>}, 19 | {<<"cowlib">>, <<"4DFFFB1DB296EAB9F2E8B95EE3017007F674BC920CE30AEB5A53BBDA82FC38C0">>}, 20 | {<<"fast_tls">>, <<"EDD241961AB20B71EC1E9F75A2A2C043128FF117ADF3EFD42E6CEC94F1937539">>}, 21 | {<<"goldrush">>, <<"F06E5D5F1277DA5C413E84D5A2924174182FB108DABB39D5EC548B27424CD106">>}, 22 | {<<"jsx">>, <<"7ACC7D785B5ABE8A6E9ADBDE926A24E481F29956DD8B4DF49E3E4E7BCC92A018">>}, 23 | {<<"lager">>, <<"63897A61AF646C59BB928FEE9756CE8BDD02D5A1A2F3551D4A5E38386C2CC071">>}, 24 | {<<"p1_utils">>, <<"A6D6927114BAC79CF6468A10824125492034AF7071ADC6ED5EBC4DDB443845D4">>}, 25 | {<<"ranch">>, <<"10272F95DA79340FA7E8774BA7930B901713D272905D0012B06CA6D994F8826B">>}, 26 | {<<"stun">>, <<"6B156FA11606BEBB6086D02CB2F6532C84EFFB59C95BA93D0E2D8E2510970253">>}, 27 | {<<"syn">>, <<"728A9C521B3815831259A98FDEC9C74875612A4D2854E324DD1EC5AD6DF4146E">>}]} 28 | ]. 29 | -------------------------------------------------------------------------------- /examples/multi/src/callbacks.erl: -------------------------------------------------------------------------------- 1 | -module(callbacks). 2 | 3 | -export([authenticate/1, 4 | create/3, 5 | join/3, 6 | leave/3]). 7 | 8 | authenticate(_Username) -> 9 | %% in a real scenario this may lookup the password in the db, request an external service, etc. 10 | {ok, Password} = application:get_env(example, example_password), 11 | Password. 12 | 13 | create(Room, Username, _OtherUsers) -> 14 | lager:info("~s created ~s", [Username, Room]). 15 | 16 | join(Room, Username, _OtherUsers) -> 17 | lager:info("~s joined ~s", [Username, Room]). 18 | 19 | leave(Room, Username, _OtherUsers) -> 20 | lager:info("~s left ~s", [Username, Room]). 21 | -------------------------------------------------------------------------------- /examples/multi/src/example.app.src: -------------------------------------------------------------------------------- 1 | {application, example, 2 | [{description, "An OTP application"}, 3 | {vsn, "0.1.0"}, 4 | {registered, []}, 5 | {mod, { example_app, []}}, 6 | {applications, 7 | [kernel, 8 | stdlib, 9 | cowboy, 10 | webrtc_server 11 | ]}, 12 | {env,[]}, 13 | {modules, []}, 14 | 15 | {maintainers, []}, 16 | {licenses, ["Apache 2.0"]}, 17 | {links, []} 18 | ]}. 19 | -------------------------------------------------------------------------------- /examples/multi/src/example_app.erl: -------------------------------------------------------------------------------- 1 | -module(example_app). 2 | 3 | -behaviour(application). 4 | 5 | %% Application callbacks 6 | -export([start/2, stop/1]). 7 | 8 | start(_StartType, _StartArgs) -> 9 | Dispatch = cowboy_router:compile([ 10 | {'_', [ 11 | {"/js/[...]", cowboy_static, {priv_dir, example, "/js"}}, 12 | {"/css/[...]", cowboy_static, {priv_dir, example, "/css"}}, 13 | {"/websocket/:room", webrtc_ws_handler, []}, 14 | {'_', cowboy_static, {priv_file, example, "/index.html"}} 15 | ]} 16 | ]), 17 | {ok, _} = cowboy:start_tls(my_http_listener, 18 | [{port, config(port)}, 19 | {certfile, config(certfile)}, 20 | {keyfile, config(keyfile)} 21 | ], 22 | #{env => #{dispatch => Dispatch}} 23 | ), 24 | example_sup:start_link(). 25 | 26 | stop(_State) -> 27 | ok. 28 | 29 | %% Internal functions 30 | config(Key) -> 31 | {ok, Value} = application:get_env(example, Key), 32 | Value. 33 | -------------------------------------------------------------------------------- /examples/multi/src/example_sup.erl: -------------------------------------------------------------------------------- 1 | %%%------------------------------------------------------------------- 2 | %% @doc example top level supervisor. 3 | %% @end 4 | %%%------------------------------------------------------------------- 5 | 6 | -module(example_sup). 7 | 8 | -behaviour(supervisor). 9 | 10 | %% API 11 | -export([start_link/0]). 12 | 13 | %% Supervisor callbacks 14 | -export([init/1]). 15 | 16 | -define(SERVER, ?MODULE). 17 | 18 | %%==================================================================== 19 | %% API functions 20 | %%==================================================================== 21 | 22 | start_link() -> 23 | supervisor:start_link({local, ?SERVER}, ?MODULE, []). 24 | 25 | %%==================================================================== 26 | %% Supervisor callbacks 27 | %%==================================================================== 28 | 29 | %% Child :: {Id,StartFunc,Restart,Shutdown,Type,Modules} 30 | init([]) -> 31 | {ok, { {one_for_all, 0, 1}, []} }. 32 | 33 | %%==================================================================== 34 | %% Internal functions 35 | %%==================================================================== 36 | -------------------------------------------------------------------------------- /examples/simple/.gitignore: -------------------------------------------------------------------------------- 1 | .rebar3 2 | _* 3 | .eunit 4 | *.o 5 | *.beam 6 | *.plt 7 | *.swp 8 | *.swo 9 | .erlang.cookie 10 | ebin 11 | log 12 | erl_crash.dump 13 | .rebar 14 | logs 15 | _build 16 | .idea 17 | *.iml 18 | rebar3.crashdump 19 | -------------------------------------------------------------------------------- /examples/simple/Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: dev 2 | 3 | dev: 4 | ../../rebar3 compile && ../../rebar3 shell 5 | 6 | release: 7 | ../../rebar3 as prod release 8 | -------------------------------------------------------------------------------- /examples/simple/README.md: -------------------------------------------------------------------------------- 1 | # WebRTC simple example 2 | 3 | Simple Cowboy application that serves an HTML client for WebRTC. 4 | Connections are established by two peers that enter in the same room URL. 5 | The client connects to the signaling and ICE servers provided by webrtc_server. 6 | 7 | ## Run in development 8 | 9 | make dev 10 | 11 | The example app will run on `https://localhost:8443/:room` 12 | 13 | ## Run in production 14 | 15 | To run the example app stand alone in a production server, update the 16 | relevant configuration in `conf/sys.config` (port, certs, host, etc.) 17 | and run: 18 | 19 | make release 20 | 21 | Unpack the generated tar and run `bin/webrtc_server start`. 22 | -------------------------------------------------------------------------------- /examples/simple/conf/sys.config: -------------------------------------------------------------------------------- 1 | [{example, [ 2 | {port, 8443}, 3 | {certfile, <<"../../priv/certs/certificate.pem">>}, 4 | {keyfile, <<"../../priv/certs/key.pem">>}, 5 | {example_password, <<"password">>} 6 | ]}, 7 | {webrtc_server, [ 8 | {hostname, <<"localhost">>}, 9 | {certfile, <<"../priv/certs/certificate.pem">>}, 10 | {keyfile, <<"../priv/certs/key.pem">>}, 11 | {auth_fun, {callbacks, authenticate}}, 12 | {create_callback, {callbacks, create}}, 13 | {join_callback, {callbacks, join}}, 14 | {leave_callback, {callbacks, leave}}, 15 | {idle_timeout, infinity} 16 | ]}, 17 | {lager, [ 18 | {handlers, [ 19 | {lager_console_backend, [{level, debug}]}, 20 | {lager_file_backend, [{file, "log/error.log"}, {level, error}]}, 21 | {lager_file_backend, [{file, "log/console.log"}, {level, info}]}]} 22 | ]} 23 | 24 | ]. 25 | -------------------------------------------------------------------------------- /examples/simple/priv/css/main.css: -------------------------------------------------------------------------------- 1 | body { 2 | font-family: sans-serif; 3 | } 4 | 5 | video { 6 | max-width: 100%; 7 | width: 320px; 8 | } 9 | -------------------------------------------------------------------------------- /examples/simple/priv/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Realtime communication with WebRTC 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 |

Realtime communication with WebRTC

15 | 16 |
17 | 18 | 19 |
20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /examples/simple/priv/js/main.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var localStream; 4 | var pc; 5 | var remotePeerId; 6 | 7 | var username = 'username'; 8 | var password = 'password'; 9 | 10 | var localVideo = document.querySelector('#localVideo'); 11 | var remoteVideo = document.querySelector('#remoteVideo'); 12 | 13 | const stunUrl = 'stun:' + window.location.hostname + ':3478'; 14 | const turnUrl = 'turn:' + window.location.hostname + ':3478'; 15 | var pcConfig = { 16 | iceServers: [{ 17 | urls: turnUrl, 18 | username: username, 19 | credential: password 20 | },{ 21 | urls: stunUrl 22 | }] 23 | }; 24 | 25 | /** 26 | * General algorithm: 27 | * 1. get local stream 28 | * 2. connect to signaling server 29 | * 3. authenticate and wait for other client to connect 30 | * 4. when both peers are connected (socker receives joined message), 31 | * start webRTC peer connection 32 | */ 33 | getStream() 34 | .then(() => connectSocket()) 35 | .catch(console.log); 36 | 37 | function getStream() { 38 | return navigator.mediaDevices 39 | .getUserMedia({ 40 | audio: true, 41 | video: true 42 | }) 43 | .then(function (stream) { 44 | console.log('Adding local stream.'); 45 | localVideo.srcObject = stream; 46 | localStream = stream; 47 | }) 48 | .catch(function(e) { 49 | console.log(e.stack); 50 | alert('getUserMedia() error: ' + e.name); 51 | }); 52 | } 53 | 54 | ///////////////////////////////////////////// 55 | 56 | const room = window.location.pathname.split(/\//g)[1] ; 57 | console.log("room is", room); 58 | const wsUrl = 'wss://' + window.location.host + '/websocket/' + room; 59 | var socket; 60 | 61 | // helper to send ws messages with {event, data} structure 62 | function sendMessage(event, message) { 63 | const payload = { 64 | event, 65 | data: message, 66 | to: remotePeerId 67 | }; 68 | console.log('Client sending message: ', payload); 69 | socket.send(JSON.stringify(payload)); 70 | } 71 | 72 | //// SOCKET EVENT LISTENERS 73 | 74 | function authenticated (data) { 75 | console.log('authenticated:', data.peer_id); 76 | } 77 | 78 | // we're asssuming 1on1 conversations. when a second client joins then both 79 | // clients are connected => channel ready 80 | function joined(data) { 81 | console.log('peer joined', data.peer_id); 82 | remotePeerId = data.peer_id; 83 | 84 | // this is the initiator 85 | startRTC(true); 86 | } 87 | 88 | function candidate(data) { 89 | var candidate = new RTCIceCandidate({ 90 | sdpMLineIndex: data.label, 91 | candidate: data.candidate 92 | }); 93 | pc.addIceCandidate(candidate); 94 | } 95 | 96 | function offer (data, fromPeer) { 97 | // received offer from the other peer, start as receiver 98 | remotePeerId = fromPeer; 99 | startRTC(false); 100 | 101 | pc.setRemoteDescription(new RTCSessionDescription(data)); 102 | console.log('Sending answer to peer.'); 103 | pc.createAnswer().then( 104 | function(sessionDescription) { 105 | pc.setLocalDescription(sessionDescription); 106 | sendMessage('answer', sessionDescription); 107 | }, 108 | logEvent('Failed to create session description:') 109 | ); 110 | } 111 | 112 | function answer (data) { 113 | pc.setRemoteDescription(new RTCSessionDescription(data)); 114 | } 115 | 116 | function left () { 117 | console.log('Session terminated.'); 118 | if (pc) { 119 | pc.close(); 120 | pc = null; 121 | remotePeerId = undefined; 122 | } 123 | remoteVideo.srcObject = undefined; 124 | } 125 | 126 | /* 127 | * Connect the socket and set up its listeners. 128 | * Will return a promise that resolves once both clients are connected. 129 | */ 130 | function connectSocket() { 131 | // setting global var, sorry 132 | socket = new WebSocket(wsUrl); 133 | 134 | socket.onopen = function(event) { 135 | console.log('socket connected'); 136 | sendMessage('authenticate', {username, password}); 137 | }; 138 | 139 | socket.onclose = function(event) { 140 | console.log('socket was closed', event); 141 | }; 142 | 143 | const listeners = { 144 | authenticated, 145 | joined, 146 | left, 147 | candidate, 148 | offer, 149 | answer 150 | }; 151 | 152 | socket.onmessage = function(e) { 153 | const data = JSON.parse(e.data); 154 | console.log('Client received message:', data); 155 | const listener = listeners[data.event]; 156 | if (listener) { 157 | listener(data.data, data.from); 158 | } else { 159 | console.log('no listener for message', data.event); 160 | } 161 | }; 162 | 163 | } 164 | 165 | //////////////////////////////////////////////////// 166 | 167 | function startRTC(isInitiator) { 168 | console.log('>>>>>> creating peer connection'); 169 | 170 | try { 171 | pc = new RTCPeerConnection(pcConfig); 172 | pc.onicecandidate = handleIceCandidate; 173 | pc.ontrack = handleRemoteStreamAdded; 174 | pc.onremovestream = logEvent('Remote stream removed,'); 175 | console.log('Created RTCPeerConnnection'); 176 | 177 | pc.addStream(localStream); 178 | 179 | if (isInitiator) { 180 | createOffer(); 181 | } 182 | } catch (e) { 183 | console.log('Failed to create PeerConnection, exception: ' + e.message); 184 | alert('Cannot create RTCPeerConnection object.'); 185 | return; 186 | } 187 | } 188 | 189 | //// PeerConnection handlers 190 | 191 | function handleIceCandidate(event) { 192 | console.log('icecandidate event: ', event); 193 | if (event.candidate) { 194 | sendMessage('candidate', { 195 | label: event.candidate.sdpMLineIndex, 196 | id: event.candidate.sdpMid, 197 | candidate: event.candidate.candidate 198 | }); 199 | } else { 200 | console.log('End of candidates.'); 201 | } 202 | } 203 | 204 | function handleRemoteStreamAdded(event) { 205 | console.log('Remote stream added.'); 206 | remoteVideo.srcObject = event.streams[0]; 207 | } 208 | 209 | function createOffer() { 210 | console.log('Sending offer to peer'); 211 | pc.createOffer(function(sessionDescription) { 212 | pc.setLocalDescription(sessionDescription); 213 | sendMessage('offer', sessionDescription); 214 | }, logEvent('createOffer() error:')); 215 | } 216 | 217 | // event/error logger 218 | function logEvent(text) { 219 | return function (data) { 220 | console.log(text, data); 221 | }; 222 | } 223 | -------------------------------------------------------------------------------- /examples/simple/rebar.config: -------------------------------------------------------------------------------- 1 | {erl_opts, [debug_info, {parse_transform, lager_transform}]}. 2 | 3 | {deps, [ 4 | {cowboy, "2.0.0"}, 5 | {webrtc_server, {git, "https://github.com/lambdaclass/webrtc-server", {ref, "fabb07a"}}} 6 | ]}. 7 | 8 | {relx, [{release, {example, "0.1.0"}, 9 | [example]}, 10 | {dev_mode, true}, 11 | {include_erts, false}, 12 | {extended_start_script, false}, 13 | {sys_config, "conf/sys.config"} 14 | ]}. 15 | 16 | {profiles, [{prod, [{relx, [{dev_mode, false}, 17 | {include_erts, true}, 18 | {sys_config, "./conf/prod.config"}, 19 | {extended_start_script, true}]}]}]}. 20 | -------------------------------------------------------------------------------- /examples/simple/rebar.lock: -------------------------------------------------------------------------------- 1 | {"1.1.0", 2 | [{<<"cowboy">>,{pkg,<<"cowboy">>,<<"2.0.0">>},0}, 3 | {<<"cowlib">>,{pkg,<<"cowlib">>,<<"2.0.1">>},1}, 4 | {<<"fast_tls">>,{pkg,<<"fast_tls">>,<<"1.0.20">>},2}, 5 | {<<"goldrush">>,{pkg,<<"goldrush">>,<<"0.1.9">>},2}, 6 | {<<"jsx">>,{pkg,<<"jsx">>,<<"2.8.2">>},1}, 7 | {<<"lager">>,{pkg,<<"lager">>,<<"3.5.1">>},1}, 8 | {<<"p1_utils">>,{pkg,<<"p1_utils">>,<<"1.0.10">>},2}, 9 | {<<"ranch">>,{pkg,<<"ranch">>,<<"1.4.0">>},1}, 10 | {<<"stun">>,{pkg,<<"stun">>,<<"1.0.20">>},1}, 11 | {<<"syn">>,{pkg,<<"syn">>,<<"1.6.1">>},1}, 12 | {<<"webrtc_server">>, 13 | {git,"https://github.com/lambdaclass/webrtc-server", 14 | {ref,"fabb07aac0cd2f64c80fb9c9224bce78e3ff6741"}}, 15 | 0}]}. 16 | [ 17 | {pkg_hash,[ 18 | {<<"cowboy">>, <<"A3B680BCC1156C6FBCB398CC56ADC35177037012D7DC28D8F7E7926D6C243561">>}, 19 | {<<"cowlib">>, <<"4DFFFB1DB296EAB9F2E8B95EE3017007F674BC920CE30AEB5A53BBDA82FC38C0">>}, 20 | {<<"fast_tls">>, <<"EDD241961AB20B71EC1E9F75A2A2C043128FF117ADF3EFD42E6CEC94F1937539">>}, 21 | {<<"goldrush">>, <<"F06E5D5F1277DA5C413E84D5A2924174182FB108DABB39D5EC548B27424CD106">>}, 22 | {<<"jsx">>, <<"7ACC7D785B5ABE8A6E9ADBDE926A24E481F29956DD8B4DF49E3E4E7BCC92A018">>}, 23 | {<<"lager">>, <<"63897A61AF646C59BB928FEE9756CE8BDD02D5A1A2F3551D4A5E38386C2CC071">>}, 24 | {<<"p1_utils">>, <<"A6D6927114BAC79CF6468A10824125492034AF7071ADC6ED5EBC4DDB443845D4">>}, 25 | {<<"ranch">>, <<"10272F95DA79340FA7E8774BA7930B901713D272905D0012B06CA6D994F8826B">>}, 26 | {<<"stun">>, <<"6B156FA11606BEBB6086D02CB2F6532C84EFFB59C95BA93D0E2D8E2510970253">>}, 27 | {<<"syn">>, <<"728A9C521B3815831259A98FDEC9C74875612A4D2854E324DD1EC5AD6DF4146E">>}]} 28 | ]. 29 | -------------------------------------------------------------------------------- /examples/simple/src/callbacks.erl: -------------------------------------------------------------------------------- 1 | -module(callbacks). 2 | 3 | -export([authenticate/1, 4 | create/3, 5 | join/3, 6 | leave/3]). 7 | 8 | authenticate(_Username) -> 9 | %% in a real scenario this may lookup the password in the db, request an external service, etc. 10 | {ok, Password} = application:get_env(example, example_password), 11 | Password. 12 | 13 | create(Room, Username, _OtherUsers) -> 14 | lager:info("~s created ~s", [Username, Room]). 15 | 16 | join(Room, Username, _OtherUsers) -> 17 | lager:info("~s joined ~s", [Username, Room]). 18 | 19 | leave(Room, Username, _OtherUsers) -> 20 | lager:info("~s left ~s", [Username, Room]). 21 | -------------------------------------------------------------------------------- /examples/simple/src/example.app.src: -------------------------------------------------------------------------------- 1 | {application, example, 2 | [{description, "An OTP application"}, 3 | {vsn, "0.1.0"}, 4 | {registered, []}, 5 | {mod, { example_app, []}}, 6 | {applications, 7 | [kernel, 8 | stdlib, 9 | cowboy, 10 | webrtc_server 11 | ]}, 12 | {env,[]}, 13 | {modules, []}, 14 | 15 | {maintainers, []}, 16 | {licenses, ["Apache 2.0"]}, 17 | {links, []} 18 | ]}. 19 | -------------------------------------------------------------------------------- /examples/simple/src/example_app.erl: -------------------------------------------------------------------------------- 1 | -module(example_app). 2 | 3 | -behaviour(application). 4 | 5 | %% Application callbacks 6 | -export([start/2, stop/1]). 7 | 8 | start(_StartType, _StartArgs) -> 9 | Dispatch = cowboy_router:compile([ 10 | {'_', [ 11 | {"/js/[...]", cowboy_static, {priv_dir, example, "/js"}}, 12 | {"/css/[...]", cowboy_static, {priv_dir, example, "/css"}}, 13 | {"/websocket/:room", webrtc_ws_handler, []}, 14 | {'_', cowboy_static, {priv_file, example, "/index.html"}} 15 | ]} 16 | ]), 17 | {ok, _} = cowboy:start_tls(my_http_listener, 18 | [{port, config(port)}, 19 | {certfile, config(certfile)}, 20 | {keyfile, config(keyfile)} 21 | ], 22 | #{env => #{dispatch => Dispatch}} 23 | ), 24 | example_sup:start_link(). 25 | 26 | stop(_State) -> 27 | ok. 28 | 29 | %% Internal functions 30 | config(Key) -> 31 | {ok, Value} = application:get_env(example, Key), 32 | Value. 33 | -------------------------------------------------------------------------------- /examples/simple/src/example_sup.erl: -------------------------------------------------------------------------------- 1 | %%%------------------------------------------------------------------- 2 | %% @doc example top level supervisor. 3 | %% @end 4 | %%%------------------------------------------------------------------- 5 | 6 | -module(example_sup). 7 | 8 | -behaviour(supervisor). 9 | 10 | %% API 11 | -export([start_link/0]). 12 | 13 | %% Supervisor callbacks 14 | -export([init/1]). 15 | 16 | -define(SERVER, ?MODULE). 17 | 18 | %%==================================================================== 19 | %% API functions 20 | %%==================================================================== 21 | 22 | start_link() -> 23 | supervisor:start_link({local, ?SERVER}, ?MODULE, []). 24 | 25 | %%==================================================================== 26 | %% Supervisor callbacks 27 | %%==================================================================== 28 | 29 | %% Child :: {Id,StartFunc,Restart,Shutdown,Type,Modules} 30 | init([]) -> 31 | {ok, { {one_for_all, 0, 1}, []} }. 32 | 33 | %%==================================================================== 34 | %% Internal functions 35 | %%==================================================================== 36 | -------------------------------------------------------------------------------- /priv/certs/certificate.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIDtTCCAp2gAwIBAgIJANdiRiJAr9WWMA0GCSqGSIb3DQEBBQUAMEUxCzAJBgNV 3 | BAYTAkFVMRMwEQYDVQQIEwpTb21lLVN0YXRlMSEwHwYDVQQKExhJbnRlcm5ldCBX 4 | aWRnaXRzIFB0eSBMdGQwHhcNMTcxMDEwMTc0MjI3WhcNMTgxMDEwMTc0MjI3WjBF 5 | MQswCQYDVQQGEwJBVTETMBEGA1UECBMKU29tZS1TdGF0ZTEhMB8GA1UEChMYSW50 6 | ZXJuZXQgV2lkZ2l0cyBQdHkgTHRkMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIB 7 | CgKCAQEAyx1suMxSOAZzjJ26s71nFztb66zPyRfIbYW0BHfhaKbthO57yvF+FP6+ 8 | 6JRMEyuH60l3H5LQksIucXI3agUmu5jQtuI85PRMUoQD+Lc6fuIegsxMCrs48URc 9 | /jLnYmT7Q8q7COFSQEeNfjnEAVKDXHd/WfrTLsdYziZycKMNh+qWkHzRZ7A5W0ma 10 | spbH/x8GUujsa8Snf97gyBS96Ur5EmjdFMuH+HeC4VBkcKOAXwrmn5JOv0fAolGu 11 | ArV8EKvZIBB4qvMjo3dAnJlihqAtCsYNGZ1gk9qt6sYBzbMpfGrPOkxrLl8biJHc 12 | WWp0Aj0mjkNQUi3cdzAdgPys/X2ruQIDAQABo4GnMIGkMB0GA1UdDgQWBBRQKjvL 13 | //sG7PAtHyC3NLEpwjmvCzB1BgNVHSMEbjBsgBRQKjvL//sG7PAtHyC3NLEpwjmv 14 | C6FJpEcwRTELMAkGA1UEBhMCQVUxEzARBgNVBAgTClNvbWUtU3RhdGUxITAfBgNV 15 | BAoTGEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZIIJANdiRiJAr9WWMAwGA1UdEwQF 16 | MAMBAf8wDQYJKoZIhvcNAQEFBQADggEBAFCR/oO3rgm+H2ruTy4KAYahh9uv/ITu 17 | 4OiKY4mGez4fAIZICxCbM5CDSzIu6wAlr8D6xGr63uGixSDgYWt2ufyasigzOWoK 18 | UYp3kj9y3V1Rx2P8iqCGbmYq62oPV67mBGYKZCVWhp18p3nSNvad3vmGNOYKTtw8 19 | ITPAmYGFjkE+mF1Jecahd2hk/CLvpzvMYbSnd4Yh4Cp8AP5PDAuk2L61OBGA1Bfi 20 | S1i3zWX9rju7s/nNL7eTkFzLTqjC1QS6/gtBjL2fImtt4wMaDwFTRcmRr2j7vrc2 21 | eeVmk8Ahp06hMPSRli9Z+IRPS0hJUKalFnbkEuLxA36iHP+5kDP9Y/g= 22 | -----END CERTIFICATE----- 23 | -------------------------------------------------------------------------------- /priv/certs/key.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN RSA PRIVATE KEY----- 2 | MIIEowIBAAKCAQEAyx1suMxSOAZzjJ26s71nFztb66zPyRfIbYW0BHfhaKbthO57 3 | yvF+FP6+6JRMEyuH60l3H5LQksIucXI3agUmu5jQtuI85PRMUoQD+Lc6fuIegsxM 4 | Crs48URc/jLnYmT7Q8q7COFSQEeNfjnEAVKDXHd/WfrTLsdYziZycKMNh+qWkHzR 5 | Z7A5W0maspbH/x8GUujsa8Snf97gyBS96Ur5EmjdFMuH+HeC4VBkcKOAXwrmn5JO 6 | v0fAolGuArV8EKvZIBB4qvMjo3dAnJlihqAtCsYNGZ1gk9qt6sYBzbMpfGrPOkxr 7 | Ll8biJHcWWp0Aj0mjkNQUi3cdzAdgPys/X2ruQIDAQABAoIBAQCNrb6ywMLxFX7w 8 | LO2YhpssV1ls8SQXLyG9U7AYFc8DgrzXJsru6rh5yKA994OBM7IhayNOwMgANrbS 9 | p0sEBwfXf2bGytSTF91NCY0DpjuFWhDSR9MbATFdCcScA8Hmnm2uAfGo5hLLh52o 10 | 2H5iNb4vd6M7jnxUevT1B1h4PSQpEzjw7CRiyOa8SeDSKfa2vudEdn00hLEKMAXd 11 | VswCcAnmFmQ4W3d3NCjNzi+cGYPxhFcRyfF330Bb2VqebY9W+7toJv7reqDJ8j43 12 | L7HhENZBpP+5gkBaofITYZsYz4HDWAQFz9Ulv2E8xGIuU0QzDc9jOSIq7277ietX 13 | RiqVBbYpAoGBAO7a1mPgwIwXjo4MWI3QVgzi8GtN0p0r2bOFzxM7p9fBgzp2vvY+ 14 | N2ovy5HuQclqgrbn18ewWzin6tkw6+px4+klownVVKO98rvCR4O2ObSxm7gB6qxF 15 | 9gs4PrQAp4sh+1Le8Bw1qijdFkTgx2FjKYlnmzsBLMn1t7jCtP+XbL2nAoGBANmx 16 | 1gVT4Q1dny1rqY15RIWzDJ8dxy4eY2n0pAfsUuE3oMaFRrtxrQ/Yd5ZLzwQStu6l 17 | AfRfPFG1JzwV2WVVBcVUOjWChGQHDlt4xIfdGN5d9pG3OIYFSAg9sPFugJEFOgJM 18 | qrnbK1aeUyd0kxCdYqZ5kVm8vWnCkj0FahWywzefAoGAVilY9xSHQMHqqbEobJe/ 19 | wsxGf97F3+6GjKzzQvPdGwZyaS+WuUs+QC7Xl1/EGX0zg/lkLGOgtHJWVFzCbYMB 20 | /QOXqZ9r9dk6a6Ksm4WrkVQUYS9H0Tc3h1qVu+cUiSsL9xv2r6ZoKG+Abf5LzgSw 21 | YiGerI1C8+OQj7SlCCI+lrcCgYBwmj8U9GUlj7alPNov9nkOGyY9K576aPeNN+Cc 22 | xI2+NxLvfMKwdEVLO/HniQDkn3WGDU3shFJkBSrtNnQDqS3Z+w483WzzfH7dq6Mk 23 | j6WsZ7gBeV9AW3z93kMnLrxLxwNRayyoBAjvvedPMkpbvrznVVxsqWbkTKNt8t4D 24 | qTq3CwKBgA/tCmtQlcaEV76tKwgo4sHWkjfrmoKg+nDoNY6Mwm9l3iFYO/VV46YQ 25 | jJBNmgdQM58QTvOpbPO9XZH4+0DfCn/Rfab7bFzcga2lr9JiSHuaZprvJhelLQBM 26 | L9p59M8jme/a+cH05AsCAMh3jwp7qS/ZCEotCQKi5YyzduyQDaWW 27 | -----END RSA PRIVATE KEY----- 28 | -------------------------------------------------------------------------------- /rebar.config: -------------------------------------------------------------------------------- 1 | {erl_opts, [debug_info, {parse_transform, lager_transform}]}. 2 | {deps, [ 3 | {cowboy, "2.0.0"}, 4 | {lager, "3.5.1"}, 5 | {syn, "1.6.1"}, 6 | {jsx, "2.8.2"}, 7 | {stun, "1.0.20"} 8 | ]}. 9 | 10 | {ct_opts, [{sys_config, "conf/test.config"}]}. 11 | 12 | {profiles, [ 13 | {test, [ 14 | {deps, [ 15 | {websocket_client, {git, "https://github.com/jeremyong/websocket_client.git", {ref, "9a6f65d"}}} 16 | ]}, 17 | {erl_opts, [nowarn_export_all]} 18 | ]} 19 | ]}. 20 | -------------------------------------------------------------------------------- /rebar.lock: -------------------------------------------------------------------------------- 1 | {"1.1.0", 2 | [{<<"cowboy">>,{pkg,<<"cowboy">>,<<"2.0.0">>},0}, 3 | {<<"cowlib">>,{pkg,<<"cowlib">>,<<"2.0.1">>},1}, 4 | {<<"fast_tls">>,{pkg,<<"fast_tls">>,<<"1.0.20">>},1}, 5 | {<<"goldrush">>,{pkg,<<"goldrush">>,<<"0.1.9">>},1}, 6 | {<<"jsx">>,{pkg,<<"jsx">>,<<"2.8.2">>},0}, 7 | {<<"lager">>,{pkg,<<"lager">>,<<"3.5.1">>},0}, 8 | {<<"p1_utils">>,{pkg,<<"p1_utils">>,<<"1.0.10">>},1}, 9 | {<<"ranch">>,{pkg,<<"ranch">>,<<"1.4.0">>},1}, 10 | {<<"stun">>,{pkg,<<"stun">>,<<"1.0.20">>},0}, 11 | {<<"syn">>,{pkg,<<"syn">>,<<"1.6.1">>},0}]}. 12 | [ 13 | {pkg_hash,[ 14 | {<<"cowboy">>, <<"A3B680BCC1156C6FBCB398CC56ADC35177037012D7DC28D8F7E7926D6C243561">>}, 15 | {<<"cowlib">>, <<"4DFFFB1DB296EAB9F2E8B95EE3017007F674BC920CE30AEB5A53BBDA82FC38C0">>}, 16 | {<<"fast_tls">>, <<"EDD241961AB20B71EC1E9F75A2A2C043128FF117ADF3EFD42E6CEC94F1937539">>}, 17 | {<<"goldrush">>, <<"F06E5D5F1277DA5C413E84D5A2924174182FB108DABB39D5EC548B27424CD106">>}, 18 | {<<"jsx">>, <<"7ACC7D785B5ABE8A6E9ADBDE926A24E481F29956DD8B4DF49E3E4E7BCC92A018">>}, 19 | {<<"lager">>, <<"63897A61AF646C59BB928FEE9756CE8BDD02D5A1A2F3551D4A5E38386C2CC071">>}, 20 | {<<"p1_utils">>, <<"A6D6927114BAC79CF6468A10824125492034AF7071ADC6ED5EBC4DDB443845D4">>}, 21 | {<<"ranch">>, <<"10272F95DA79340FA7E8774BA7930B901713D272905D0012B06CA6D994F8826B">>}, 22 | {<<"stun">>, <<"6B156FA11606BEBB6086D02CB2F6532C84EFFB59C95BA93D0E2D8E2510970253">>}, 23 | {<<"syn">>, <<"728A9C521B3815831259A98FDEC9C74875612A4D2854E324DD1EC5AD6DF4146E">>}]} 24 | ]. 25 | -------------------------------------------------------------------------------- /rebar3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lambdaclass/webrtc-server/05bcc994d692f8d9f4a601ece1bfd4ff5e939062/rebar3 -------------------------------------------------------------------------------- /src/webrtc_server.app.src: -------------------------------------------------------------------------------- 1 | {application, webrtc_server, 2 | [{description, "An OTP application"}, 3 | {vsn, "0.1.0"}, 4 | {registered, []}, 5 | {mod, { webrtc_server_app, []}}, 6 | {applications, 7 | [kernel, 8 | stdlib, 9 | cowboy, 10 | lager, 11 | stun, 12 | syn 13 | ]}, 14 | {env,[]}, 15 | {modules, []}, 16 | 17 | {maintainers, []}, 18 | {licenses, ["Apache 2.0"]}, 19 | {links, []} 20 | ]}. 21 | -------------------------------------------------------------------------------- /src/webrtc_server.erl: -------------------------------------------------------------------------------- 1 | -module(webrtc_server). 2 | 3 | -export([peers/1, 4 | publish/2, 5 | publish/3, 6 | send/2, 7 | send/3]). 8 | 9 | peers(Room) -> 10 | [{PeerId, Name} || {_Pid, {Name, PeerId}} <- syn:get_members(Room, with_meta)]. 11 | 12 | publish(Room, Event, Data) -> 13 | Message = webrtc_utils:text_event(Event, Data), 14 | syn:publish(Room, Message). 15 | 16 | publish(Room, Event) -> 17 | Message = webrtc_utils:text_event(Event), 18 | syn:publish(Room, Message). 19 | 20 | send(Peer, Event) -> 21 | Pid = syn:find_by_key(Peer), 22 | Message = webrtc_utils:text_event(Event), 23 | Pid ! Message, 24 | ok. 25 | 26 | send(Peer, Event, Data) -> 27 | Pid = syn:find_by_key(Peer), 28 | Message = webrtc_utils:text_event(Event, Data), 29 | Pid ! Message, 30 | ok. 31 | -------------------------------------------------------------------------------- /src/webrtc_server_app.erl: -------------------------------------------------------------------------------- 1 | -module(webrtc_server_app). 2 | 3 | -behaviour(application). 4 | 5 | %% Application callbacks 6 | -export([start/2, stop/1]). 7 | 8 | %% API 9 | 10 | start(_StartType, _StartArgs) -> 11 | syn:init(), 12 | stun_listener:add_listener(3478, udp, [{use_turn, true}, 13 | {turn_ip, resolve_ip()}, 14 | {certfile, config(certfile)}, 15 | {auth_type, user}, 16 | {auth_realm, config(hostname)}, 17 | {auth_fun, get_stun_auth_fun()}]), 18 | webrtc_server_sup:start_link(). 19 | 20 | stop(_State) -> 21 | ok. 22 | 23 | %% Internal functions 24 | config(Key) -> 25 | {ok, Value} = application:get_env(webrtc_server, Key), 26 | Value. 27 | 28 | resolve_ip() -> 29 | case application:get_env(webrtc_server, turn_ip) of 30 | {ok, IP} -> 31 | IP; 32 | undefined -> 33 | Host = case config(hostname) of 34 | H when is_binary(H) -> binary_to_list(H); 35 | H when is_list(H) -> H; 36 | _ -> 37 | lager:error("hostname should be a string or binary"), 38 | throw(bad_hostname) 39 | end, 40 | [IP | _] = inet_res:lookup(Host, in, a), 41 | IP 42 | end. 43 | 44 | get_stun_auth_fun() -> 45 | {AuthMod, AuthFun} = config(auth_fun), 46 | fun (User, _Realm) -> 47 | lager:debug("Stun authentication for ~p", [User]), 48 | %% the stun app considers <<"">> as an auth failure 49 | try 50 | case AuthMod:AuthFun(User) of 51 | Password when is_binary(Password) -> Password; 52 | _ -> <<"">> 53 | end 54 | catch 55 | _:_ -> <<"">> 56 | end 57 | end. 58 | -------------------------------------------------------------------------------- /src/webrtc_server_sup.erl: -------------------------------------------------------------------------------- 1 | -module(webrtc_server_sup). 2 | 3 | -behaviour(supervisor). 4 | 5 | %% API 6 | -export([start_link/0]). 7 | 8 | %% Supervisor callbacks 9 | -export([init/1]). 10 | 11 | -define(SERVER, ?MODULE). 12 | 13 | %% API functions 14 | 15 | start_link() -> 16 | supervisor:start_link({local, ?SERVER}, ?MODULE, []). 17 | 18 | %% Supervisor callbacks 19 | 20 | %% Child :: {Id,StartFunc,Restart,Shutdown,Type,Modules} 21 | init([]) -> 22 | {ok, { {one_for_all, 0, 1}, []} }. 23 | -------------------------------------------------------------------------------- /src/webrtc_utils.erl: -------------------------------------------------------------------------------- 1 | -module(webrtc_utils). 2 | 3 | -export([json_encode/1, 4 | json_decode/1, 5 | text_event/1, 6 | text_event/2]). 7 | 8 | json_decode(Data) -> 9 | jsx:decode(Data, [return_maps, {labels, attempt_atom}]). 10 | 11 | json_encode(Data) -> 12 | jsx:encode(Data). 13 | 14 | text_event(Event) -> 15 | {text, json_encode(#{event => Event})}. 16 | 17 | text_event(Event, Data) -> 18 | {text, json_encode(#{event => Event, data => Data})}. 19 | -------------------------------------------------------------------------------- /src/webrtc_ws_handler.erl: -------------------------------------------------------------------------------- 1 | -module(webrtc_ws_handler). 2 | 3 | -export([init/2, 4 | websocket_init/1, 5 | websocket_handle/2, 6 | websocket_info/2, 7 | terminate/3 8 | ]). 9 | 10 | init(Req, _) -> 11 | Room = cowboy_req:binding(room, Req), 12 | IdleTimeout = application:get_env(webrtc_server, idle_timeout, 60000), 13 | {cowboy_websocket, Req, 14 | #{room => Room, authenticated => false}, 15 | #{idle_timeout => IdleTimeout}}. 16 | 17 | websocket_init(State) -> 18 | Time = application:get_env(webrtc_server, ws_auth_delay, 300), 19 | 20 | %% give the ws some time to authenticate before disconnecting it 21 | timer:send_after(Time, check_auth), 22 | {ok, State}. 23 | 24 | %% not all ws clients can send a ping frame (namely, browsers can't) 25 | %% so we handle a ping text frame. 26 | websocket_handle({text, <<"ping">>}, State) -> 27 | {reply, {text, <<"pong">>}, State}; 28 | 29 | %% Before authentication, just expect the socket to send user/pass 30 | websocket_handle({text, Text}, State = #{authenticated := false, room := Room}) -> 31 | case authenticate(Text) of 32 | {success, Username} -> 33 | lager:debug("socket authenticated"), 34 | 35 | PeerId = peer_id(), 36 | join_room(Room, Username, PeerId), 37 | 38 | State2 = State#{authenticated => true, 39 | username => Username, 40 | peer_id => PeerId}, 41 | 42 | {reply, webrtc_utils:text_event(authenticated, #{peer_id => PeerId}), State2}; 43 | Reason -> 44 | lager:debug("bad authentication: ~p ~p", [Reason, Text]), 45 | {reply, webrtc_utils:text_event(unauthorized), State} 46 | end; 47 | 48 | %% After authentication, any message should be targeted to a specific peer 49 | websocket_handle({text, Text}, State = #{authenticated := true, 50 | room := Room, 51 | peer_id := ThisPeer}) -> 52 | lager:debug("Received text frame ~p", [Text]), 53 | 54 | case webrtc_utils:json_decode(Text) of 55 | #{to := OtherPeer} = Message -> 56 | %% crash if room doesn't match 57 | {Pid, {_Username, _PeerId, Room}} = syn:find_by_key(OtherPeer, with_meta), 58 | 59 | %% extend message with this peer id before sending 60 | Message2 = Message#{from => ThisPeer}, 61 | Pid ! {text, webrtc_utils:json_encode(Message2)}, 62 | 63 | {ok, State}; 64 | _ -> 65 | {reply, webrtc_utils:text_event(invalid_message), State} 66 | end; 67 | 68 | websocket_handle(Frame, State) -> 69 | lager:warning("Received non text frame ~p~p", [Frame, State]), 70 | {ok, State}. 71 | 72 | %% If user/password not sent before ws_auth_delay, disconnect 73 | websocket_info(check_auth, State = #{authenticated := false}) -> 74 | lager:debug("disconnecting unauthenticated socket"), 75 | {stop, State}; 76 | 77 | websocket_info(check_auth, State) -> 78 | %% already authenticated, do nothing 79 | {ok, State}; 80 | 81 | %% incoming text frame, send to the client socket 82 | websocket_info({text, Text}, State = #{authenticated := true}) -> 83 | lager:debug("Sending to client ~p", [Text]), 84 | {reply, {text, Text}, State}; 85 | 86 | websocket_info(Info, State) -> 87 | lager:warning("Received unexpected info ~p~p", [Info, State]), 88 | {ok, State}. 89 | 90 | terminate(_Reason, _Req, #{room := Room, username := Username, peer_id := PeerId}) -> 91 | OtherUsers = [Name || {Pid, {Name, _PeerId}} <- syn:get_members(Room, with_meta), Pid /= self()], 92 | syn:publish(Room, webrtc_utils:text_event(left, #{username => Username, 93 | peer_id => PeerId})), 94 | run_callback(leave_callback, Room, Username, OtherUsers), 95 | ok; 96 | terminate(_Reason, _Req, _State) -> 97 | ok. 98 | 99 | %%% internal 100 | authenticate(Data) -> 101 | try webrtc_utils:json_decode(Data) of 102 | #{event := <<"authenticate">>, data := #{username := User, password := Password}} -> 103 | case safe_auth(User) of 104 | Password -> {success, User}; 105 | _ -> wrong_credentials 106 | end; 107 | _ -> invalid_format 108 | catch 109 | Type:Error -> 110 | lager:debug("invalid json ~p ~p", [Type, Error]), 111 | invalid_json 112 | end. 113 | 114 | safe_auth(Username) -> 115 | {ok, {AuthMod, AuthFun}} = application:get_env(webrtc_server, auth_fun), 116 | try 117 | AuthMod:AuthFun(Username) 118 | catch 119 | _:_ -> 120 | auth_error 121 | end. 122 | 123 | join_room(Room, Username, PeerId) -> 124 | OtherMembers = syn:get_members(Room, with_meta), 125 | syn:register(PeerId, self(), {Username, PeerId, Room}), 126 | syn:join(Room, self(), {Username, PeerId}), 127 | 128 | %% broadcast peer joined to the rest of the peers in the room 129 | Message = webrtc_utils:text_event(joined, #{peer_id => PeerId, 130 | username => Username}), 131 | lists:foreach(fun({Pid, _}) -> Pid ! Message end, OtherMembers), 132 | 133 | OtherNames = [Name || {_, {Name, _Peer}} <- OtherMembers], 134 | run_callback(join_callback, Room, Username, OtherNames). 135 | 136 | run_callback(Type, Room, Username, CurrentUsers) -> 137 | case application:get_env(webrtc_server, Type) of 138 | {ok, {Module, Function}} -> 139 | try 140 | Module:Function(Room, Username, CurrentUsers) 141 | catch 142 | ErrorType:Error -> 143 | lager:error( 144 | "~nError running ~p ~p ~p:~s", 145 | [Type, Room, Username, lager:pr_stacktrace(erlang:get_stacktrace(), 146 | {ErrorType, Error})]) 147 | end; 148 | undefined -> 149 | ok 150 | end. 151 | 152 | peer_id() -> 153 | base64:encode(crypto:strong_rand_bytes(10)). 154 | -------------------------------------------------------------------------------- /test/webrtc_ws_handler_SUITE.erl: -------------------------------------------------------------------------------- 1 | -module(webrtc_ws_handler_SUITE). 2 | -include_lib("common_test/include/ct.hrl"). 3 | 4 | -compile(export_all). 5 | 6 | %% callbacks 7 | auth_fun(_Username) -> 8 | %% password hardcoded for all users 9 | <<"password">>. 10 | 11 | join_callback(Room, Username, OtherUsers) -> 12 | ets:insert(callback_log, {Username, join, Room, OtherUsers}). 13 | 14 | leave_callback(Room, Username, OtherUsers) -> 15 | ets:insert(callback_log, {Username, leave, Room, OtherUsers}). 16 | 17 | all() -> 18 | [ 19 | join_and_send_message, 20 | auth_failure, 21 | callbacks, 22 | ping, 23 | server_api 24 | ]. 25 | 26 | init_per_suite(Config) -> 27 | Port = 8444, 28 | start_cowboy(8444), 29 | application:set_env(webrtc_server, auth_fun, {webrtc_ws_handler_SUITE, auth_fun}), 30 | application:ensure_all_started(webrtc_server), 31 | [{port, Port} | Config]. 32 | 33 | end_per_suite(_Config) -> 34 | ok. 35 | 36 | init_per_testcase(_TestCase, Config) -> 37 | Room = random_name(<<"Room">>), 38 | Port = proplists:get_value(port, Config), 39 | PortBin = integer_to_binary(Port), 40 | Url = <<"wss://localhost:", PortBin/binary, "/websocket/", Room/binary>>, 41 | 42 | [{room, Room}, 43 | {url, Url} | Config]. 44 | 45 | end_per_testcase(_TestCase, _Config) -> 46 | ok. 47 | 48 | join_and_send_message(Config) -> 49 | Url = proplists:get_value(url, Config), 50 | User1 = random_name(<<"User1">>), 51 | User2 = random_name(<<"User2">>), 52 | User3 = random_name(<<"User3">>), 53 | 54 | %% user 1 joins and auths -> created 55 | {ok, Conn1} = ws_client:start_link(Url), 56 | AuthData = #{<<"event">> => <<"authenticate">>, 57 | <<"data">> => #{<<"username">> => User1, 58 | <<"password">> => <<"password">>}}, 59 | {ok, #{<<"event">> := <<"authenticated">>, 60 | <<"data">> := #{<<"peer_id">> := PeerId1}}} = ws_client:send(Conn1, AuthData), 61 | 62 | %% user 2 joins and auths -> joined 63 | {ok, Conn2} = ws_client:start_link(Url), 64 | AuthData2 = #{<<"event">> => <<"authenticate">>, 65 | <<"data">> => #{<<"username">> => User2, 66 | <<"password">> => <<"password">>}}, 67 | {ok, #{<<"event">> := <<"authenticated">>, 68 | <<"data">> := #{<<"peer_id">> := PeerId2}}} = ws_client:send(Conn2, AuthData2), 69 | {ok, #{<<"event">> := <<"joined">>, 70 | <<"data">> := #{<<"peer_id">> := PeerId2}}} = ws_client:recv(Conn1), 71 | 72 | %% user 3 joins 73 | {ok, Conn3} = ws_client:start_link(Url), 74 | AuthData3 = #{<<"event">> => <<"authenticate">>, 75 | <<"data">> => #{<<"username">> => User3, 76 | <<"password">> => <<"password">>}}, 77 | {ok, #{<<"event">> := <<"authenticated">>}} = ws_client:send(Conn3, AuthData3), 78 | {ok, #{<<"event">> := <<"joined">>}} = ws_client:recv(Conn1), 79 | {ok, #{<<"event">> := <<"joined">>}} = ws_client:recv(Conn2), 80 | 81 | %% user 1 sends message, user 2 receives 82 | ws_client:send(Conn1, #{<<"event">> => <<"hello!">>, 83 | <<"to">> => PeerId2}), 84 | {ok, #{<<"event">> := <<"hello!">>}} = ws_client:recv(Conn2), 85 | 86 | %% user 2 sends message, user 1 receives 87 | ws_client:send(Conn2, #{<<"event">> => <<"bye!">>, 88 | <<"to">> => PeerId1}), 89 | {ok, #{<<"event">> := <<"bye!">>}} = ws_client:recv(Conn1), 90 | 91 | %% user 3 received no messages 92 | {error, timeout} = ws_client:recv(Conn3, 200), 93 | 94 | %% user 2 leaves -> left 95 | ws_client:stop(Conn2), 96 | {ok, #{<<"event">> := <<"left">>}} = ws_client:recv(Conn1), 97 | {ok, #{<<"event">> := <<"left">>}} = ws_client:recv(Conn3), 98 | 99 | %% fails without `to` field 100 | {ok, #{<<"event">> := <<"invalid_message">>}} = ws_client:send(Conn1, #{<<"event">> => <<"bye!">>}), 101 | 102 | ok. 103 | 104 | auth_failure(Config) -> 105 | Url = proplists:get_value(url, Config), 106 | User1 = random_name(<<"User1">>), 107 | User2 = random_name(<<"User2">>), 108 | 109 | %% user 1 joins and auths -> created 110 | {ok, Conn1} = ws_client:start_link(Url), 111 | AuthData = #{<<"event">> => <<"authenticate">>, 112 | <<"data">> => #{<<"username">> => User1, 113 | <<"password">> => <<"WRONG!">>}}, 114 | {ok, #{<<"event">> := <<"unauthorized">>}} = ws_client:send(Conn1, AuthData), 115 | 116 | %% user 2 joins and auths -> joined 117 | {ok, Conn2} = ws_client:start_link(Url), 118 | AuthData2 = #{<<"event">> => <<"authenticate">>, 119 | <<"data">> => #{<<"username">> => User2, 120 | <<"password">> => <<"password">>}}, 121 | {ok, #{<<"event">> := <<"authenticated">>}} = ws_client:send(Conn2, AuthData2), 122 | ok. 123 | 124 | callbacks(Config) -> 125 | application:set_env(webrtc_server, join_callback, {webrtc_ws_handler_SUITE, join_callback}), 126 | application:set_env(webrtc_server, leave_callback, {webrtc_ws_handler_SUITE, leave_callback}), 127 | 128 | Url = proplists:get_value(url, Config), 129 | User1 = random_name(<<"User1">>), 130 | User2 = random_name(<<"User2">>), 131 | Room = proplists:get_value(room, Config), 132 | 133 | ets:new(callback_log, [named_table, public, bag]), 134 | 135 | %% user 1 creates the room 136 | {ok, Conn1} = ws_client:start_link(Url), 137 | AuthData = #{<<"event">> => <<"authenticate">>, 138 | <<"data">> => #{<<"username">> => User1, 139 | <<"password">> => <<"password">>}}, 140 | {ok, #{<<"event">> := <<"authenticated">>}} = ws_client:send(Conn1, AuthData), 141 | 142 | %% user 2 joins 143 | {ok, Conn2} = ws_client:start_link(Url), 144 | AuthData2 = #{<<"event">> => <<"authenticate">>, 145 | <<"data">> => #{<<"username">> => User2, 146 | <<"password">> => <<"password">>}}, 147 | {ok, #{<<"event">> := <<"authenticated">>}} = ws_client:send(Conn2, AuthData2), 148 | {ok, #{<<"event">> := <<"joined">>}} = ws_client:recv(Conn1), 149 | 150 | %% users leave the room 151 | ws_client:stop(Conn1), 152 | timer:sleep(100), 153 | ws_client:stop(Conn2), 154 | timer:sleep(100), 155 | 156 | [{User1, join, Room, []}, 157 | {User1, leave, Room, [User2]}] = ets:lookup(callback_log, User1), 158 | [{User2, join, Room, [User1]}, 159 | {User2, leave, Room, []}] = ets:lookup(callback_log, User2), 160 | ok. 161 | 162 | ping(Config) -> 163 | Url = proplists:get_value(url, Config), 164 | User1 = random_name(<<"User1">>), 165 | {ok, Conn1} = ws_client:start_link(Url), 166 | 167 | %% ping before auth 168 | {ok, <<"pong">>} = ws_client:ping(Conn1), 169 | 170 | %% ping after auth 171 | AuthData = #{<<"event">> => <<"authenticate">>, 172 | <<"data">> => #{<<"username">> => User1, 173 | <<"password">> => <<"password">>}}, 174 | {ok, #{<<"event">> := <<"authenticated">>}} = ws_client:send(Conn1, AuthData), 175 | {ok, <<"pong">>} = ws_client:ping(Conn1), 176 | ok. 177 | 178 | server_api(Config) -> 179 | %% connect/auth two users 180 | Url = proplists:get_value(url, Config), 181 | Room = proplists:get_value(room, Config), 182 | User1 = random_name(<<"User1">>), 183 | User2 = random_name(<<"User2">>), 184 | 185 | %% user 1 joins and auths -> created 186 | {ok, Conn1} = ws_client:start_link(Url), 187 | AuthData = #{<<"event">> => <<"authenticate">>, 188 | <<"data">> => #{<<"username">> => User1, 189 | <<"password">> => <<"password">>}}, 190 | {ok, #{<<"event">> := <<"authenticated">>, 191 | <<"data">> := #{<<"peer_id">> := PeerId1}}} = ws_client:send(Conn1, AuthData), 192 | 193 | %% user 2 joins and auths -> joined 194 | {ok, Conn2} = ws_client:start_link(Url), 195 | AuthData2 = #{<<"event">> => <<"authenticate">>, 196 | <<"data">> => #{<<"username">> => User2, 197 | <<"password">> => <<"password">>}}, 198 | {ok, #{<<"event">> := <<"authenticated">>, 199 | <<"data">> := #{<<"peer_id">> := PeerId2}}} = ws_client:send(Conn2, AuthData2), 200 | {ok, #{<<"event">> := <<"joined">>, 201 | <<"data">> := #{<<"peer_id">> := PeerId2}}} = ws_client:recv(Conn1), 202 | 203 | %% get peers 204 | [{PeerId1, User1}, {PeerId2, User2}] = webrtc_server:peers(Room), 205 | 206 | %% publish, both peers receive 207 | {ok, 2} = webrtc_server:publish(Room, greeting, #{<<"hello">> => <<"world">>}), 208 | {ok, #{<<"event">> := <<"greeting">>, 209 | <<"data">> := #{<<"hello">> := <<"world">>}}} = ws_client:recv(Conn1), 210 | {ok, #{<<"event">> := <<"greeting">>, 211 | <<"data">> := #{<<"hello">> := <<"world">>}}} = ws_client:recv(Conn2), 212 | 213 | %% send, peer receives 214 | ok = webrtc_server:send(PeerId2, salutation, #{<<"goodbye">> => <<"world">>}), 215 | {error, timeout} = ws_client:recv(Conn1, 200), 216 | {ok, #{<<"event">> := <<"salutation">>, 217 | <<"data">> := #{<<"goodbye">> := <<"world">>}}} = ws_client:recv(Conn2, 200), 218 | 219 | ok. 220 | 221 | %% internal 222 | start_cowboy(Port) -> 223 | application:ensure_all_started(cowboy), 224 | {ok, Cert} = application:get_env(webrtc_server, certfile), 225 | {ok, Key} = application:get_env(webrtc_server, keyfile), 226 | Dispatch = cowboy_router:compile([{'_', [{"/websocket/:room", webrtc_ws_handler, []}]}]), 227 | {ok, _} = cowboy:start_tls(my_http_listener, 228 | [{port, Port}, 229 | {certfile, Cert}, 230 | {keyfile, Key}], 231 | #{env => #{dispatch => Dispatch}}). 232 | 233 | random_name(Prefix) -> 234 | Random = rand:uniform(10000), 235 | Sufix = integer_to_binary(Random), 236 | <>. 237 | -------------------------------------------------------------------------------- /test/ws_client.erl: -------------------------------------------------------------------------------- 1 | -module(ws_client). 2 | 3 | -behaviour(websocket_client_handler). 4 | 5 | -export([ 6 | send/2, 7 | send_async/2, 8 | recv/1, 9 | recv/2, 10 | ping/1, 11 | start_link/1, 12 | stop/1 13 | ]). 14 | 15 | -export([ 16 | init/2, 17 | websocket_handle/3, 18 | websocket_info/3, 19 | websocket_terminate/3 20 | ]). 21 | 22 | -define(TIMEOUT, 1000). 23 | 24 | %%% api 25 | 26 | start_link(Url) -> 27 | Ref = make_ref(), 28 | State = [{url, Url}, 29 | {ref, Ref}, 30 | {caller, self()}], 31 | {ok, Pid} = websocket_client:start_link(Url, ?MODULE, State), 32 | {ok, #{pid => Pid, ref => Ref}}. 33 | 34 | stop(#{pid := ConnPid}) -> 35 | ConnPid ! stop. 36 | 37 | send(#{pid := ConnPid} = Conn, Msg) -> 38 | MsgJson = jsx:encode(Msg), 39 | websocket_client:cast(ConnPid, {text, MsgJson}), 40 | recv(Conn). 41 | 42 | send_async(#{pid := ConnPid}, Msg) -> 43 | MsgJson = jsx:encode(Msg), 44 | websocket_client:cast(ConnPid, {text, MsgJson}). 45 | 46 | recv(Conn) -> 47 | recv(Conn, ?TIMEOUT). 48 | 49 | recv(#{ref := Ref}, Timeout) -> 50 | receive 51 | {ws_client, reply, Ref, AnswerMsg} -> 52 | {ok, AnswerMsg} 53 | after Timeout -> 54 | {error, timeout} 55 | end. 56 | 57 | ping(#{pid := ConnPid} = Conn) -> 58 | websocket_client:cast(ConnPid, {text, <<"ping">>}), 59 | recv(Conn). 60 | 61 | %%% websocket client callbacks 62 | 63 | init(State, _ConnState) -> 64 | {ok, State}. 65 | 66 | websocket_handle({text, <<"pong">> = Msg}, _ConnState, State) -> 67 | Caller = proplists:get_value(caller, State), 68 | Ref = proplists:get_value(ref, State), 69 | 70 | Caller ! {ws_client, reply, Ref, Msg}, 71 | {ok, State}; 72 | websocket_handle({text, MsgJson}, _ConnState, State) -> 73 | Caller = proplists:get_value(caller, State), 74 | Ref = proplists:get_value(ref, State), 75 | 76 | Msg = jsx:decode(MsgJson, [return_maps]), 77 | Caller ! {ws_client, reply, Ref, Msg}, 78 | {ok, State}. 79 | 80 | websocket_info(stop, _, State) -> 81 | {close, <<>>, State}; 82 | websocket_info(_Msg, _ConnState, State) -> 83 | {ok, State}. 84 | 85 | websocket_terminate(_Msg, _ConnState, _State) -> 86 | ok. 87 | --------------------------------------------------------------------------------