Skip to content

Commit 7d4956d

Browse files
authored
Merge pull request #15 from membraneframework/update_videoroom
Update videoroom tutorial description
2 parents 5742e36 + bdbbcdd commit 7d4956d

File tree

5 files changed

+208
-145
lines changed

5 files changed

+208
-145
lines changed

videoroom/3_SystemArchitecture.md

Lines changed: 28 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,27 @@
11
---
22
title: 3. System architecture
33
description: >-
4-
Create your very own videoconferencing room with a little help from the Membrane Framework!
5-
<div>
6-
<br> <b>Page:</b> <a style="color: white" href=https://www.membraneframework.org/>Membrane Framework</a>
7-
<br> <b>Forum:</b> <a style="color: white" href=https://elixirforum.com/c/elixir-framework-forums/membrane-forum/104/>Membrane Forum</a>
8-
</div>
4+
Create your very own videoconferencing room with a little help from the Membrane Framework!
5+
<div>
6+
<br> <b>Page:</b> <a style="color: white" href=https://www.membraneframework.org/>Membrane Framework</a>
7+
<br> <b>Forum:</b> <a style="color: white" href=https://elixirforum.com/c/elixir-framework-forums/membrane-forum/104/>Membrane Forum</a>
8+
</div>
99
---
1010
# Planning is always a good idea
11-
Hang on for a moment! I know that after slipping through the tons of the documentation you are really eager to start coding, but let's think for a moment before taking any actions. How do we want our application to look like?
11+
Hang on for a moment! I know that after slipping through the tons of the documentation you are really eager to start coding, but let's think for a moment before taking any actions. What do we want our application to look like?
1212
Can we somehow decompose our application?
1313

1414
Sure we can - as in each web application we have two independent subsystems:
1515
+ server (backend) - written in Elixir, one and the only for the whole system. It will spawn a `Room` process for each of the rooms created by the users, which will handle
16-
signaling and relay media among the peers in the room.
17-
+ client application (frontend) - the one written in form of JS code and executed on each client's machine (to be precise - by client's web browser). It will be responsible for fetching user's media stream as well as displaying the stream from the peers.
16+
signaling and relay media among the peers in the room.
17+
+ client application (frontend) - the one written in form of JS code and executed on each client's machine (to be precise - by client's web browser). It will be responsible for fetching the user's media stream as well as displaying the stream from the peers.
1818

1919
## We might need something else than the plain Elixir standard library...
20-
Ugh...I am sure till now on you have already found out that media streaming is not that easy. It covers many topic which originates to the nature of the reality.
20+
Ugh...I am sure till now on you have already found out that media streaming is not that easy. It covers many topics which originate from the nature of reality.
2121
We need to deal with some limitations brought to us by the physics of the surrounding universe, we want to compress the data being sent with the great tools
22-
mathematics has equipped us with, we are taking an advantage of imperfections of our perception system...
22+
mathematics has equipped us with, we are taking advantage of imperfections of our perception system...
2323
All this stuff is both complex and complicated - and that is why we don't want to design it from very scratch. Fortunately, we have access to the protocols
24-
and codecs - ICE, RTP, H264, VP9, VP8, Opus etc. - which already solves the aforementioned problems. But that's not enough -
24+
and codecs - ICE, RTP, H264, VP9, VP8, Opus, etc. - which already solves the aforementioned problems. But that's not enough -
2525
those protocols are also complicated and implementing or even using them requires digging into their fundamentals.
2626
That is why we will be using the framework that provides some level of abstraction on top of these protocols. Ladies and gents - let me introduce to you - the Membrane framework.
2727
## What does Membrane framework do?
@@ -44,32 +44,38 @@ And here is how the **SFU Engine** will relay multimedia streams:<br>
4444

4545
In terms of media streaming, our server will be a Selective Forwarding Unit (SFU).
4646
Why do we want our server to be a Selective Forwarding Unit? The reason is that such a model of streaming data
47-
among peers allows us to balance between server's and client's bandwidth and limit CPU usage of the server.
48-
SFU is receiving stream from each of the peers and passes each of these streams to each of the other peers. <br>
47+
among peers allows us to balance between the server's and client's bandwidth and limit CPU usage of the server.
48+
RTC is receiving streams from each of the peers and passes each of these streams to each of the other peers. <br>
4949

