WebRTC - Demostración de voz
En este capítulo, vamos a crear una aplicación cliente que permite que dos usuarios en dispositivos separados se comuniquen usando transmisiones de audio WebRTC. Nuestra aplicación tendrá dos páginas. Uno para iniciar sesión y el otro para realizar una llamada de audio 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 realizar una llamada de audio 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, "audiochat". 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-audiochat",
"version": "0.1.0",
"description": "webrtc-audiochat",
"author": "Author",
"license": "BSD-2-Clause"
}
Luego ejecute npm install bootstrap . Esto instalará la biblioteca bootstrap en la carpeta audiochat / 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 Voice 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 Voice 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">
<div class = "row">
<div class = "col-md-6 text-right">
Local audio: <audio id = "localAudio"
controls autoplay></audio>
</div>
<div class = "col-md-6 text-left">
Remote audio: <audio id = "remoteAudio"
controls autoplay></audio>
</div>
</div>
<div class = "row text-center">
<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>
<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. Debería ver los dos elementos de audio para transmisiones de audio locales y remotas. 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 y comenzamos a configurar una conexión entre pares.
Para iniciar una conexión entre pares, necesitamos:
- Obtenga una transmisión de audio de un micrófono
- Cree el objeto RTCPeerConnection
Agregue el siguiente código al "bloque de selectores de UI" -
var localAudio = document.querySelector('#localAudio');
var remoteAudio = document.querySelector('#remoteAudio');
var yourConn;
var stream;
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
//**********************
//getting local audio stream
navigator.webkitGetUserMedia({ video: false, audio: true }, function (myStream) {
stream = myStream;
//displaying local audio stream on the page
localAudio.src = window.URL.createObjectURL(stream);
//using Google public stun server
var configuration = {
"iceServers": [{ "url": "stun:stun2.1.google.com:19302" }]
};
yourConn = new webkitRTCPeerConnection(configuration);
// setup stream listening
yourConn.addStream(stream);
//when a remote user adds stream to the peer connection, we display it
yourConn.onaddstream = function (e) {
remoteAudio.src = window.URL.createObjectURL(e.stream);
};
// Setup ice handling
yourConn.onicecandidate = function (event) {
if (event.candidate) {
send({
type: "candidate",
});
}
};
}, function (error) {
console.log(error);
});
}
};
Ahora, si ejecuta el código, la página debería permitirle iniciar sesión y mostrar su transmisión de audio local en la página.
Ahora estamos listos para iniciar una llamada. En primer lugar, enviamos una oferta a otro usuario. 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 último paso es implementar la función de colgar. Esto dejará de transmitir datos y le dirá al otro usuario que cierre la llamada. Agrega el siguiente código -
//hang up
hangUpBtn.addEventListener("click", function () {
send({
type: "leave"
});
handleLeave();
});
function handleLeave() {
connectedUser = null;
remoteAudio.src = null;
yourConn.close();
yourConn.onicecandidate = null;
yourConn.onaddstream = null;
};
Cuando el usuario hace clic en el botón Colgar:
- Enviará un mensaje de "dejar" al otro usuario
- Cerrará la RTCPeerConnection y destruirá la conexión localmente
Ahora ejecuta el código. Debería poder iniciar sesión en el servidor utilizando dos pestañas del navegador. A continuación, puede realizar una llamada de audio a la pestaña y colgar la llamada.
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 localAudio = document.querySelector('#localAudio');
var remoteAudio = document.querySelector('#remoteAudio');
var yourConn;
var stream;
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
//**********************
//getting local audio stream
navigator.webkitGetUserMedia({ video: false, audio: true }, function (myStream) {
stream = myStream;
//displaying local audio stream on the page
localAudio.src = window.URL.createObjectURL(stream);
//using Google public stun server
var configuration = {
"iceServers": [{ "url": "stun:stun2.1.google.com:19302" }]
};
yourConn = new webkitRTCPeerConnection(configuration);
// setup stream listening
yourConn.addStream(stream);
//when a remote user adds stream to the peer connection, we display it
yourConn.onaddstream = function (e) {
remoteAudio.src = window.URL.createObjectURL(e.stream);
};
// Setup ice handling
yourConn.onicecandidate = function (event) {
if (event.candidate) {
send({
type: "candidate",
candidate: event.candidate
});
}
};
}, function (error) {
console.log(error);
});
}
};
//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;
remoteAudio.src = null;
yourConn.close();
yourConn.onicecandidate = null;
yourConn.onaddstream = null;
};