WebRTC - Demo Suara
Dalam bab ini, kita akan membuat aplikasi klien yang memungkinkan dua pengguna di perangkat terpisah untuk berkomunikasi menggunakan aliran audio WebRTC. Aplikasi kita akan memiliki dua halaman. Satu untuk login dan yang lainnya untuk melakukan panggilan audio ke pengguna lain.
Kedua halaman tersebut akan menjadi tag div . Sebagian besar masukan dilakukan melalui penanganan acara sederhana.
Server Pensinyalan
Untuk membuat klien koneksi WebRTC harus dapat mentransfer pesan tanpa menggunakan koneksi peer WebRTC. Di sinilah kita akan menggunakan HTML5 WebSockets - koneksi soket dua arah antara dua titik akhir - server web dan browser web. Sekarang mari mulai menggunakan pustaka WebSocket. Buat file server.js dan masukkan kode berikut -
//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");
});
Baris pertama membutuhkan pustaka WebSocket yang telah kami instal. Kemudian kami membuat server soket pada port 9090. Selanjutnya, kami mendengarkan acara koneksi . Kode ini akan dijalankan ketika pengguna membuat koneksi WebSocket ke server. Kami kemudian mendengarkan setiap pesan yang dikirim oleh pengguna. Akhirnya, kami mengirim tanggapan ke pengguna yang terhubung dengan mengatakan "Halo dari server".
Di server pensinyalan kami, kami akan menggunakan nama pengguna berbasis string untuk setiap koneksi sehingga kami tahu ke mana harus mengirim pesan. Mari kita ubah sedikit penangan koneksi kita -
connection.on('message', function(message) {
var data;
//accepting only JSON messages
try {
data = JSON.parse(message);
} catch (e) {
console.log("Invalid JSON");
data = {};
}
});
Dengan cara ini kami hanya menerima pesan JSON. Selanjutnya, kita perlu menyimpan semua pengguna yang terhubung di suatu tempat. Kami akan menggunakan objek Javascript sederhana untuk itu. Ubah bagian atas file kami -
//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 = {};
Kami akan menambahkan bidang tipe untuk setiap pesan yang datang dari klien. Misalnya jika pengguna ingin masuk, dia mengirimkan pesan jenis masuk . Mari kita definisikan -
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;
}
});
Jika pengguna mengirim pesan dengan tipe login , kita -
- Periksa apakah ada yang sudah masuk dengan nama pengguna ini.
- Jika demikian, beri tahu pengguna bahwa dia belum berhasil masuk.
- Jika tidak ada yang menggunakan nama pengguna ini, kami menambahkan nama pengguna sebagai kunci ke objek koneksi.
- Jika perintah tidak dikenali, kami mengirimkan kesalahan.
Kode berikut adalah fungsi pembantu untuk mengirim pesan ke koneksi. Tambahkan ke file server.js -
function sendTo(connection, message) {
connection.send(JSON.stringify(message));
}
Ketika pengguna memutuskan koneksi, kita harus membersihkan koneksinya. Kita bisa menghapus pengguna saat event close diaktifkan. Tambahkan kode berikut untuk koneksi handler-
connection.on("close", function() {
if(connection.name) {
delete users[connection.name];
}
});
Setelah berhasil login, pengguna ingin menelepon yang lain. Dia harus membuat penawaran kepada pengguna lain untuk mencapainya. Tambahkan penangan penawaran -
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;
Pertama, kami mendapatkan koneksi dari pengguna yang kami coba hubungi. Jika ada, kami mengirimkan detail penawaran kepadanya . Kami juga menambahkan otherName ke objek koneksi . Ini dibuat untuk kesederhanaan menemukannya nanti.
Menjawab respon memiliki pola yang sama dengan yang kami gunakan di penangan penawaran . Server kami hanya melewati semua pesan sebagai jawaban untuk pengguna lain. Tambahkan kode berikut setelah penangan penawaran -
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;
Bagian terakhir adalah menangani kandidat ICE antar pengguna. Kami menggunakan teknik yang sama hanya dengan meneruskan pesan antar pengguna. Perbedaan utamanya adalah bahwa pesan kandidat dapat terjadi beberapa kali per pengguna dalam urutan apa pun. Tambahkan penangan kandidat -
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;
Untuk memungkinkan pengguna kami memutuskan sambungan dari pengguna lain, kami harus menerapkan fungsi menutup telepon. Ini juga akan memberi tahu server untuk menghapus semua referensi pengguna. Tambahkan penangan cuti -
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;
Ini juga akan mengirim pengguna lain acara cuti sehingga dia dapat memutuskan koneksi peer-nya yang sesuai. Kita juga harus menangani kasus ketika pengguna memutuskan koneksi dari server pensinyalan. Mari kita memodifikasi penangan dekat kita -
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"
});
}
}
}
});
Berikut ini adalah seluruh kode server pensinyalan kami -
//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));
}
Aplikasi Klien
Salah satu cara untuk menguji aplikasi ini adalah membuka dua tab browser dan mencoba melakukan panggilan audio satu sama lain.
Pertama-tama, kita perlu menginstal library bootstrap . Bootstrap adalah kerangka kerja frontend untuk mengembangkan aplikasi web. Anda dapat mempelajari lebih lanjut dihttp://getbootstrap.com/.Buat folder bernama, misalnya, "audiochat". Ini akan menjadi folder aplikasi root kami. Di dalam folder ini buat file package.json (diperlukan untuk mengelola dependensi npm) dan tambahkan yang berikut ini -
{
"name": "webrtc-audiochat",
"version": "0.1.0",
"description": "webrtc-audiochat",
"author": "Author",
"license": "BSD-2-Clause"
}
Kemudian jalankan npm install bootstrap . Ini akan menginstal pustaka bootstrap di folder audiochat / node_modules .
Sekarang kita perlu membuat halaman HTML dasar. Buat file index.html di folder root dengan kode berikut -
<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>
Halaman ini pasti sudah tidak asing lagi bagi Anda. Kami telah menambahkan file css bootstrap . Kami juga telah menetapkan dua halaman. Akhirnya, kami telah membuat beberapa bidang teks dan tombol untuk mendapatkan informasi dari pengguna. Anda akan melihat dua elemen audio untuk aliran audio lokal dan jarak jauh. Perhatikan bahwa kami telah menambahkan tautan ke file client.js .
Sekarang kita perlu membuat koneksi dengan server pensinyalan kita. Buat file client.js di folder root dengan kode berikut -
//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));
};
Sekarang jalankan server pensinyalan kami melalui server node . Kemudian, di dalam folder root jalankan perintah statis dan buka halaman di dalam browser. Anda harus melihat output konsol berikut -
Langkah selanjutnya adalah mengimplementasikan login pengguna dengan nama pengguna unik. Kami hanya mengirim nama pengguna ke server, yang kemudian memberi tahu kami apakah itu sudah diambil atau tidak. Tambahkan kode berikut ke file client.js Anda -
//******
//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
//**********************
}
};
Pertama, kami memilih beberapa referensi ke elemen di halaman. Kami menyembunyikan halaman panggilan. Kemudian, kami menambahkan pendengar acara di tombol masuk. Ketika pengguna mengkliknya, kami mengirimkan nama penggunanya ke server. Terakhir, kami mengimplementasikan callback handleLogin. Jika login berhasil, kami menampilkan halaman panggilan dan mulai mengatur koneksi peer.
Untuk memulai koneksi rekan yang kita butuhkan -
- Dapatkan aliran audio dari mikrofon
- Buat objek RTCPeerConnection
Tambahkan kode berikut ke "blok pemilih UI" -
var localAudio = document.querySelector('#localAudio');
var remoteAudio = document.querySelector('#remoteAudio');
var yourConn;
var stream;
Ubah fungsi 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);
});
}
};
Sekarang jika Anda menjalankan kode, halaman tersebut memungkinkan Anda untuk masuk dan menampilkan aliran audio lokal Anda di halaman.
Sekarang kami siap untuk memulai panggilan. Pertama, kami mengirimkan penawaran ke pengguna lain. Setelah pengguna mendapatkan penawaran, dia membuat jawaban dan mulai memperdagangkan kandidat ICE. Tambahkan kode berikut ke file 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));
};
Kami menambahkan handler klik ke tombol Panggil, yang memulai penawaran. Kemudian kami menerapkan beberapa penangan yang diharapkan oleh penangan onmessage . Mereka akan diproses secara asinkron hingga kedua pengguna membuat sambungan.
Langkah terakhir adalah menerapkan fitur menutup telepon. Ini akan menghentikan transmisi data dan memberi tahu pengguna lain untuk menutup panggilan. Tambahkan kode berikut -
//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;
};
Saat pengguna mengklik tombol Tutup -
- Ini akan mengirim pesan "pergi" ke pengguna lain
- Ini akan menutup RTCPeerConnection dan menghancurkan koneksi secara lokal
Sekarang jalankan kodenya. Anda harus bisa masuk ke server menggunakan dua tab browser. Anda kemudian dapat melakukan panggilan audio ke tab tersebut dan menutup panggilan.
Berikut ini adalah seluruh file client.js -
//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;
};