A simple p2p video conference – the server application
We prepared a client-side code to be executed inside a web browser. Now it is time to develop the signaling server. As a transport layer for the signaling mechanism, we will use WebSockets; it is supported well by all web browsers that support WebRTC, and this protocol is pretty suitable for the signaling role.
The application description file
The application description file describes our application. It is something similar to the manifest
file for C# applications or Java applets. Here, we describe what our application itself is, define its version number, define other modules it depends on, and so on.
Edit the apps/rtcserver/src/rtcserver.app.src
file.
The application ID/name is as follows:
{application, rtcserver, [
The application description is not mandatory, so for now, we can skip it. The version number is set to 1
as we have just started. The applications
option gives a list of applications that we depend on. We also define the main module's name and environment variables (empty list):
{description, ""}, {vsn, "1"}, {registered, []}, {applications, [ kernel, stdlib, cowlib, cowboy, compiler, gproc ]}, {mod, { rtcserver_app, []}}, {env, []} ]}.
The application module
This application module is the main module of our signaling server application. Here, we start all the applications we're depending on and set up a web server and WebSocket handler for it.
Edit the apps/rtcserver/src/rtcserver_app.erl
the file.
The module name should be the same as the file name:
-module(rtcserver_app).
We tell Erlang VM that this is an application module:
-behaviour(application).
Describe which functions should be accessible from this module; /2
, /1
, and /0
are the parities of the function, that is, the number of arguments:
-export([start/2, stop/1, start/0]).
Now we need to start all the helping applications that we're depending on and then start our application itself:
start() -> ok = application:start(compiler),
Ranch is an effective connection pool:
ok = application:start(ranch),
Crypto needs to support SSL:
ok = application:start(crypto),
Cowboy is a lightweight web server that we use to build our signaling server on WebSockets:
ok = application:start(cowlib), ok = application:start(cowboy),
We use gproc
as a simple key/value DB in the memory to store the virtual rooms' numbers:
ok = application:start(gproc),
Start our application:
ok = application:start(rtcserver).
The following function will be called during the process of starting the application:
start(_StartType, _StartArgs) ->
First of all, we define a dispatcher, an entity used by the Cowboy application. With the dispatcher, we tell Cowboy where it should listen for requests from the clients and how to map requests to handlers:
Dispatch = cowboy_router:compile([ {'_',[
Here, we define that every /*
request to our signaling server should be processed by the handler_websocket
module (will be reviewed on the following page):
{"/", handler_websocket,[]} ]} ]),
Here, we ask Cowboy to start listening and processing clients' requests. Our HTTP process is named websocket
; it should listen on port 30000
and bind to any available network interface(s). The connection timeout value is set to 500
ms and the max_keep_alive
timeout is set to 50
seconds:
{ok, _} = cowboy:start_http(websocket, 100, [{port, 30000}], [{env, [{dispatch, Dispatch}]}, {max_keepalive, 50}, {timeout, 500} ]),
To make our application work, we need to call the start_link
function of the application's supervisor:
rtcserver_sup:start_link().
The following function is called when we want to stop the signaling server:
stop(_State) -> ok.
The server supervisor
To make our Erlang-based signaling server work properly, we need to implement a supervisor process. This is the standard way in which Erlang applications usually work. This is not something specific to WebRTC applications, so we won't dive into deep details here. The code is very short.
Edit the apps/rtcserver/src/rtcserver_sup.erl
file:
-module(rtcserver_sup). -behaviour(supervisor). -export([start_link/0]). -export([init/1]). -define(CHILD(I, Type), {I, {I, start_link, []}, permanent, 5000, Type, [I]}). start_link() -> supervisor:start_link({local, ?MODULE}, ?MODULE, []). init([]) -> {ok, { {one_for_one, 5, 10}, []} }.
The WebSocket handler
A WebSocket handler module will implement the signaling server's functionality. It will communicate with both the peers, create rooms, and do all the other stuff that we're awaiting to get done from the signaling server.
Edit the apps/rtcserver/src/handler_websocket.erl
file:
-module(handler_websocket). -behaviour(cowboy_websocket_handler). -export([init/3]). -export([websocket_init/3, websocket_handle/3, websocket_info/3, websocket_terminate/3]).
The following is a record where we can store useful information about the connection and peers:
-record(state, { client = undefined :: undefined | binary(), state = undefined :: undefined | connected | running, room = undefined :: undefined | integer() }).
We're trapped here when a peer tries to connect to the signaling server. At this stage, we just need to reply with the upgrade
state to establish the WebSockets connection with the web browser properly:
init(_Any, _Req, _Opt) -> {upgrade, protocol, cowboy_websocket}.
The following function is called when the connection is established (a peer has been connected to the signaling server):
websocket_init(_TransportName, Req, _Opt) ->
Get the x-forwarded-for
field from HTTP request header, and store it as the peer's IP address:
{Client, Req1} = cowboy_req:header(<<"x-forwarded-for">>, Req), State = #state{client = Client, state = connected}, {ok, Req1, State, hibernate}.
The following function is called when we get a message from some of our peers. We need to parse the message, decide what to do, and reply if necessary:
websocket_handle({text,Data}, Req, State) ->
Mark our state as running
; the new peer is connected and the peer to signaling server connection has been established:
StateNew = case (State#state.state) of started -> State#state{state = running}; _ -> State end,
We use JSON to encode messages that are transferred between the clients and the signaling server, so we need to decode the message:
JSON = jsonerl:decode(Data), {M,Type} = element(1,JSON), case M of <<"type">> -> case Type of
The type of the message is GETROOM
; someone wants to create a virtual room. Here, we will create the room and reply with the room's number:
<<"GETROOM">> ->
We use the generate_room
function to create a virtual room:
Room = generate_room(),
Construct the answer message and encode it to JSON:
R = iolist_to_binary(jsonerl:encode({{type, <<"GETROOM">>}, {value, Room}})),
Store the room number and the associated process ID in the key/value DB. If someone tries to enter a virtual room, we need some mechanism to understand whether the room exists:
gproc:reg({p,l, Room}),
Store the room number in the state
entity; we will want to reuse this value further on:
S = (StateNew#state{room = Room}),
Send our reply back to the peer and exit:
{reply, {text, <<R/binary>>}, Req, S, hibernate};
If the message type is ENTERROOM
, it means that someone tries to enter a virtual room that does exist and someone has to be present in this room already:
<<"ENTERROOM">> ->
Extract the room number from the message and look up all the participants present in the virtual room:
{<<"value">>,Room} = element(2,JSON), Participants = gproc:lookup_pids({p,l,Room}), case length(Participants) of
If we have just one participant, register the new peer process ID in this room and store the room number in the state
entity:
1 -> gproc:reg({p,l, Room}), S = (StateNew#state{room = Room}), {ok, Req, S, hibernate};
Otherwise, reply with the WRONGROOM
message back to the peer:
_ -> R = iolist_to_binary(jsonerl:encode({{type, <<"WRONGROOM">>}})), {reply, {text, <<R/binary>>}, Req, StateNew, hibernate} end;
If we get a message of some other type, then just transfer it to connected peer:
_ -> reply2peer(Data, StateNew#state.room), {ok, Req, StateNew, hibernate} end; _ -> reply2peer(Data, State#state.room), {ok, Req, StateNew, hibernate} end;
If we get a message of an unknown sort, we just ignore it:
websocket_handle(_Any, Req, State) -> {ok, Req, State, hibernate}.
The preceding method is called when we receive a message from the other process; in this case, we send the message to the connected peer. We will use the following code to implement the web chat and data transfer functionality in later chapters:
websocket_info(_Info, Req, State) -> {reply, {text,_Info}, Req, State, hibernate}.
The following code is called when the connection is terminated (the remote peer closed the web browser, for example):
websocket_terminate(_Reason, _Req, _State) -> ok.
Send a message (R
) to every peer that is connected to the room except the one we received the message from:
reply2peer(R, Room) -> [P ! <<R/binary>> || P <- gproc:lookup_pids({p,l,Room}) -- [self()]].
Generate the virtual room number using a random number generator:
generate_room() -> random:seed(now()), random:uniform(999999).
Developing a configuration script for Rebar
We need to tell the Rebar tool which applications our server is dependent on and where we can download them.
Edit the apps/rtcserver/rebar.config
file:
{erl_opts, [warnings_as_errors]}. {deps, [ {'gproc', ".*", { git, "git://github.com/esl/gproc.git", {tag, "0.2.16"} }}, {'jsonerl', ".*", { git, "git://github.com/fycth/jsonerl.git", "master" }}, {'cowboy', ".*", { git,"https://github.com/extend/cowboy.git","0.9.0" }} ]}.
Compiling and running the signaling server
Create another rebar.config
file under your project's folder:
{sub_dirs, [ "apps/rtcserver" ]}.
This configuration file tells the Rebar tool that it needs to look into apps/rtcserver
and process the content.
Now, go to the project's directory and execute the following command in the console:
rebar get-deps
It will download all the necessary dependencies to the deps
directory.
We want to compile our code, so we execute the following command:
rebar compile
It will compile our application and dependencies. After this gets completed, start the rtcserver
application using the following command:
erl -pa deps/*/ebin apps/*/ebin -saslerrlog_type error -s rtcserver_app
Using this command, you will get into the Erlang VM console and start the signaling server (the rtcserver
application). From now on, it will listen on the TCP port 30000
(or the other one, if you changed it in the code).
You can check where the server is listening on the requests using the netstat
command. For Linux, you can use the following command:
netstat –na | grep 30000
If the server is running, you should see it listening on the port that is binded to the 0.0.0.0
address.
For Windows, you can use the following construction:
netstat –na | findstr 30000
Let's start the conference!
We started the signaling server and now it is time to test our application. Now, point your web browser to the domain you prepared for your application. It should open the index page with your web camera's view on it. Above the camera's view, you should see the URL that will direct the second participant to join the conference. Open this URL on another machine, and the connection should establish automatically and both sides should be able to see each other's videos.
To stop the signaling server and quit from VM console, you can use the q().
command.