├── .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 [](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 |
24 |
25 |
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 |
--------------------------------------------------------------------------------