Guide
Peer initialization
For each browser client, there is a process on the server side, which is called peer. Peers,
grouped in rooms, are responsible for communication with their clients. Since this communication
is based on WebSocket, the peer process starts when a client opens a WebSocket connection
(probably with new WebSocket(URL)) and thus, upgrade request is send.
After receiving a request, Membrane.WebRTC.Server.Peer.peer_id/0 will be automatically
generated. Then a peer should parse the request with
Membrane.WebRTC.Server.Peer.parse_request/1 callback. Credentials and metadata returned from
it will be used to create Membrane.WebRTC.Server.Peer.AuthData and room's name will be used
to get a room's PID from the registry (specified in Membrane.WebRTC.Options. Then, the state is
initialized in Membrane.WebRTC.Server.Peer.on_init/3 and authentication is performed with
Membrane.WebRTC.Server.Peer.AuthData extracted from the request. Finally,
WebSocket is initialized.
After successful initialization, the peer will try to join the room with the name returned from
the Membrane.WebRTC.Server.Peer.parse_request/1. Authorization (with the same
Membrane.WebRTC.Server.Peer.AuthData) or other checks can be performed in
Membrane.WebRTC.Server.Room.on_join/2 callback. Room automatically broadcast
Membrane.WebRTC.Server.Message.joined_message/0 to notify other peers about the new one.
After that, the peer will send Membrane.WebRTC.Server.Message.authenticated_message/0 to the
client to inform about successful initialization.

Signalling
The client communicates with a peer by exchanging JSON messages. These messages should have
the same fields as the Membrane.WebRTC.Server.Message, which is used in internal communication.
Other fields of JSON will be lost in decoding.
Every JSON received from the client will be decoded into the Membrane.WebRTC.Server.Message
struct. The peer will set :from field with own peer_id. Then it will send a message to the room,
where it will be forwarded to the addressees. The addressees and sender are specified in
to and from message fields by peer_ids.
The message can be modified or ignored by both peer and room using
Membrane.WebRTC.Server.Peer.on_receive/3 and Membrane.WebRTC.Server.Room.on_forward/2
callbacks. The addressee peer, after receiving the message will encode it back to JSON
and send it to its client.

