WebRTC-비디오 데모
이 장에서는 별도의 장치에있는 두 명의 사용자가 WebRTC를 사용하여 통신 할 수있는 클라이언트 응용 프로그램을 빌드 할 것입니다. 신청서에는 두 페이지가 있습니다. 하나는 로그인 용이고 다른 하나는 다른 사용자에게 전화하기위한 것입니다.
두 페이지는 div 태그가됩니다. 대부분의 입력은 간단한 이벤트 핸들러를 통해 이루어집니다.
시그널링 서버
WebRTC 연결을 생성하려면 클라이언트는 WebRTC 피어 연결을 사용하지 않고 메시지를 전송할 수 있어야합니다. 여기에서 HTML5 WebSockets-두 끝점 사이의 양방향 소켓 연결-웹 서버와 웹 브라우저를 사용할 것입니다. 이제 WebSocket 라이브러리 사용을 시작하겠습니다. server.js 파일을 생성하고 다음 코드를 삽입하십시오-
//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");
});
첫 번째 줄에는 이미 설치 한 WebSocket 라이브러리가 필요합니다. 그런 다음 포트 9090에 소켓 서버를 만듭니다. 다음으로 연결 이벤트를 수신합니다 . 이 코드는 사용자가 서버에 WebSocket 연결을 할 때 실행됩니다. 그런 다음 사용자가 보낸 모든 메시지를 듣습니다. 마지막으로 연결된 사용자에게 "Hello from server"라는 응답을 보냅니다.
시그널링 서버에서는 각 연결에 문자열 기반 사용자 이름을 사용하여 메시지를 보낼 위치를 알 수 있습니다. 연결 핸들러 를 약간 변경해 보겠습니다.
connection.on('message', function(message) {
var data;
//accepting only JSON messages
try {
data = JSON.parse(message);
} catch (e) {
console.log("Invalid JSON");
data = {};
}
});
이렇게하면 JSON 메시지 만 허용됩니다. 다음으로 연결된 모든 사용자를 어딘가에 저장해야합니다. 이를 위해 간단한 Javascript 객체를 사용합니다. 파일 상단 변경-
//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 = {};
클라이언트에서 오는 모든 메시지에 대해 유형 필드 를 추가 할 것 입니다. 예를 들어 사용자가 로그인을 원하면 로그인 유형 메시지 를 보냅니다 . 그것을 정의합시다-
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;
}
});
사용자가 로그인 유형 으로 메시지를 보내면 우리는-
이 사용자 이름으로 이미 로그인 한 사람이 있는지 확인하십시오.
그렇다면 사용자에게 성공적으로 로그인하지 않았다고 알립니다.
이 사용자 이름을 사용하는 사람이 없으면 사용자 이름을 연결 개체에 키로 추가합니다.
명령이 인식되지 않으면 오류를 보냅니다.
다음 코드는 연결에 메시지를 보내기위한 도우미 함수입니다. server.js 파일에 추가 하십시오-
function sendTo(connection, message) {
connection.send(JSON.stringify(message));
}
사용자가 연결을 끊으면 연결을 정리해야합니다. 닫기 이벤트가 발생 하면 사용자를 삭제할 수 있습니다 . 연결 핸들러에 다음 코드를 추가하십시오 -
connection.on("close", function() {
if(connection.name) {
delete users[connection.name];
}
});
로그인에 성공하면 사용자는 다른 사람에게 전화를 걸고 싶어합니다. 그는 그것을 달성하기 위해 다른 사용자에게 제안 을 해야 합니다. 오퍼 핸들러 추가 -
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;
먼저, 우리 가 호출하려는 사용자 의 연결 을 얻습니다 . 존재하는 경우 제안 세부 정보를 보냅니다 . 또한 연결 개체 에 otherName 을 추가 합니다. 이것은 나중에 쉽게 찾을 수 있도록 만들어졌습니다.
응답에 대한 응답에는 오퍼 핸들러 에서 사용한 것과 유사한 패턴이 있습니다. 우리 서버는 다른 사용자에 대한 응답 으로 모든 메시지를 통과 합니다. 오퍼 핸들러 뒤에 다음 코드를 추가하십시오 -
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;
마지막 부분은 사용자 간의 ICE 후보를 처리하는 것입니다. 우리는 사용자간에 메시지를 전달하는 것과 동일한 기술을 사용합니다. 주요 차이점은 후보 메시지가 순서에 관계없이 사용자 당 여러 번 발생할 수 있다는 것입니다. 후보 핸들러 추가 -
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;
또한 다른 사용자에게 나가기 이벤트를 보내서 그에 따라 피어 연결을 끊을 수 있습니다. 사용자가 시그널링 서버에서 연결을 끊는 경우도 처리해야합니다. 닫기 핸들러를 수정 해 보겠습니다.
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"
});
}
}
}
});
다음은 시그널링 서버의 전체 코드입니다.
//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));
}
클라이언트 애플리케이션
이 애플리케이션을 테스트하는 한 가지 방법은 두 개의 브라우저 탭을 열고 서로를 호출하는 것입니다.
먼저 부트 스트랩 라이브러리 를 설치해야합니다 . 부트 스트랩은 웹 애플리케이션 개발을위한 프런트 엔드 프레임 워크입니다. 자세한 내용은http://getbootstrap.com/.예를 들어 "videochat"이라는 폴더를 만듭니다. 이것이 우리의 루트 애플리케이션 폴더가 될 것입니다. 이 폴더 안에 package.json 파일을 만들고 (npm 종속성을 관리하는 데 필요함) 다음을 추가하십시오.
{
"name": "webrtc-videochat",
"version": "0.1.0",
"description": "webrtc-videochat",
"author": "Author",
"license": "BSD-2-Clause"
}
그런 다음 npm install bootstrap을 실행하십시오 . 그러면 videochat / node_modules 폴더 에 부트 스트랩 라이브러리가 설치됩니다 .
이제 기본 HTML 페이지를 만들어야합니다. 다음 코드를 사용하여 루트 폴더에 index.html 파일을 만듭니다.
<html>
<head>
<title>WebRTC Video Demo</title>
<link rel = "stylesheet" href = "node_modules/bootstrap/dist/css/bootstrap.min.css"/>
</head>
<style>
body {
background: #eee;
padding: 5% 0;
}
video {
background: black;
border: 1px solid gray;
}
.call-page {
position: relative;
display: block;
margin: 0 auto;
width: 500px;
height: 500px;
}
#localVideo {
width: 150px;
height: 150px;
position: absolute;
top: 15px;
right: 15px;
}
#remoteVideo {
width: 500px;
height: 500px;
}
</style>
<body>
<div id = "loginPage" class = "container text-center">
<div class = "row">
<div class = "col-md-4 col-md-offset-4">
<h2>WebRTC Video Demo. Please sign in</h2>
<label for = "usernameInput" class = "sr-only">Login</label>
<input type = "email" id = "usernameInput" c
lass = "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">
<video id = "localVideo" autoplay></video>
<video id = "remoteVideo" autoplay></video>
<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>
이 페이지는 익숙 할 것입니다. 부트 스트랩 CSS 파일 을 추가했습니다 . 또한 두 페이지를 정의했습니다. 마지막으로 사용자로부터 정보를 얻기위한 여러 텍스트 필드와 버튼을 만들었습니다. 로컬 및 원격 비디오 스트림에 대한 두 개의 비디오 요소가 표시되어야합니다. client.js 파일 에 대한 링크를 추가했습니다 .
이제 시그널링 서버와 연결을 설정해야합니다. 다음 코드로 루트 폴더에 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));
};
이제 노드 서버 를 통해 신호 서버를 실행 합니다 . 그런 다음 루트 폴더 내에서 정적 명령을 실행 하고 브라우저에서 페이지를 엽니 다. 다음 콘솔 출력이 표시되어야합니다.
다음 단계는 고유 한 사용자 이름으로 사용자 로그인을 구현하는 것입니다. 우리는 단순히 사용자 이름을 서버로 보낸 다음 가져 왔는지 여부를 알려줍니다. 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');
//hide call page
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 {
//display the call page if login is successful
loginPage.style.display = "none";
callPage.style.display = "block";
//start peer connection
}
};
먼저 페이지의 요소에 대한 몇 가지 참조를 선택합니다. 우리는 호출 페이지를 숨 깁니다. 그런 다음 로그인 버튼에 이벤트 리스너를 추가합니다. 사용자가 클릭하면 사용자 이름을 서버로 보냅니다. 마지막으로 handleLogin 콜백을 구현합니다. 로그인에 성공하면 통화 페이지가 표시되고 피어 연결 설정이 시작됩니다.
피어 연결을 시작하려면 다음이 필요합니다.
- 웹 카메라에서 스트림을 가져옵니다.
- RTCPeerConnection 개체를 만듭니다.
"UI 선택기 블록"에 다음 코드를 추가하십시오-
var localVideo = document.querySelector('#localVideo');
var remoteVideo = document.querySelector('#remoteVideo');
var yourConn;
var stream;
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 video stream
navigator.webkitGetUserMedia({ video: true, audio: true }, function (myStream) {
stream = myStream;
//displaying local video stream on the page
localVideo.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) {
remoteVideo.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);
});
}
};
이제 코드를 실행하면 페이지에서 로그인하고 페이지에 로컬 비디오 스트림을 표시 할 수 있습니다.
이제 통화를 시작할 준비가되었습니다. 먼저 다른 사용자 에게 제안 을 보냅니다 . 사용자가 제안을 받으면 답변 을 작성하고 ICE 후보자 거래를 시작합니다. 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));
};
오퍼를 시작하는 Call 버튼에 클릭 핸들러를 추가 합니다. 그런 다음 onmessage 핸들러에서 예상하는 여러 핸들러를 구현 합니다. 두 사용자가 모두 연결될 때까지 비동기 적으로 처리됩니다.
마지막 단계는 끊기 기능을 구현하는 것입니다. 이렇게하면 데이터 전송이 중지되고 다른 사용자에게 통화를 종료하도록 알립니다. 다음 코드를 추가하십시오-
//hang up
hangUpBtn.addEventListener("click", function () {
send({
type: "leave"
});
handleLeave();
});
function handleLeave() {
connectedUser = null;
remoteVideo.src = null;
yourConn.close();
yourConn.onicecandidate = null;
yourConn.onaddstream = null;
};
사용자가 전화 끊기 버튼을 클릭하면-
- 다른 사용자에게 "나가기"메시지를 보냅니다.
- RTCPeerConnection을 닫고 로컬에서 연결을 파괴합니다.
이제 코드를 실행하십시오. 두 개의 브라우저 탭을 사용하여 서버에 로그인 할 수 있어야합니다. 그런 다음 탭을 호출하고 전화를 끊을 수 있습니다.
다음은 전체 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 localVideo = document.querySelector('#localVideo');
var remoteVideo = document.querySelector('#remoteVideo');
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 video stream
navigator.webkitGetUserMedia({ video: true, audio: true }, function (myStream) {
stream = myStream;
//displaying local video stream on the page
localVideo.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) {
remoteVideo.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;
remoteVideo.src = null;
yourConn.close();
yourConn.onicecandidate = null;
yourConn.onaddstream = null;
};
요약
이 데모는 모든 WebRTC 애플리케이션에 필요한 기능의 기준을 제공합니다. 이 데모를 개선하려면 Facebook 또는 Google과 같은 플랫폼을 통해 사용자 식별을 추가하고 잘못된 데이터에 대한 사용자 입력을 처리 할 수 있습니다. 또한 기술을 지원하지 않거나 방화벽을 통과 할 수없는 등 여러 가지 이유로 인해 WebRTC 연결이 실패 할 수 있습니다. WebRTC 애플리케이션을 안정적으로 만드는 데 많은 노력이 듭니다.