Chapter 4. The Need for a Signaling Channel
As we anticipated in Chapter 3, a signaling channel is needed in a WebRTC-enabled application in order to allow for the exchanging of both session descriptions and network reachability information. Up until now, we have disregarded this specific aspect by sticking to a local perspective. This turned out to be useful, since it allowed us to just focus on the details of the WebRTC APIs, while leaving aside all networking-related aspects. The time is now ripe to also tackle these last issues. In this chapter, we will describe how we can create a proper signaling channel between any pair of peers that are interested in successfully setting up a WebRTC-enabled communication session.
The material presented in this chapter is only loosely related to the main topic of the book. More precisely, we will herein just focus on the creation of the above-mentioned signaling channel by describing the design and implementation of a very simple JavaScript application involving two clients and a server. The example itself should provide the reader with a set of tools that can be easily reused in a wide set of application scenarios. In the following chapter we will finally put all pieces together in order to complete the 10-step WebRTC recipe in a distributed setting.
Building Up a Simple Call Flow
As usual, we will continue to embrace the learn-by-example approach in order to let you figure out how to build a server-assisted signaling channel between two remote peers. In this chapter, we will focus on the realization of a simple interaction scenario, as formally depicted in the sequence diagram in Figure 4-1.
The diagram in the picture involves three different actors:
- A channel initiator, such as the peer that first takes the initiative of creating a dedicated communication channel with a remote party
- A signaling server, managing channel creation and acting as a message relaying node
- A channel joiner, for instance, a remote party joining an already existing channel
The idea is that the channel is created on demand by the server after receiving a specific request issued by the initiator. As soon as the second peer joins the channel, conversation can start. Message exchanging always happens through the server, which basically acts as a transparent relay node. When one of the peers decides to quit an ongoing conversation, it issues an ad hoc message (called Bye in the figure) towards the server, before disconnecting. This message is dispatched by the server to the remote party, which also disconnects, after having sent an acknowledgment back to the server. The receipt of the acknowledgment eventually triggers the channel reset procedure on the server’s side, thus bringing the overall scenario back to its original configuration.
Let’s start by building a simple HTML5 page (see Example 4-1), containing an initially empty <div>
element which will be used to track down the evolution of the communication between two remote peers interacting through the signaling server.
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"
"http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<title>
WebRTC client</title>
</head>
<body>
<script
src=
'/socket.io/socket.io.js'
></script>
<div
id=
"scratchPad"
></div>
<script
type=
"text/javascript"
src=
"js/simpleNodeClient.js"
></script>
</body>
</html>
As you can see from the HTML code, the page includes two JavaScript files. The former (socket.io.js) refers to the well-known socket.io
library for real-time web applications.
The latter file (simpleNodeClient.js) is presented in the following:
// Get <div> placeholder element from DOM
div
=
document
.
getElementById
(
'scratchPad'
);
// Connect to server
var
socket
=
io
.
connect
(
'http://localhost:8181'
);
// Ask channel name from user
channel
=
prompt
(
"Enter signaling channel name:"
);
if
(
channel
!==
""
)
{
console
.
log
(
'Trying to create or join channel: '
,
channel
);
// Send 'create or join' to the server
socket
.
emit
(
'create or join'
,
channel
);
}
// Handle 'created' message
socket
.
on
(
'created'
,
function
(
channel
){
console
.
log
(
'channel '
+
channel
+
' has been created!'
);
console
.
log
(
'This peer is the initiator...'
);
// Dynamically modify the HTML5 page
div
.
insertAdjacentHTML
(
'beforeEnd'
,
'<p>Time: '
+
(
performance
.
now
()
/
1000
).
toFixed
(
3
)
+
' --> Channel '
+
channel
+
' has been created! </p>'
);
div
.
insertAdjacentHTML
(
'beforeEnd'
,
'<p>Time: '
+
(
performance
.
now
()
/
1000
).
toFixed
(
3
)
+
' --> This peer is the initiator...</p>'
);
});
// Handle 'full' message
socket
.
on
(
'full'
,
function
(
channel
){
console
.
log
(
'channel '
+
channel
+
' is too crowded! \
Cannot allow you to enter, sorry :-('
);
div
.
insertAdjacentHTML
(
'beforeEnd'
,
'<p>Time: '
+
(
performance
.
now
()
/
1000
).
toFixed
(
3
)
+
' --> \
channel '
+
channel
+
' is too crowded! \
Cannot allow you to enter, sorry :-( </p>'
);
});
// Handle 'remotePeerJoining' message
socket
.
on
(
'remotePeerJoining'
,
function
(
channel
){
console
.
log
(
'Request to join '
+
channel
);
console
.
log
(
'You are the initiator!'
);
div
.
insertAdjacentHTML
(
'beforeEnd'
,
'<p style="color:red">Time: '
+
(
performance
.
now
()
/
1000
).
toFixed
(
3
)
+
' --> Message from server: request to join channel '
+
channel
+
'</p>'
);
});
// Handle 'joined' message
socket
.
on
(
'joined'
,
function
(
msg
){
console
.
log
(
'Message from server: '
+
msg
);
div
.
insertAdjacentHTML
(
'beforeEnd'
,
'<p>Time: '
+
(
performance
.
now
()
/
1000
).
toFixed
(
3
)
+
' --> Message from server: </p>'
);
div
.
insertAdjacentHTML
(
'beforeEnd'
,
'<p style="color:blue">'
+
msg
+
'</p>'
);
div
.
insertAdjacentHTML
(
'beforeEnd'
,
'<p>Time: '
+
(
performance
.
now
()
/
1000
).
toFixed
(
3
)
+
' --> Message from server: </p>'
);
div
.
insertAdjacentHTML
(
'beforeEnd'
,
'<p style="color:blue">'
+
msg
+
'</p>'
);
});
// Handle 'broadcast: joined' message
socket
.
on
(
'broadcast: joined'
,
function
(
msg
){
div
.
insertAdjacentHTML
(
'beforeEnd'
,
'<p style="color:red">Time: '
+
(
performance
.
now
()
/
1000
).
toFixed
(
3
)
+
' --> Broadcast message from server: </p>'
);
div
.
insertAdjacentHTML
(
'beforeEnd'
,
'<p style="color:red">'
+
msg
+
'</p>'
);
console
.
log
(
'Broadcast message from server: '
+
msg
);
// Start chatting with remote peer:
// 1. Get user's message
var
myMessage
=
prompt
(
'Insert message to be sent to your peer:'
,
""
);
// 2. Send to remote peer (through server)
socket
.
emit
(
'message'
,
{
channel
:
channel
,
message
:
myMessage
});
});
// Handle remote logging message from server
socket
.
on
(
'log'
,
function
(
array
){
console
.
log
.
apply
(
console
,
array
);
});
// Handle 'message' message
socket
.
on
(
'message'
,
function
(
message
){
console
.
log
(
'Got message from other peer: '
+
message
);
div
.
insertAdjacentHTML
(
'beforeEnd'
,
'<p>Time: '
+
(
performance
.
now
()
/
1000
).
toFixed
(
3
)
+
' --> Got message from other peer: </p>'
);
div
.
insertAdjacentHTML
(
'beforeEnd'
,
'<p style="color:blue">'
+
message
+
'</p>'
);
// Send back response message:
// 1. Get response from user
var
myResponse
=
prompt
(
'Send response to other peer:'
,
""
);
// 2. Send it to remote peer (through server)
socket
.
emit
(
'response'
,
{
channel
:
channel
,
message
:
myResponse
});
});
// Handle 'response' message
socket
.
on
(
'response'
,
function
(
response
){
console
.
log
(
'Got response from other peer: '
+
response
);
div
.
insertAdjacentHTML
(
'beforeEnd'
,
'<p>Time: '
+
(
performance
.
now
()
/
1000
).
toFixed
(
3
)
+
' --> Got response from other peer: </p>'
);
div
.
insertAdjacentHTML
(
'beforeEnd'
,
'<p style="color:blue">'
+
response
+
'</p>'
);
// Keep on chatting
var
chatMessage
=
prompt
(
'Keep on chatting. \
Write "Bye" to quit conversation'
,
""
);
// User wants to quit conversation: send 'Bye' to remote party
if
(
chatMessage
==
"Bye"
){
div
.
insertAdjacentHTML
(
'beforeEnd'
,
'<p>Time: '
+
(
performance
.
now
()
/
1000
).
toFixed
(
3
)
+
' --> Sending "Bye" to server...</p>'
);
console
.
log
(
'Sending "Bye" to server'
);
socket
.
emit
(
'Bye'
,
channel
);
div
.
insertAdjacentHTML
(
'beforeEnd'
,
'<p>Time: '
+
(
performance
.
now
()
/
1000
).
toFixed
(
3
)
+
' --> Going to disconnect...</p>'
);
console
.
log
(
'Going to disconnect...'
);
// Disconnect from server
socket
.
disconnect
();
}
else
{
// Keep on going: send response back
// to remote party (through server)
socket
.
emit
(
'response'
,
{
channel
:
channel
,
message
:
chatMessage
});
}
});
// Handle 'Bye' message
socket
.
on
(
'Bye'
,
function
(){
console
.
log
(
'Got "Bye" from other peer! Going to disconnect...'
);
div
.
insertAdjacentHTML
(
'beforeEnd'
,
'<p>Time: '
+
(
performance
.
now
()
/
1000
).
toFixed
(
3
)
+
' --> Got "Bye" from other peer!</p>'
);
div
.
insertAdjacentHTML
(
'beforeEnd'
,
'<p>Time: '
+
(
performance
.
now
()
/
1000
).
toFixed
(
3
)
+
' --> Sending "Ack" to server</p>'
);
// Send 'Ack' back to remote party (through server)
console
.
log
(
'Sending "Ack" to server'
);
socket
.
emit
(
'Ack'
);
// Disconnect from server
div
.
insertAdjacentHTML
(
'beforeEnd'
,
'<p>Time: '
+
(
performance
.
now
()
/
1000
).
toFixed
(
3
)
+
' --> Going to disconnect...</p>'
);
console
.
log
(
'Going to disconnect...'
);
socket
.
disconnect
();
});
The code performs the following actions:
-
Allows the client to connect to the server (through the
socket.io
library) - Prompts the user for the name of the channel she wants to join
-
Sends a
create
orjoin
request to the server - Starts to asynchronously handle server-sent events.
In the remainder of this chapter, we will follow a complete call flow in a step-by-step fashion. Before doing this, though, we will take a look at the server-side behavior. The server has been written by leveraging the Node.js JavaScript library.
Let’s go over the server-side code. It basically looks after the creation of a server instance listening on port 8181. The code allows for the creation of server-side “rooms” hosting two client sockets at most. The first client that asks for the creation of a room is the channel initiator.
After channel creation, the server-side policy is the following:
- The second client arriving is allowed to join the newly created channel.
All other clients are denied access to the room (and are consequently notified of such an event).
var
static
=
require
(
'node-static'
);
var
http
=
require
(
'http'
);
// Create a node-static server instance listening on port 8181
var
file
=
new
(
static
.
Server
)();
// We use the http module’s createServer function and
// use our instance of node-static to serve the files
var
app
=
http
.
createServer
(
function
(
req
,
res
)
{
file
.
serve
(
req
,
res
);
}).
listen
(
8181
);
// Use socket.io JavaScript library for real-time web applications
var
io
=
require
(
'socket.io'
).
listen
(
app
);
// Let's start managing connections...
io
.
sockets
.
on
(
'connection'
,
function
(
socket
){
// Handle 'message' messages
socket
.
on
(
'message'
,
function
(
message
)
{
log
(
'S --> Got message: '
,
message
);
socket
.
broadcast
.
to
(
message
.
channel
).
emit
(
'message'
,
\
message
.
message
);
});
// Handle 'create or join' messages
socket
.
on
(
'create or join'
,
function
(
channel
)
{
var
numClients
=
io
.
sockets
.
clients
(
channel
).
length
;
console
.
log
(
'numclients = '
+
numClients
);
// First client joining...
if
(
numClients
==
0
){
socket
.
join
(
channel
);
socket
.
emit
(
'created'
,
channel
);
// Second client joining...
}
else
if
(
numClients
==
1
)
{
// Inform initiator...
io
.
sockets
.
in
(
channel
).
emit
(
'remotePeerJoining'
,
channel
);
// Let the new peer join channel
socket
.
join
(
channel
);
socket
.
broadcast
.
to
(
channel
).
emit
(
'broadcast: joined'
,
'S --> \
broadcast(): client '
+
socket
.
id
+
' joined channel '
\
+
channel
);
}
else
{
// max two clients
console
.
log
(
"Channel full!"
);
socket
.
emit
(
'full'
,
channel
);
}
});
// Handle 'response' messages
socket
.
on
(
'response'
,
function
(
response
)
{
log
(
'S --> Got response: '
,
response
);
// Just forward message to the other peer
socket
.
broadcast
.
to
(
response
.
channel
).
emit
(
'response'
,
response
.
message
);
});
// Handle 'Bye' messages
socket
.
on
(
'Bye'
,
function
(
channel
){
// Notify other peer
socket
.
broadcast
.
to
(
channel
).
emit
(
'Bye'
);
// Close socket from server's side
socket
.
disconnect
();
});
// Handle 'Ack' messages
socket
.
on
(
'Ack'
,
function
()
{
console
.
log
(
'Got an Ack!'
);
// Close socket from server's side
socket
.
disconnect
();
});
// Utility function used for remote logging
function
log
(){
var
array
=
[
">>> "
];
for
(
var
i
=
0
;
i
<
arguments
.
length
;
i
++
)
{
array
.
push
(
arguments
[
i
]);
}
socket
.
emit
(
'log'
,
array
);
}
});
We’re now ready to get started with our signaling example walk-through.
Creating the Signaling Channel
We herein focus on the very first steps of the example call flow, as illustrated in Figure 4-2.
Let’s assume that a first client using the Chrome browser loads the HTML5 page of Example 4-1. The page first connects to the server and then prompts the user for the name of the channel (Figure 4-3):
...
// Connect to server
var
socket
=
io
.
connect
(
'http://localhost:8181'
);
// Ask channel name from user
channel
=
prompt
(
"Enter signaling channel name:"
);
...
Once the user fills in the channel name field and hits the OK button, the JavaScript code in the page sends a create
or join
message to the server:
...
if
(
channel
!==
""
)
{
console
.
log
(
'Trying to create or join channel: '
,
channel
);
// Send 'create or join' to the server
socket
.
emit
(
'create or join'
,
channel
);
}
...
Upon reception of the client’s request, the server performs the following actions:
- Verifies that the mentioned channel is a brand new one (i.e., there are no clients in it)
- Associates a server-side room with the channel
- Allows the requesting client to join the channel
-
Sends back to the client a notification message called
created
The following snippet shows this sequence of actions:
...
socket
.
on
(
'create or join'
,
function
(
channel
)
{
var
numClients
=
io
.
sockets
.
clients
(
channel
).
length
;
console
.
log
(
'numclients = '
+
numClients
);
// First client joining...
if
(
numClients
==
0
){
socket
.
join
(
channel
);
socket
.
emit
(
'created'
,
channel
);
...
Figure 4-4 shows the server’s console right after the aforementioned actions have been performed.
When the initiating client receives the server’s answer, it simply logs the event both on the JavaScript console and inside the <div>
element contained in the HTML5 page:
...
// Handle 'created' message
socket
.
on
(
'created'
,
function
(
channel
){
console
.
log
(
'channel '
+
channel
+
' has been created!'
);
console
.
log
(
'This peer is the initiator...'
);
// Dynamically modify the HTML5 page
div
.
insertAdjacentHTML
(
'beforeEnd'
,
'<p>Time: '
+
(
performance
.
now
()
/
1000
).
toFixed
(
3
)
+
' --> Channel '
+
channel
+
' has been created! </p>'
);
div
.
insertAdjacentHTML
(
'beforeEnd'
,
'<p>Time: '
+
(
performance
.
now
()
/
1000
).
toFixed
(
3
)
+
' --> This peer is the initiator...</p>'
);
});
...
The situation described above is illustrated in Figure 4-5.
Joining the Signaling Channel
Let’s now move on to the second client, the channel joiner, focusing on the call flow section shown in Figure 4-6.
For the sake of completeness, we will this time use Firefox as the client browser, the look and feel of which, right after loading the application page, is illustrated in Figure 4-7.
As already described, the client first connects to the server and then sends it a create
or join
request. Since this time the requesting peer is not the initiator, the server’s behavior will be driven by the following code snippet:
...
}
else
if
(
numClients
==
1
)
{
// Inform initiator...
io
.
sockets
.
in
(
channel
).
emit
(
'remotePeerJoining'
,
channel
);
// Let the new peer join channel
socket
.
join
(
channel
);
socket
.
broadcast
.
to
(
channel
).
emit
(
'broadcast: joined'
,
'S -->
broadcast(): client '
+
socket
.
id
+
' joined channel '
+
channel
);
...
Basically, the server will:
-
Notify the channel initiator of the arrival of a new
join
request. - Allow the new client to enter the already existing room.
-
Update (through a
broadcast
message) the channel initiator about the successful completion of thejoin
operation, allowing it to prepare to start a new conversation.
Such a sequence of actions is reported in Figure 4-8, which shows the server’s console at this stage of the call flow.
Figures 4-9 and 4-10 show, respectively, the joiner’s and initiator’s windows right after the former has successfully joined the signaling channel created by the latter. As the reader will recognize, this sequence of server-side actions is reported in red in the initiator’s HTML5 page in Figure 4-10, which now prompts the user for the very first message to be exchanged across the server-mediated communication path.
Starting a Server-Mediated Conversation
We have now arrived at the call flow stage reported in Figure 4-11, which basically captures the core of the application. In this phase, in fact, the initiator sends a first message to the joiner, who is first notified of this event and then prompted for the introduction of a proper answer.
As usual, the client retrieves the user’s input and emits a message towards the server in order for it to be properly dispatched. On the server’s side, the received message is simply broadcast[2] on the channel:
...
// Handle 'message' messages
socket
.
on
(
'message'
,
function
(
message
)
{
log
(
'S --> Got message: '
,
message
);
socket
.
broadcast
.
to
(
message
.
channel
).
emit
(
'message'
,
message
.
message
);
});
...
The above described server’s behavior is illustrated in the console snapshot of Figure 4-12.
Figure 4-13 shows the remote peer (the joiner) that has just received the message relayed by the server.
As evidenced by the figure, the following actions are performed:
- Logging the received message both on the JavaScript console and on the HTML5 page
- Prompting the receiver for proper input
- Sending the receiver’s answer back to the sender (across the signaling channel)
Such a sequence is driven by the following code snippet:
...
// Handle 'message' message
socket
.
on
(
'message'
,
function
(
message
){
console
.
log
(
'Got message from other peer: '
+
message
);
div
.
insertAdjacentHTML
(
'beforeEnd'
,
'<p>Time: '
+
(
performance
.
now
()
/
1000
).
toFixed
(
3
)
+
' --> Got message from other peer: </p>'
);
div
.
insertAdjacentHTML
(
'beforeEnd'
,
'<p style="color:blue">'
+
message
+
'</p>'
);
// Send back response message:
// 1. Get response from user
var
myResponse
=
prompt
(
'Send response to other peer:'
,
""
);
// 2. Send it to remote peer (through server)
socket
.
emit
(
'response'
,
{
channel
:
channel
,
message
:
myResponse
});
});
...
As soon as the receiver hits the OK button on the prompt window in Figure 4-14, the response message is emitted towards the server, which forwards it to the remote party:
...
// Handle 'response' messages
socket
.
on
(
'response'
,
function
(
response
)
{
log
(
'S --> Got response: '
,
response
);
// Just forward message to the other peer
socket
.
broadcast
.
to
(
response
.
channel
).
emit
(
'response'
,
response
.
message
);
});
...
This behavior is once again illustrated by the server’s console snapshot in Figure 4-14.
Continuing to Chat Across the Channel
We are now in the steady-state portion of the application (Figure 4-15), where the two peers simply take turns in asking the server to relay messages towards the other party.
Message exchanging is achieved on the client’s side through the following code:
...
// Handle 'response' message
socket
.
on
(
'response'
,
function
(
response
){
console
.
log
(
'Got response from other peer: '
+
response
);
div
.
insertAdjacentHTML
(
'beforeEnd'
,
'<p>Time: '
+
(
performance
.
now
()
/
1000
).
toFixed
(
3
)
+
' --> Got response from other peer: </p>'
);
div
.
insertAdjacentHTML
(
'beforeEnd'
,
'<p style="color:blue">'
+
response
+
'</p>'
);
// Keep on chatting
var
chatMessage
=
prompt
(
'Keep on chatting. Write \
"Bye" to quit conversation'
,
""
);
...
...
// Keep on going: send response back to remote party (through server)
socket
.
emit
(
'response'
,
{
channel
:
channel
,
message
:
chatMessage
});
}
});
Basically, upon reception of a new message, each peer performs the usual logging operations and then prompts the user for new input. As long as the inserted text has a value other than Bye, it sends a new message to the remote party. Figure 4-16 shows the initiator’s window right before a new message is emitted across the channel.
Figure 4-17 in turn shows the server’s console upon reception of such a message, which is, as usual, broadcast to the remote party.
Finally, Figure 4-18 shows reception of the relayed message on the receiver’s side.
Closing the Signaling Channel
We are now ready to analyze channel teardown, as described in the call flow snippet in Figure 4-19.
The teardown procedure is actually triggered by the insertion of a Bye message in one of the two browsers (see Figure 4-20).
What happens behind the scenes is the following:
...
// User wants to quit conversation: send 'Bye' to remote party
if
(
chatMessage
==
"Bye"
){
div
.
insertAdjacentHTML
(
'beforeEnd'
,
'<p>Time: '
+
(
performance
.
now
()
/
1000
).
toFixed
(
3
)
+
' --> Sending "Bye" to server...</p>'
);
console
.
log
(
'Sending "Bye" to server'
);
socket
.
emit
(
'Bye'
,
channel
);
div
.
insertAdjacentHTML
(
'beforeEnd'
,
'<p>Time: '
+
(
performance
.
now
()
/
1000
).
toFixed
(
3
)
+
' --> Going to disconnect...</p>'
);
console
.
log
(
'Going to disconnect...'
);
// Disconnect from server
socket
.
disconnect
();
}
...
As we can see in the code, the disconnecting client first sends a Bye message across the channel and immediately thereafter closes the web socket (Figure 4-21).
As soon as the server gets the Bye message, it first relays it to the remote party and then closes the communication channel towards the disconnecting client:
...
// Handle 'Bye' messages
socket
.
on
(
'Bye'
,
function
(
channel
){
// Notify other peer
socket
.
broadcast
.
to
(
channel
).
emit
(
'Bye'
);
// Close socket from server's side
socket
.
disconnect
();
});
...
Let’s finally analyze the behavior of the peer receiving the Bye message from the remote party. The peer first logs information about the received message (both on the JavaScript console and inside the HTML5 page):
...
// Handle 'Bye' message
socket
.
on
(
'Bye'
,
function
(){
console
.
log
(
'Got "Bye" from other peer! Going to disconnect...'
);
div
.
insertAdjacentHTML
(
'beforeEnd'
,
'<p>Time: '
+
(
performance
.
now
()
/
1000
).
toFixed
(
3
)
+
' --> Got "Bye" from other peer!</p>'
);
...
Then, an Ack message is sent back to the server to confirm reception of the disconnection request:
...
div
.
insertAdjacentHTML
(
'beforeEnd'
,
'<p>Time: '
+
(
performance
.
now
()
/
1000
).
toFixed
(
3
)
+
' --> Sending "Ack" to server</p>'
);
// Send 'Ack' back to remote party (through server)
console
.
log
(
'Sending "Ack" to server'
);
socket
.
emit
(
'Ack'
);
// Disconnect from server
div
.
insertAdjacentHTML
(
'beforeEnd'
,
'<p>Time: '
+
(
performance
.
now
()
/
1000
).
toFixed
(
3
)
+
' --> Going to disconnect...</p>'
);
console
.
log
(
'Going to disconnect...'
);
socket
.
disconnect
();
...
Finally, the receiving peer tears down its own connection to the server:
...
console
.
log
(
'Going to disconnect...'
);
socket
.
disconnect
();
});
The above sequence of actions can be easily identified in the snapshot in Figure 4-22.
The final actions are undertaken on the server’s side. Reception of the Ack message is logged on the console (see Figure 4-23) and the channel is eventually torn down:
// Handle 'Ack' messages
socket
.
on
(
'Ack'
,
function
()
{
console
.
log
(
'Got an Ack!'
);
// Close socket from server's side
socket
.
disconnect
();
});
[2] Note that broadcasting on a channel made of just two peers is equivalent to sending a notification to the peer who was not the sender of the message itself.
Get Real-Time Communication with WebRTC now with the O’Reilly learning platform.
O’Reilly members experience books, live events, courses curated by job role, and more from O’Reilly and nearly 200 top publishers.