Creating example application
This guide focus on writing simple WebRTC application based on Plug.
Complete source code can be found here.
Setting up a project
Create a new mix project with
$ mix new server --module ServerTo create a server, we have to add Membrane WebRTC Server to dependencies. Add this line
to the deps in mix.exs.
{:membrane_webrtc_server, "~> 0.1"}We will also use Plug (i.a. to set up routing) and Jason (to parse credentials received from
the app:
{:jason, "~> 1.1"},
{:plug, "~> 1.7"},
{:plug_cowboy, "~> 2.0"}Creating a peer module
Inside lib/server folder create a new module which uses Membrane.WebRTC.Server.Peer.
defmodule Server.Peer
use Membrane.WebRTC.Server.Peer
...
Implementing parse_request callback
Before initialization of peer, an authentication request is parsed in parse_request. This function
receives the request and should return a tuple containing :ok atom, credentials, metadata and name of
the room peer wants to join.
@impl true
def parse_request(request) do
...
{:ok, credentials, metadata, room_name}
endLet's assume that JS client will specify room in URL binding. We can get the name with the following function:
defp get_room_name(request) do
room_name = :cowboy_req.binding(:room, request)
if room_name == :undefined do
{:error, :no_room_name_bound_in_url}
else
{:ok, room_name}
end
endEasy way to send credentials with WebSocket upgrade request is to include them in a cookie. After
retrieving them, we should decode them with Jason.decode.
defp get_credentials(request) do
case :cowboy_req.parse_cookies(request) |> List.keyfind("credentials", 0) do
{"credentials", json} ->
Jason.decode(json)
_ ->
{:error, :no_credentials_passed}
endNow we can finish implementing parse_request. We have our room name, credentials and we don't
need metadata for this request, so we will return nil instead of it. The finished function should
look like something like this:
@impl true
def parse_request(request) do
with {:ok, room_name} <- get_room_name(request),
{:ok, credentials} <- get_credentials(request) do
{:ok, credentials, nil, room_name}
end
endPlease notice that storing non-hashed credentials in cookie is unsafe (since they are available as plain text). Example of more sophisticated authentication based on Guardian can be found here.
Authentication
Authentication happens before WebSocket initialization. We can perform it in on_init callback,
for example:
@impl true
def on_init(_context, auth_data, _options) do
username = Map.get(auth_data.credentials, "username")
password = Map.get(auth_data.credentials, "password")
if username == "USERNAME" and password == "PASSWORD" do
{:ok, %{}}
else
{:error, :wrong_credentials}
end
endThe return value (in case of successful authentication) contains an empty map, which is a new state for the peer.
Implementing room
Since mesh WebRTC can't scale to a large number of participants, we will create Room which will block not let more than 2 peers in.
Inside lib/server/ folder create a new module which uses Membrane.WebRTC.Server.Room.
defmodule Example.Simple.Room do
use Membrane.WebRTC.Server.RoomStarting room process will get the maximal number of peers from initial options
(value under :custom_options field in Membrane.WebRTC.Server.Room.Options).
We can specify that behaviour in handle_init implementation.
@impl true
def on_init(options) do
{:ok, %{number_of_peers: 0, max_peers: options.max_peers}}
endSo, as you can see, options will be map with field :max_peers.
The return value contains a map (new state of our room) with the current number of peers and the maximal
number of peers. Starting room is empty, so :number_of_peers equals 0.
Every time peer will join the room we must check, if we surpass allowed number. If not,
the room must increment it. We'll specify that behaviour in on_join callback.
@impl true
def on_join(_auth_data, state) do
current_number = state.number_of_peers
if current_number < state.max_peers do
{:ok, Map.put(state, :number_of_peers, current_number + 1)}
else
{{:error, :room_is_full}, state}
end
endWhen this function return error, an error message will be sent to the client.
Of course, we also have to decrement the number of peers if some peer leaves.
@impl true
def on_leave(_peer_id, state) do
{:ok, Map.put(state, :number_of_peers, state.number_of_peers - 1)}
endTo sum up, the whole file should look like this:
defmodule Server.Room do
use Membrane.WebRTC.Server.Room
@impl true
def on_init(options) do
{:ok, %{number_of_peers: 0, max_peers: options.max_peers}}
end
@impl true
def on_join(_auth_data, state) do
current_number = state.number_of_peers
if current_number < state.max_peers do
{:ok, Map.put(state, :number_of_peers, current_number + 1)}
else
{{:error, :room_is_full}, state}
end
end
@impl true
def on_leave(_peer_id, state) do
{:ok, Map.put(state, :number_of_peers, state.number_of_peers - 1)}
end
endConfiguring router
As mentioned before, this application uses Plug to set up routing. Let's configure our router:
defmodule Example.Simple.Router do
use Plug.Router
plug(Plug.Static,
at: "/",
from: :example_simple
)
plug(:match)
plug(:dispatch)
get "/:room" do
send_file(conn, 200, "priv/static/html/index.html")
end
match _ do
send_resp(conn, 404, "404")
end
endAs you can see, the URL will specify room for the client.
Generating key and certificate
Since the application uses HTTPS, certificate and key are needed to run it. You generate them with
$ openssl req -newkey rsa:2048 -nodes -keyout priv/certs/key.pem -x509 -days 365 -out priv/certs/certificate.pemNote that this certificate is not validated and thus may cause warnings in the browser.
Dispatching and starting the application
Now, with ready Room, Peer and Router, we can configure how our application is started. Inside
lib/server.ex create module Server, which uses Application.
defmodule Server do
use Application
alias Membrane.WebRTC.Server.Peer
alias Membrane.WebRTC.Server.Room
...First of all, we have to configure our dispatch function. It will specify routes rules. Let's
assume that a WebSocket upgrade request will be given at /socket/[:room]. Our Router will take
care of every other request.
defp dispatch do
peer_options = %Peer.Options{module: Server.Peer}
[
{:_,
[
{"/socket/[:room]/", Membrane.WebRTC.Server.Peer, peer_options},
{:_, Plug.Cowboy.Handler, {Example.Simple.Router, []}}
]}
]
endAs you can see, we also specify options for starting peer process.
We have to implement a start function, in which we will start other processes.
@impl true
def start(_type, _args) do
options = [strategy: :one_for_one, name: Server]
children = [
...
]
Supervisor.start_link(children, options)
endInside the children list, we will specify two workers: Plug.Cowboy and Server.Room.
children = [
Plug.Cowboy.child_spec(
scheme: Application.fetch_env!(:server, :scheme),
plug: Example.Simple.Router,
options: [
dispatch: dispatch(),
port: 8443,
ip: {0, 0, 0, 0},
password: "PASSWORD",
otp_app: :example_simple,
keyfile: "priv/certs/key.pem",
certfile: "priv/certs/certificate.pem"
]
),
Supervisor.child_spec(
{Room,
%Room.Options{
name: "room",
module: Server.Room,
custom_options: %{max_peers: 2}
}},
id: :room
)
]If you want to start room after application is started (i.e. every time peer wants to join
non-existing room), you can use Room.start_supervised function.
We can also add other rooms, with different names and/or maximal numbers of peers.
...
Supervisor.child_spec(
{Room,
%Room.Options{
name: "other",
module: Server.Room,
custom_options: %{max_peers: 4}
}},
id: :other_room
)
...