WebRTC: demostración de texto
En este capítulo, vamos a crear una aplicación cliente que permita a dos usuarios en dispositivos separados enviarse mensajes entre sí usando WebRTC. Nuestra aplicación tendrá dos páginas. Uno para iniciar sesión y otro para enviar mensajes a otro usuario.
Las dos páginas serán las etiquetas div . La mayor parte de la entrada se realiza a través de controladores de eventos simples.
Servidor de señalización
Para crear una conexión WebRTC, los clientes deben poder transferir mensajes sin utilizar una conexión de pares WebRTC. Aquí es donde usaremos HTML5 WebSockets, una conexión de socket bidireccional entre dos puntos finales, un servidor web y un navegador web. Ahora comencemos a usar la biblioteca WebSocket. Cree el archivo server.js e inserte el siguiente código:
//require our websocket library
var WebSocketServer = require('ws').Server;
//creating a websocket server at port 9090
var wss = new WebSocketServer({port: 9090});
//when a user connects to our sever
wss.on('connection', function(connection) {
console.log("user connected");
//when server gets a message from a connected user
connection.on('message', function(message) {
console.log("Got message from a user:", message);
});
connection.send("Hello from server");
});
La primera línea requiere la biblioteca WebSocket que ya hemos instalado. Luego creamos un servidor de socket en el puerto 9090. Luego, escuchamos el evento de conexión . Este código se ejecutará cuando un usuario establezca una conexión WebSocket al servidor. Luego escuchamos los mensajes enviados por el usuario. Finalmente, enviamos una respuesta al usuario conectado diciendo "Hola desde el servidor".
En nuestro servidor de señalización, usaremos un nombre de usuario basado en cadenas para cada conexión para que sepamos dónde enviar mensajes. Cambiemos un poco nuestro controlador de conexión :
connection.on('message', function(message) {
var data;
//accepting only JSON messages
try {
data = JSON.parse(message);
} catch (e) {
console.log("Invalid JSON");
data = {};
}
});
De esta forma solo aceptamos mensajes JSON. A continuación, debemos almacenar a todos los usuarios conectados en algún lugar. Usaremos un objeto Javascript simple para ello. Cambie la parte superior de nuestro archivo -
//require our websocket library
var WebSocketServer = require('ws').Server;
//creating a websocket server at port 9090
var wss = new WebSocketServer({port: 9090});
//all connected to the server users
var users = {};
Vamos a agregar un campo de tipo para cada mensaje proveniente del cliente. Por ejemplo, si un usuario desea iniciar sesión, envía el mensaje de tipo de inicio de sesión . Vamos a definirlo -
connection.on('message', function(message) {
var data;
//accepting only JSON messages
try {
data = JSON.parse(message);
} catch (e) {
console.log("Invalid JSON");
data = {};
}
//switching type of the user message
switch (data.type) {
//when a user tries to login
case "login":
console.log("User logged:", data.name);
//if anyone is logged in with this username then refuse
if(users[data.name]) {
sendTo(connection, {
type: "login",
success: false
});
} else {
//save user connection on the server
users[data.name] = connection;
connection.name = data.name;
sendTo(connection, {
type: "login",
success: true
});
}
break;
default:
sendTo(connection, {
type: "error",
message: "Command no found: " + data.type
});
break;
}
});
Si el usuario envía un mensaje con el tipo de inicio de sesión , nosotros:
- Compruebe si alguien ya ha iniciado sesión con este nombre de usuario.
- Si es así, dígale al usuario que no ha iniciado sesión correctamente.
- Si nadie está usando este nombre de usuario, agregamos el nombre de usuario como clave para el objeto de conexión.
- Si no se reconoce un comando, enviamos un error.
El siguiente código es una función auxiliar para enviar mensajes a una conexión. Añadirlo a la server.js archivo -
function sendTo(connection, message) {
connection.send(JSON.stringify(message));
}
Cuando el usuario se desconecta debemos limpiar su conexión. Podemos eliminar al usuario cuando se dispara el evento de cierre . Agregue el siguiente código al controlador de conexión :
connection.on("close", function() {
if(connection.name) {
delete users[connection.name];
}
});
Después de iniciar sesión correctamente, el usuario desea llamar a otro. Debería hacer una oferta a otro usuario para lograrlo. Agregar el controlador de ofertas :
case "offer":
//for ex. UserA wants to call UserB
console.log("Sending offer to: ", data.name);
//if UserB exists then send him offer details
var conn = users[data.name];
if(conn != null){
//setting that UserA connected with UserB
connection.otherName = data.name;
sendTo(conn, {
type: "offer",
offer: data.offer,
name: connection.name
});
break;
En primer lugar, obtenemos la conexión del usuario al que intentamos llamar. Si existe le enviamos los detalles de la oferta . También agregamos otherName al objeto de conexión . Esto se hace por la simplicidad de encontrarlo más tarde.
Responder a la respuesta tiene un patrón similar al que usamos en el controlador de ofertas . Nuestro servidor simplemente pasa todos los mensajes como respuesta a otro usuario. Agregue el siguiente código después del controlador de ofertas :
case "answer":
console.log("Sending answer to: ", data.name);
//for ex. UserB answers UserA
var conn = users[data.name];
if(conn != null) {
connection.otherName = data.name;
sendTo(conn, {
type: "answer",
answer: data.answer
});
}
break;
La parte final es el manejo del candidato ICE entre usuarios. Usamos la misma técnica simplemente pasando mensajes entre usuarios. La principal diferencia es que los mensajes candidatos pueden aparecer varias veces por usuario en cualquier orden. Agregar el controlador de candidatos -
case "candidate":
console.log("Sending candidate to:",data.name);
var conn = users[data.name];
if(conn != null) {
sendTo(conn, {
type: "candidate",
candidate: data.candidate
});
}
break;
Para permitir que nuestros usuarios se desconecten de otro usuario, debemos implementar la función de colgar. También le indicará al servidor que elimine todas las referencias de usuario. Agregue el controlador de licencia -
case "leave":
console.log("Disconnecting from", data.name);
var conn = users[data.name];
conn.otherName = null;
//notify the other user so he can disconnect his peer connection
if(conn != null) {
sendTo(conn, {
type: "leave"
});
}
break;
Esto también enviará al otro usuario el evento de permiso para que pueda desconectar su conexión de igual en consecuencia. También deberíamos manejar el caso cuando un usuario pierde su conexión desde el servidor de señalización. Modifiquemos nuestro controlador cercano -
connection.on("close", function() {
if(connection.name) {
delete users[connection.name];
if(connection.otherName) {
console.log("Disconnecting from ", connection.otherName);
var conn = users[connection.otherName];
conn.otherName = null;
if(conn != null) {
sendTo(conn, {
type: "leave"
});
}
}
}
});
El siguiente es el código completo de nuestro servidor de señalización:
//require our websocket library
var WebSocketServer = require('ws').Server;
//creating a websocket server at port 9090
var wss = new WebSocketServer({port: 9090});
//all connected to the server users
var users = {};
//when a user connects to our sever
wss.on('connection', function(connection) {
console.log("User connected");
//when server gets a message from a connected user
connection.on('message', function(message) {
var data;
//accepting only JSON messages
try {
data = JSON.parse(message);
} catch (e) {
console.log("Invalid JSON");
data = {};
}
//switching type of the user message
switch (data.type) {
//when a user tries to login
case "login":
console.log("User logged", data.name);
//if anyone is logged in with this username then refuse
if(users[data.name]) {
sendTo(connection, {
type: "login",
success: false
});
} else {
//save user connection on the server
users[data.name] = connection;
connection.name = data.name;
sendTo(connection, {
type: "login",
success: true
});
}
break;
case "offer":
//for ex. UserA wants to call UserB
console.log("Sending offer to: ", data.name);
//if UserB exists then send him offer details
var conn = users[data.name];
if(conn != null) {
//setting that UserA connected with UserB
connection.otherName = data.name;
sendTo(conn, {
type: "offer",
offer: data.offer,
name: connection.name
});
}
break;
case "answer":
console.log("Sending answer to: ", data.name);
//for ex. UserB answers UserA
var conn = users[data.name];
if(conn != null) {
connection.otherName = data.name;
sendTo(conn, {
type: "answer",
answer: data.answer
});
}
break;
case "candidate":
console.log("Sending candidate to:",data.name);
var conn = users[data.name];
if(conn != null) {
sendTo(conn, {
type: "candidate",
candidate: data.candidate
});
}
break;
case "leave":
console.log("Disconnecting from", data.name);
var conn = users[data.name];
conn.otherName = null;
//notify the other user so he can disconnect his peer connection
if(conn != null) {
sendTo(conn, {
type: "leave"
});
}
break;
default:
sendTo(connection, {
type: "error",
message: "Command not found: " + data.type
});
break;
}
});
//when user exits, for example closes a browser window
//this may help if we are still in "offer","answer" or "candidate" state
connection.on("close", function() {
if(connection.name) {
delete users[connection.name];
if(connection.otherName) {
console.log("Disconnecting from ", connection.otherName);
var conn = users[connection.otherName];
conn.otherName = null;
if(conn != null) {
sendTo(conn, {
type: "leave"
});
}
}
}
});
connection.send("Hello world");
});
function sendTo(connection, message) {
connection.send(JSON.stringify(message));
}
Solicitud de cliente
Una forma de probar esta aplicación es abrir dos pestañas del navegador e intentar enviarse un mensaje entre sí.
En primer lugar, necesitamos instalar la biblioteca de arranque . Bootstrap es un marco frontend para desarrollar aplicaciones web. Puedes aprender más enhttp://getbootstrap.com/.Cree una carpeta llamada, por ejemplo, "chat de texto". Esta será nuestra carpeta raíz de la aplicación. Dentro de esta carpeta, cree un archivo package.json (es necesario para administrar las dependencias de npm) y agregue lo siguiente:
{
"name": "webrtc-textochat",
"version": "0.1.0",
"description": "webrtc-textchat",
"author": "Author",
"license": "BSD-2-Clause"
}
Luego ejecute npm install bootstrap . Esto instalará la biblioteca bootstrap en la carpeta textchat / node_modules .
Ahora necesitamos crear una página HTML básica. Cree un archivo index.html en la carpeta raíz con el siguiente código:
<html>
<head>
<title>WebRTC Text Demo</title>
<link rel = "stylesheet" href = "node_modules/bootstrap/dist/css/bootstrap.min.css"/>
</head>
<style>
body {
background: #eee;
padding: 5% 0;
}
</style>
<body>
<div id = "loginPage" class = "container text-center">
<div class = "row">
<div class = "col-md-4 col-md-offset-4">
<h2>WebRTC Text Demo. Please sign in</h2>
<label for = "usernameInput" class = "sr-only">Login</label>
<input type = "email" id = "usernameInput"
class = "form-control formgroup" placeholder = "Login"
required = "" autofocus = "">
<button id = "loginBtn" class = "btn btn-lg btn-primary btnblock">
Sign in</button>
</div>
</div>
</div>
<div id = "callPage" class = "call-page container">
<div class = "row">
<div class = "col-md-4 col-md-offset-4 text-center">
<div class = "panel panel-primary">
<div class = "panel-heading">Text chat</div>
<div id = "chatarea" class = "panel-body text-left"></div>
</div>
</div>
</div>
<div class = "row text-center form-group">
<div class = "col-md-12">
<input id = "callToUsernameInput" type = "text"
placeholder = "username to call" />
<button id = "callBtn" class = "btn-success btn">Call</button>
<button id = "hangUpBtn" class = "btn-danger btn">Hang Up</button>
</div>
</div>
<div class = "row text-center">
<div class = "col-md-12">
<input id = "msgInput" type = "text" placeholder = "message" />
<button id = "sendMsgBtn" class = "btn-success btn">Send</button>
</div>
</div>
</div>
<script src = "client.js"></script>
</body>
</html>
Esta página debería resultarle familiar. Hemos agregado el archivo bootstrap css. También hemos definido dos páginas. Finalmente, hemos creado varios campos de texto y botones para obtener información del usuario. En la página de "chat" debería ver la etiqueta div con el ID de "chatarea" donde se mostrarán todos nuestros mensajes. Observe que hemos agregado un enlace a un archivo client.js .
Ahora necesitamos establecer una conexión con nuestro servidor de señalización. Cree el archivo client.js en la carpeta raíz con el siguiente código:
//our username
var name;
var connectedUser;
//connecting to our signaling server
var conn = new WebSocket('ws://localhost:9090');
conn.onopen = function () {
console.log("Connected to the signaling server");
};
//when we got a message from a signaling server
conn.onmessage = function (msg) {
console.log("Got message", msg.data);
var data = JSON.parse(msg.data);
switch(data.type) {
case "login":
handleLogin(data.success);
break;
//when somebody wants to call us
case "offer":
handleOffer(data.offer, data.name);
break;
case "answer":
handleAnswer(data.answer);
break;
//when a remote peer sends an ice candidate to us
case "candidate":
handleCandidate(data.candidate);
break;
case "leave":
handleLeave();
break;
default:
break;
}
};
conn.onerror = function (err) {
console.log("Got error", err);
};
//alias for sending JSON encoded messages
function send(message) {
//attach the other peer username to our messages
if (connectedUser) {
message.name = connectedUser;
}
conn.send(JSON.stringify(message));
};
Ahora ejecute nuestro servidor de señalización a través del servidor de nodo . Luego, dentro de la carpeta raíz, ejecute el comando estático y abra la página dentro del navegador. Debería ver la siguiente salida de la consola:
El siguiente paso es implementar un inicio de sesión de usuario con un nombre de usuario único. Simplemente enviamos un nombre de usuario al servidor, que luego nos dice si está tomado o no. Agregue el siguiente código a su archivo client.js :
//******
//UI selectors block
//******
var loginPage = document.querySelector('#loginPage');
var usernameInput = document.querySelector('#usernameInput');
var loginBtn = document.querySelector('#loginBtn');
var callPage = document.querySelector('#callPage');
var callToUsernameInput = document.querySelector('#callToUsernameInput');
var callBtn = document.querySelector('#callBtn');
var hangUpBtn = document.querySelector('#hangUpBtn');
callPage.style.display = "none";
// Login when the user clicks the button
loginBtn.addEventListener("click", function (event) {
name = usernameInput.value;
if (name.length > 0) {
send({
type: "login",
name: name
});
}
});
function handleLogin(success) {
if (success === false) {
alert("Ooops...try a different username");
} else {
loginPage.style.display = "none";
callPage.style.display = "block";
//**********************
//Starting a peer connection
//**********************
}
};
En primer lugar, seleccionamos algunas referencias a los elementos de la página. Ocultamos la página de llamadas. Luego, agregamos un detector de eventos en el botón de inicio de sesión. Cuando el usuario hace clic en él, enviamos su nombre de usuario al servidor. Finalmente, implementamos la devolución de llamada handleLogin. Si el inicio de sesión fue exitoso, mostramos la página de llamada, configuramos una conexión entre pares y creamos un canal de datos.
Para iniciar una conexión entre pares con un canal de datos, necesitamos:
- Cree el objeto RTCPeerConnection
- Cree un canal de datos dentro de nuestro objeto RTCPeerConnection
Agregue el siguiente código al "bloque de selectores de UI" -
var msgInput = document.querySelector('#msgInput');
var sendMsgBtn = document.querySelector('#sendMsgBtn');
var chatArea = document.querySelector('#chatarea');
var yourConn;
var dataChannel;
Modificar la función handleLogin -
function handleLogin(success) {
if (success === false) {
alert("Ooops...try a different username");
} else {
loginPage.style.display = "none";
callPage.style.display = "block";
//**********************
//Starting a peer connection
//**********************
//using Google public stun server
var configuration = {
"iceServers": [{ "url": "stun:stun2.1.google.com:19302" }]
};
yourConn = new webkitRTCPeerConnection(configuration, {optional: [{RtpDataChannels: true}]});
// Setup ice handling
yourConn.onicecandidate = function (event) {
if (event.candidate) {
send({
type: "candidate",
candidate: event.candidate
});
}
};
//creating data channel
dataChannel = yourConn.createDataChannel("channel1", {reliable:true});
dataChannel.onerror = function (error) {
console.log("Ooops...error:", error);
};
//when we receive a message from the other peer, display it on the screen
dataChannel.onmessage = function (event) {
chatArea.innerHTML += connectedUser + ": " + event.data + "<br />";
};
dataChannel.onclose = function () {
console.log("data channel is closed");
};
}
};
Si el inicio de sesión fue exitoso, la aplicación crea el objeto RTCPeerConnection y configura un controlador de candidatos de hielo que envía todos los candidatos de hielo encontrados al otro par. También crea un dataChannel. Tenga en cuenta que al crear el objeto RTCPeerConnection, el segundo argumento en el constructor es opcional: [{RtpDataChannels: true}] es obligatorio si está utilizando Chrome u Opera. El siguiente paso es crear una oferta para el otro compañero. Una vez que un usuario recibe la oferta, crea una respuesta y comienza a intercambiar candidatos de ICE. Agregue el siguiente código al archivo client.js :
//initiating a call
callBtn.addEventListener("click", function () {
var callToUsername = callToUsernameInput.value;
if (callToUsername.length > 0) {
connectedUser = callToUsername;
// create an offer
yourConn.createOffer(function (offer) {
send({
type: "offer",
offer: offer
});
yourConn.setLocalDescription(offer);
}, function (error) {
alert("Error when creating an offer");
});
}
});
//when somebody sends us an offer
function handleOffer(offer, name) {
connectedUser = name;
yourConn.setRemoteDescription(new RTCSessionDescription(offer));
//create an answer to an offer
yourConn.createAnswer(function (answer) {
yourConn.setLocalDescription(answer);
send({
type: "answer",
answer: answer
});
}, function (error) {
alert("Error when creating an answer");
});
};
//when we got an answer from a remote user
function handleAnswer(answer) {
yourConn.setRemoteDescription(new RTCSessionDescription(answer));
};
//when we got an ice candidate from a remote user
function handleCandidate(candidate) {
yourConn.addIceCandidate(new RTCIceCandidate(candidate));
};
Agregamos un controlador de clic al botón Llamar, que inicia una oferta. Luego implementamos varios manejadores esperados por el manejador onmessage . Se procesarán de forma asincrónica hasta que ambos usuarios se hayan conectado.
El siguiente paso es implementar la función de colgar. Esto dejará de transmitir datos y le dirá al otro usuario que cierre el canal de datos. Agrega el siguiente código -
//hang up
hangUpBtn.addEventListener("click", function () {
send({
type: "leave"
});
handleLeave();
});
function handleLeave() {
connectedUser = null;
yourConn.close();
yourConn.onicecandidate = null;
};
Cuando el usuario hace clic en el botón Colgar:
- Enviará un mensaje de "dejar" al otro usuario.
- Cerrará RTCPeerConnection y también el canal de datos.
El último paso es enviar un mensaje a otro par. Agregue el controlador de "clic" al botón "enviar mensaje" -
//when user clicks the "send message" button
sendMsgBtn.addEventListener("click", function (event) {
var val = msgInput.value;
chatArea.innerHTML += name + ": " + val + "<br />";
//sending a message to a connected peer
dataChannel.send(val);
msgInput.value = "";
});
Ahora ejecuta el código. Debería poder iniciar sesión en el servidor utilizando dos pestañas del navegador. A continuación, puede configurar una conexión entre pares con el otro usuario y enviarle un mensaje, así como cerrar el canal de datos haciendo clic en el botón "Colgar".
El siguiente es el archivo client.js completo :
//our username
var name;
var connectedUser;
//connecting to our signaling server
var conn = new WebSocket('ws://localhost:9090');
conn.onopen = function () {
console.log("Connected to the signaling server");
};
//when we got a message from a signaling server
conn.onmessage = function (msg) {
console.log("Got message", msg.data);
var data = JSON.parse(msg.data);
switch(data.type) {
case "login":
handleLogin(data.success);
break;
//when somebody wants to call us
case "offer":
handleOffer(data.offer, data.name);
break;
case "answer":
handleAnswer(data.answer);
break;
//when a remote peer sends an ice candidate to us
case "candidate":
handleCandidate(data.candidate);
break;
case "leave":
handleLeave();
break;
default:
break;
}
};
conn.onerror = function (err) {
console.log("Got error", err);
};
//alias for sending JSON encoded messages
function send(message) {
//attach the other peer username to our messages
if (connectedUser) {
message.name = connectedUser;
}
conn.send(JSON.stringify(message));
};
//******
//UI selectors block
//******
var loginPage = document.querySelector('#loginPage');
var usernameInput = document.querySelector('#usernameInput');
var loginBtn = document.querySelector('#loginBtn');
var callPage = document.querySelector('#callPage');
var callToUsernameInput = document.querySelector('#callToUsernameInput');
var callBtn = document.querySelector('#callBtn');
var hangUpBtn = document.querySelector('#hangUpBtn');
var msgInput = document.querySelector('#msgInput');
var sendMsgBtn = document.querySelector('#sendMsgBtn');
var chatArea = document.querySelector('#chatarea');
var yourConn;
var dataChannel;
callPage.style.display = "none";
// Login when the user clicks the button
loginBtn.addEventListener("click", function (event) {
name = usernameInput.value;
if (name.length > 0) {
send({
type: "login",
name: name
});
}
});
function handleLogin(success) {
if (success === false) {
alert("Ooops...try a different username");
} else {
loginPage.style.display = "none";
callPage.style.display = "block";
//**********************
//Starting a peer connection
//**********************
//using Google public stun server
var configuration = {
"iceServers": [{ "url": "stun:stun2.1.google.com:19302" }]
};
yourConn = new webkitRTCPeerConnection(configuration, {optional: [{RtpDataChannels: true}]});
// Setup ice handling
yourConn.onicecandidate = function (event) {
if (event.candidate) {
send({
type: "candidate",
candidate: event.candidate
});
}
};
//creating data channel
dataChannel = yourConn.createDataChannel("channel1", {reliable:true});
dataChannel.onerror = function (error) {
console.log("Ooops...error:", error);
};
//when we receive a message from the other peer, display it on the screen
dataChannel.onmessage = function (event) {
chatArea.innerHTML += connectedUser + ": " + event.data + "<br />";
};
dataChannel.onclose = function () {
console.log("data channel is closed");
};
}
};
//initiating a call
callBtn.addEventListener("click", function () {
var callToUsername = callToUsernameInput.value;
if (callToUsername.length > 0) {
connectedUser = callToUsername;
// create an offer
yourConn.createOffer(function (offer) {
send({
type: "offer",
offer: offer
});
yourConn.setLocalDescription(offer);
}, function (error) {
alert("Error when creating an offer");
});
}
});
//when somebody sends us an offer
function handleOffer(offer, name) {
connectedUser = name;
yourConn.setRemoteDescription(new RTCSessionDescription(offer));
//create an answer to an offer
yourConn.createAnswer(function (answer) {
yourConn.setLocalDescription(answer);
send({
type: "answer",
answer: answer
});
}, function (error) {
alert("Error when creating an answer");
});
};
//when we got an answer from a remote user
function handleAnswer(answer) {
yourConn.setRemoteDescription(new RTCSessionDescription(answer));
};
//when we got an ice candidate from a remote user
function handleCandidate(candidate) {
yourConn.addIceCandidate(new RTCIceCandidate(candidate));
};
//hang up
hangUpBtn.addEventListener("click", function () {
send({
type: "leave"
});
handleLeave();
});
function handleLeave() {
connectedUser = null;
yourConn.close();
yourConn.onicecandidate = null;
};
//when user clicks the "send message" button
sendMsgBtn.addEventListener("click", function (event) {
var val = msgInput.value;
chatArea.innerHTML += name + ": " + val + "<br />";
//sending a message to a connected peer
dataChannel.send(val);
msgInput.value = "";
});