5050
## Server
51-
As pointed previously, the server will have two responsibilities - the first one is that it will work as a signalling server, broadcasting event messages among the peers.
51+
As pointed out previously, the server will have two responsibilities - the first one is that it will work as a signaling server, broadcasting event messages among the peers.
5252
The second one is that it will act as a streaming server.
53-
The Membrane Framework provides Selective Forwarding Unit implementation called `SFU Engine`, which handles both the signaling and streaming.
54-
In the tutorial we will wrap the `SFU Engine` and provide business logic in order to add video room functionalities.
53+
A Selective Forwarding Unit implementation in the Membrane Framework can be achieved with `RTC Engine` plugin, which is capable of both the signaling and streaming media.
54+
In the tutorial, we will wrap the `RTC Engine` and provide business logic in order to add video room functionalities.
5555

5656
The server will consist of two components holding the logic and two components needed for communication.
5757
The communication will be done with the use of Phoenix sockets and that is why we will need to define the `socket` itself and a `channel` for each of the rooms.
5858

59-
The "heart" of the server will be `SFU Engine` - it will deal with all the dirty stuff connected with both the signaling and streaming. We will also have a separate `Room` process (one per each of the video rooms) whose responsibility will be to aggregate information about peers in the particular room.
60-
`SFU Engine` will send event messages (e.g. `:new_peer` or `:sfu_media_event` messages) on which obtainment the `Room` process will react, for instance, by dispatching them to the appropriate peer's `channel`. `Channel` will then send those messages to the client via the `socket`.
61-
Messages coming on the `socket` will be dispatched to the appropriate `channel`. Then the `channel` will send them to the `Room`'s process, which finally will pass them to the `SFU Engine`.
62-
Media transmission will be done with the use of streaming protocols. The way in which this will be performed is out the scope of this tutorial. The only thing you need to know is that SFU Engine will also take care of it.
59+
The "heart" of the server will be `RTC Engine` - it will deal with all the dirty stuff connected with both the signaling and streaming. We will also have a separate `Room` process (one per each of the video rooms) whose responsibility will be to aggregate information about peers in the particular room.
60+
`RTC Engine` will send event messages (e.g. `:new_peer` or `:sfu_media_event` messages) on which reception the `Room` process will react, for instance, by dispatching them to the appropriate peer's `channel`. `Channel` will then send those messages to the client via the `socket`.
61+
Messages coming on the `socket` will be dispatched to the appropriate `channel`. Then the `channel` will send them to the `Room`'s process, which finally will pass them to the `RTC Engine`. RTC Engine will receive them inside its endpoints since each peer will have a corresponding endpoint in the RTC Engine, as presented on the diagram below:
62+
63+
![RTC Engine](assets/images/modular_rtc.png)
64+
65+
Note that the scheme is simplified and does not show elements (i.e. channels) that are in between the RTC Engine and the peers' browsers
66+
If you want to find out more about the inner architecture of the RTC Engine, please refer [here](https://blog.swmansion.com/modular-rtc-engine-is-our-little-big-revolution-in-video-conferencing-cfde806c5beb).
67+
68+
Media transmission will be done with the use of streaming protocols. How this will be performed is out of the scope of this tutorial. The only thing you need to know is that RTC Engine will also take care of it.
6369

6470
## Client
6571
Each client's application will have a structure reassembling the structure of the server.
6672

6773
In the `Room` instance, the client will receive messages sent from the server on the associated `channel`. The `Room` will call the appropriate methods of `MembraneWebRTC` object.
68-
At the same time, `MembraneWebRTC` object will be able to change the `Room`'s state by invoking the callbacks provided during construction of this object. These callbacks as well as the `Room` object itself will be able to update the user's interface.
74+
At the same time, `MembraneWebRTC` object will be able to change the `Room`'s state by invoking the callbacks provided during the construction of this object. These callbacks as well as the `Room` object itself will be able to update the user's interface.
6975

7076
Be aware that `MembraneWebRTC` object will also take care of the incoming media stream.
7177
<br><br>
7278
[NEXT - Server's communication channels](4_CreatingServersCommunicationChannels.md)<br>
7379
[PREV - Environment preparation](2_EnvironmentPreparation)<br>
7480
[List of contents](index.md)<br>
75-
[List of tutorials](../../index.md)
81+
[List of tutorials](../../index.md)
Lines changed: 58 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,17 @@
11
---
22
title: 4. Server's communication channels
33
description: >-
4-
Create your very own videoconferencing room with a little help from the Membrane Framework!
5-
<div>
6-
<br> <b>Page:</b> <a style="color: white" href=https://www.membraneframework.org/>Membrane Framework</a>
7-
<br> <b>Forum:</b> <a style="color: white" href=https://elixirforum.com/c/elixir-framework-forums/membrane-forum/104/>Membrane Forum</a>
8-
</div>
4+
Create your very own videoconferencing room with a little help from the Membrane Framework!
5+
<div>
6+
<br> <b>Page:</b> <a style="color: white" href=https://www.membraneframework.org/>Membrane Framework</a>
7+
<br> <b>Forum:</b> <a style="color: white" href=https://elixirforum.com/c/elixir-framework-forums/membrane-forum/104/>Membrane Forum</a>
8+
</div>
99
---
1010

1111
# I know you have been waiting for that moment - let's start coding!
1212
## Let's prepare the server's endpoint
1313
Do you still remember about Phoenix's sockets? Hopefully, since we will make use of them in a moment! We want to provide a communication channel between our client's application and our server.
14-
Sockets fit just in a place - but be aware, that it is not the only possible option. Neither WebRTC nor Membrane Framework expects you to use any particular mean of communication between
14+
Sockets fit just in a place - but be aware, that it is not the only possible option. Neither WebRTC nor Membrane Framework expects you to use any particular means of communication between
1515
the server and the client - they just want you to communicate.
1616

1717
### Socket's declaration
@@ -22,11 +22,11 @@ You will find the following code there:
2222
#FILE: lib/videoroom_web/user_socket.ex
2323

2424
defmodule VideoRoomWeb.UserSocket do
25-
use Phoenix.Socket
25+
use Phoenix.Socket
2626

27-
channel("room:*", VideoRoomWeb.PeerChannel)
27+
channel("room:*", VideoRoomWeb.PeerChannel)
2828

29-
...
29+
...
3030
end
3131
```
3232

@@ -42,12 +42,12 @@ That's quite easy - we defined the usage of our socket in `lib/videoroom_web/end
4242
#FILE: lib/videoroom_web/endpoint.ex
4343

4444
defmodule VideoRoomWeb.Endpoint do
45-
...
46-
socket("/socket", VideoRoomWeb.UserSocket,
47-
websocket: true,
48-
longpoll: false
49-
)
50-
...
45+
...
46+
socket("/socket", VideoRoomWeb.UserSocket,
47+
websocket: true,
48+
longpoll: false
49+
)
50+
...
5151
end
5252
```
5353
In this piece of code we are simply saying, that we are defining socket-type endpoint with path ```"/socket"```, which behavior will be described by
@@ -59,9 +59,9 @@ It is in `lib/videoroom_web/peer_channel.ex` file! However, for now on, this fil
5959
#FILE: lib/videoroom_web/peer_channel.ex
6060

6161
defmodule VideoRoomWeb.PeerChannel do
62-
use Phoenix.Channel
62+
use Phoenix.Channel
6363

64-
require Logger
64+
require Logger
6565

6666
end
6767
```
@@ -74,39 +74,54 @@ Let's implement our first callback!
7474

7575
@impl true
7676
def join("room:" <> room_id, _params, socket) do
77-
case :global.whereis_name(room_id) do
78-
:undefined -> Videoroom.Room.start(name: {:global, room_id})
79-
pid -> {:ok, pid}
80-
end
81-
|> case do
82-
{:ok, room} ->
83-
peer_id = "#{UUID.uuid4()}"
84-
Process.monitor(room)
85-
send(room, {:add_peer_channel, self(), peer_id})
86-
{:ok, Phoenix.Socket.assign(socket, %{room_id: room_id, room: room, peer_id: peer_id})}
87-
88-
{:error, reason} ->
89-
Logger.error("""
77+
case :global.whereis_name(room_id) do
78+
:undefined -> Videoroom.Room.start(room_id, name: {:global, room_id})
79+
pid -> {:ok, pid}
80+
end
81+
|> case do
82+
{:ok, room_pid} ->
83+
do_join(socket, room_pid, room_id)
84+
85+
{:error, {:already_started, room_pid}} ->
86+
do_join(socket, room_pid, room_id)
87+
88+
{:error, reason} ->
89+
Logger.error("""
9090
Failed to start room.
9191
Room: #{inspect(room_id)}
9292
Reason: #{inspect(reason)}
93-
""")
93+
""")
9494

95-
{:error, %{reason: "failed to start room"}}
96-
end
95+
{:error, %{reason: "failed to start room"}}
96+
end
97+
end
98+
99+
100+
defp do_join(socket, room_pid, room_id) do
101+
peer_id = "#{UUID.uuid4()}"
102+
Process.monitor(room_pid)
103+
send(room_pid, {:add_peer_channel, self(), peer_id})
104+
{:ok,
105+
Phoenix.Socket.assign(socket, %{room_id: room_id, room_pid: room_pid, peer_id: peer_id})}
97106
end
98107
```
99108
Just the beginning - note how do we fetch the room's name by using pattern matching in the argument list of `join/3`. ([pattern matching in Elixir](https://elixir-lang.org/getting-started/pattern-matching.html#pattern-matching)). <br>
100109

101110
What happens here?
102-
`join/3` is called when the client joins the channel. First, we are looking for a `Videoroom.Room` process saved in the global registry under the `room_id` key.
111+
`join/3` is called when the client joins the channel. First, we are looking for a `Videoroom.Room` process saved in the `:global` registry under the `room_id` key.
103112
(`Videoroom.Room` module will hold the whole business logic of the video room - we will implement this module in the next chapter).
104-
If videoroom process is already registered, we are simply returning its PID. Otherwise, we are trying to create
113+
If the videoroom process is already registered, we are simply returning its PID. Otherwise, we are trying to create
105114
a new `Videoroom.Room` process on the fly (and we register it with `room_id` key in the global registry).
106115
If we are successful we return the PID of the newly created room's process.
107116
At the entrance point of the following step, we already have a `Videoroom.Room` process's pid or an `:error` notification.
108-
In case of an error occurring we have a simple error handler that logs the fact, that the room has failed to start. Otherwise, we can make use of the room's process.
109-
First, we start to monitor it (so that we will receive ```:DOWN``` message in case of the room's process crash/failure). Then we notify the room's process that
117+
Errors can occur due to multiple reasons. One of them is a situation in which a race condition between peers trying to create a room takes place.
118+
Imagine a situation, that two users are trying to join a non-existent room at the same moment. Since they are working asynchronously, there is a probability, that both of them will
119+
get an answer from the `:global.whereis_name(room_id)` saying that the room with the given name does not exist. Both of them will then try to create such a room. The request from one of these users will come to the `:global` registry first, the room will be
120+
registered - and the second user will receive an `:already_started` error, along with the PID of that room process, since the process already exists. Handling of that error is quite straightforward - the user can safely join the room with the provided PID.
121+
Of course, some other errors might also occur, but we do not distinguish between them and we simply log the fact that there was a problem with the room creation.
122+
In case we retrieve a PID of the room process, we call the `do_join/3` support function.
123+
`do_join/3` holds some repeatable parts of code concerning the joining process.
124+
Inside that function, we start to monitor the room process (so that we will receive ```:DOWN``` message in case of the room's process crash/failure). Then we notify the room's process that
110125
it should take us (peer channel) under consideration - we send our peer_id (generated as unique id with UUID module) along with the peer channel's PID to
111126
the room process in the `:add_peer_channel` message so that the room will have a way to identify our process. The last thing we do is that we are adding information about the association between
112127
room's identifier, room's PID, and peer's identifier to the map of socket's assigns. We will refer to this information later so we need to store it somehow.
@@ -119,18 +134,18 @@ The first one is done by implementing `handle_info/2` callback as shown below:
119134

120135
@impl true
121136
def handle_info({:media_event, event}, socket) do
122-
push(socket, "mediaEvent", %{data: event})
123-
{:noreply, socket}
137+
push(socket, "mediaEvent", %{data: event})
138+
{:noreply, socket}
124139
end
125140
```
126-
The second one is done by providing following implementation of `handle_in/3`:
141+
The second one is done by providing the following implementation of `handle_in/3`:
127142
```elixir
128143
#FILE: lib/videoroom_web/peer_channel.ex
129144

130145
@impl true
131146
def handle_in("mediaEvent", %{"data" => event}, socket) do
132-
send(socket.assigns.room, {:media_event, socket.assigns.peer_id, event})
133-
{:noreply, socket}
147+
send(socket.assigns.room_pid, {:media_event, socket.assigns.peer_id, event})
148+
{:noreply, socket}
134149
end
135150
```
136151
Note the use of `push` method provided by Phoenix.Channel.
@@ -140,4 +155,4 @@ Great job! You have just implemented the server's side of our communication chan
140155
[NEXT - Server's room process](5_ImplementingServerRoom.md)<br>
141156
[PREV - System architecture](3_SystemArchitecture.md)<br>
142157
[List of contents](index.md)<br>
143-
[List of tutorials](../../index.md)
158+
[List of tutorials](../../index.md)

0 commit comments

Comments
 (0)