WebRTC - Demonstração de voz
Neste capítulo, vamos construir um aplicativo cliente que permite que dois usuários em dispositivos separados se comuniquem usando fluxos de áudio WebRTC. Nosso aplicativo terá duas páginas. Um para login e outro para fazer uma chamada de áudio para outro usuário.
As duas páginas serão as tags div . A maioria das entradas é feita por meio de manipuladores de eventos simples.
Servidor de Sinalização
Para criar uma conexão WebRTC, os clientes devem ser capazes de transferir mensagens sem usar uma conexão de mesmo nível WebRTC. É aqui que usaremos WebSockets HTML5 - uma conexão de soquete bidirecional entre dois pontos de extremidade - um servidor web e um navegador web. Agora vamos começar a usar a biblioteca WebSocket. Crie o arquivo server.js e insira o seguinte 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");
});
A primeira linha requer a biblioteca WebSocket que já instalamos. Em seguida, criamos um servidor de soquete na porta 9090. Em seguida, ouvimos o evento de conexão . Este código será executado quando um usuário fizer uma conexão WebSocket com o servidor. Em seguida, ouvimos todas as mensagens enviadas pelo usuário. Por fim, enviamos uma resposta ao usuário conectado dizendo “Olá do servidor”.
Em nosso servidor de sinalização, usaremos um nome de usuário baseado em string para cada conexão, para que saibamos para onde enviar mensagens. Vamos mudar nosso gerenciador de conexão um pouco -
connection.on('message', function(message) {
var data;
//accepting only JSON messages
try {
data = JSON.parse(message);
} catch (e) {
console.log("Invalid JSON");
data = {};
}
});
Dessa forma, aceitamos apenas mensagens JSON. Em seguida, precisamos armazenar todos os usuários conectados em algum lugar. Usaremos um objeto Javascript simples para isso. Mude a parte superior do nosso arquivo -
//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 adicionar um campo de tipo para cada mensagem vinda do cliente. Por exemplo, se um usuário deseja fazer o login, ele envia a mensagem do tipo de login . Vamos definir isso -
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;
}
});
Se o usuário enviar uma mensagem com o tipo de login , nós -
- Verifique se alguém já fez login com este nome de usuário.
- Em caso afirmativo, diga ao usuário que ele não fez o login com sucesso.
- Se ninguém estiver usando este nome de usuário, adicionamos nome de usuário como uma chave para o objeto de conexão.
- Se um comando não for reconhecido, enviamos um erro.
O código a seguir é uma função auxiliar para enviar mensagens a uma conexão. Adicione-o ao arquivo server.js -
function sendTo(connection, message) {
connection.send(JSON.stringify(message));
}
Quando o usuário desconecta, devemos limpar sua conexão. Podemos excluir o usuário quando o evento close for disparado. Adicione o seguinte código para a conexão handler-
connection.on("close", function() {
if(connection.name) {
delete users[connection.name];
}
});
Após o login bem-sucedido, o usuário deseja ligar para outro. Ele deve fazer uma oferta a outro usuário para alcançá-lo. Adicione o gerenciador de oferta -
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;
Em primeiro lugar, obtemos a conexão do usuário que estamos tentando chamar. Se existir, enviamos-lhe os detalhes da oferta . Também adicionamos outroNome ao objeto de conexão . Isso é feito pela simplicidade de encontrá-lo mais tarde.
Responder à resposta tem um padrão semelhante ao que usamos no manipulador de ofertas . Nosso servidor apenas passa por todas as mensagens como resposta a outro usuário. Adicione o seguinte código após o manipulador de oferta -
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;
A parte final é lidar com o candidato ICE entre os usuários. Usamos a mesma técnica apenas para passar mensagens entre os usuários. A principal diferença é que as mensagens candidatas podem ocorrer várias vezes por usuário em qualquer ordem. Adicione o manipulador 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 nossos usuários se desconectem de outro usuário, devemos implementar a função de desligar. Também dirá ao servidor para excluir todas as referências do usuário. Adicione o manipulador de licença -
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;
Isso também enviará ao outro usuário o evento leave para que ele possa desconectar sua conexão de ponto de acordo. Também devemos lidar com o caso em que um usuário interrompe sua conexão do servidor de sinalização. Vamos modificar nosso manipulador de fechamento -
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"
});
}
}
}
});
A seguir está o código completo do nosso servidor de sinalização -
//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));
}
Aplicativo de cliente
Uma maneira de testar este aplicativo é abrir duas guias do navegador e tentar fazer uma chamada de áudio um para o outro.
Em primeiro lugar, precisamos instalar a biblioteca bootstrap . Bootstrap é uma estrutura de front-end para o desenvolvimento de aplicativos da web. Você pode aprender mais emhttp://getbootstrap.com/.Crie uma pasta chamada, por exemplo, “audiochat”. Esta será a nossa pasta raiz do aplicativo. Dentro desta pasta, crie um arquivo package.json (é necessário para gerenciar dependências npm) e adicione o seguinte -
{
"name": "webrtc-audiochat",
"version": "0.1.0",
"description": "webrtc-audiochat",
"author": "Author",
"license": "BSD-2-Clause"
}
Em seguida, execute npm install bootstrap . Isso instalará a biblioteca de bootstrap na pasta audiochat / node_modules .
Agora precisamos criar uma página HTML básica. Crie um arquivo index.html na pasta raiz com o seguinte 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 deve ser familiar para você. Adicionamos o arquivo css de bootstrap . Também definimos duas páginas. Por fim, criamos diversos campos de texto e botões para obter informações do usuário. Você deve ver os dois elementos de áudio para fluxos de áudio locais e remotos. Observe que adicionamos um link para um arquivo client.js .
Agora precisamos estabelecer uma conexão com nosso servidor de sinalização. Crie o arquivo client.js na pasta raiz com o seguinte 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));
};
Agora execute nosso servidor de sinalização via servidor de nó . Então, dentro da pasta raiz execute o comando estático e abra a página dentro do navegador. Você deve ver a seguinte saída do console -
A próxima etapa é implementar um login de usuário com um nome de usuário exclusivo. Nós simplesmente enviamos um nome de usuário para o servidor, que nos informa se ele foi usado ou não. Adicione o seguinte código ao seu arquivo 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
//**********************
}
};
Em primeiro lugar, selecionamos algumas referências aos elementos da página. O que ocultamos a página de chamada. Em seguida, adicionamos um ouvinte de evento no botão de login. Quando o usuário clica nele, enviamos seu nome de usuário para o servidor. Finalmente, implementamos o callback handleLogin. Se o login for bem-sucedido, mostramos a página de chamada e iniciamos a configuração de uma conexão de par.
Para iniciar uma conexão de par, precisamos -
- Obtenha um stream de áudio de um microfone
- Crie o objeto RTCPeerConnection
Adicione o seguinte código ao “bloco de seletores de IU” -
var localAudio = document.querySelector('#localAudio');
var remoteAudio = document.querySelector('#remoteAudio');
var yourConn;
var stream;
Modifique a função 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);
});
}
};
Agora, se você executar o código, a página deve permitir que você faça login e exiba seu stream de áudio local na página.
Agora estamos prontos para iniciar uma chamada. Em primeiro lugar, enviamos uma oferta a outro usuário. Depois que o usuário recebe a oferta, ele cria uma resposta e começa a negociar candidatos ICE. Adicione o seguinte código ao arquivo 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));
};
Adicionamos um manipulador de cliques ao botão Chamar, que inicia uma oferta. Em seguida, implementamos vários manipuladores esperados pelo manipulador onmessage . Eles serão processados de forma assíncrona até que ambos os usuários tenham feito uma conexão.
A última etapa é implementar o recurso de desligamento. Isso interromperá a transmissão de dados e dirá ao outro usuário para fechar a chamada. Adicione o seguinte 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;
};
Quando o usuário clica no botão Desligar -
- Ele enviará uma mensagem de "deixe" para o outro usuário
- Ele fechará o RTCPeerConnection e destruirá a conexão localmente
Agora execute o código. Você deve conseguir fazer login no servidor usando duas guias do navegador. Você pode então fazer uma chamada de áudio para a guia e desligar a chamada.
A seguir está o arquivo client.js inteiro -
//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;
};