1:1 영상통화 기능을 확장시켜 영상채팅 시스템을 나름 구현해봤다. 코드는 voww project 에서 많은부분 가져다 썼다.
개발환경: 브라우저는 크롬, 리눅스 서버에 Node.js를 설치 (설치관련 내용은 이전 포스팅 참고)
개발도구: 디버깅 - 크롬, 소스편집 툴 - Notepad++(FTP 기능을 이용)
브라우저(크롬)에서 거의 모든 기능을 지원해주다보니 구현할 내용이 별로 없었다.
자잘한 에러들이 남았겠지만 주기능 구현 후 귀찮... -_-;
주요기능
로그인, 방 만들기, 영상채팅 참가하기, 방에서 뛰쳐나오기, 로그아웃
Front-end
body {
margin: 0;
padding: 10px;
font-family: sans-serif;
font-size: 14px;
color: #404040;
overflow: hidden;
}
span.copyright {
float: right;
color: #808080;
font-size: 11px;
}
span.copyright a {
color: #808080;
text-decoration: none;
}
video {
-webkit-transform: scaleX(-1);
}
span.footer {
color: #808080;
font-size: 11px;
padding-left: 10px;
}
span.footer a {
color: #808080;
text-decoration: none;
}
input.button {
border: 1px outset #a0a0a0;
cursor: pointer;
}
.has-shadow {
border: 1px solid #a0a0a0;
-moz-box-shadow: 3px 3px 10px #808080;
-webkit-box-shadow: 3px 3px 10px #808080;
box-shadow: 3px 3px 10px #808080;
}
.has-gradient {
background: -moz-linear-gradient(100% 100% 90deg, #e0e0e0, #f0f0f0);
background: -webkit-gradient(linear, 0% 0%, 0% 100%, from(#f0f0f0), to(#e0e0e0));
background: -webkit-linear-gradient(#f0f0f0, #e0e0e0);
background: -o-linear-gradient(#f0f0f0, #e0e0e0);
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#f0f0f0', endColorstr='#e0e0e0');
}
.heading {
font-weight: bold;
font-size: 14px;
}
#div-header {
position: absolute;
left: 10px;
right: 10px;
top: 10px;
height: 47px;
}
#div-header div {
border: none;
background: #e0e0e0;
height: 19px;
padding: 4px;
}
#div-footer {
position: absolute;
left: 10px;
right: 10px;
bottom: 10px;
height: 50px;
}
#div-footer div {
border: none;
background: #e0e0e0;
height: 19px;
padding: 4px;
}
div#div-connecting {
position: absolute;
top: 100px;
left: 100px;
right: 100px;
bottom: 100px;
background-color: #f0f0f0;
border: 1px solid #a0a0a0;
text-align: center;
color: #ff0000;
font-size: 16px;
padding: 20px;
opacity: 0.8;
z-index: 100;
}
#div-main {
position: absolute;
top: 60px;
bottom: 60px;
left: 10px;
right: 10px;
padding-top: 10px;
}
ol.toc {
height: em;
list-style: none;
margin: 0;
padding: 0;
}
ol.toc li {
float: left;
cursor: pointer;
margin: 0 1px 0 0;
padding-top: 2px;
padding-bottom: 2px;
padding-left: 10px;
padding-right: 10px;
border: 1px solid #c0c0c0;
background-color: #f0f0f0;
background: -moz-linear-gradient(100% 100% 90deg, #b0b0b0, #d0d0d0);
background: -webkit-gradient(linear, 0% 0%, 0% 100%, from(#d0d0d0), to(#b0b0b0));
background: -webkit-linear-gradient(#d0d0d0, #b0b0b0);
background: -o-linear-gradient(#d0d0d0, #b0b0b0);
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#d0d0d0', endColorstr='#b0b0b0');
}
ol.toc li.current {
cursor: default;
border: 1px solid #c0c0c0;
background-color: #d0d0d0;
background: -moz-linear-gradient(100% 100% 90deg, #e0e0e0, #f0f0f0);
background: -webkit-gradient(linear, 0% 0%, 0% 100%, from(#f0f0f0), to(#e0e0e0));
background: -webkit-linear-gradient(#f0f0f0, #e0e0e0);
background: -o-linear-gradient(#f0f0f0, #e0e0e0);
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#f0f0f0', endColorstr='#e0e0e0');
}
div.content {
position: absolute;
border: 1px solid #a0a0a0;
top: 32px;
bottom: 0px;
left: 0px;
right: 0px;
clear: left;
padding: 10px;
background-color: #a8a8a8;
}
div.content-box {
background-color: #f0f0f0;
border: 1px solid #a0a0a0;
}
div.content-header {
padding-left: 4px;
padding-right: 4px;
padding-top: 2px;
padding-bottom: 2px;
border: 1px solid #c0c0c0;
background-color: #f0f0f0;
background: -moz-linear-gradient(100% 100% 90deg, #b0b0b0, #d0d0d0);
background: -webkit-gradient(linear, 0% 0%, 0% 100%, from(#d0d0d0), to(#b0b0b0));
background: -webkit-linear-gradient(#d0d0d0, #b0b0b0);
background: -o-linear-gradient(#d0d0d0, #b0b0b0);
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#d0d0d0', endColorstr='#b0b0b0');
}
div#user-list-box {
position: absolute;
top: 10px;
height: 190px;
left: 10px;
width: 200px;
}
div#text-chat-box {
position: absolute;
top: 210px;
bottom: 10px;
left: 10px;
width: 200px;
}
div#join-conf-box {
position: absolute;
top: 20px;
height: 100px;
left: 20px;
width: 310px;
/* border: 1px solid #a0a0a0;*/
}
div#create-conf-box {
position: absolute;
top: 140px;
height: 100px;
left: 20px;
width: 310px;
/* border: 1px solid #a0a0a0;*/
}
div#login-box {
position: absolute;
top: 20px;
left: 350px;
height: 80px;
width: 210px;
}
div#who-is-online-box {
position: absolute;
top: 120px;
left: 350px;
height: 240px;
width: 210px;
}
div#online-user-list {
padding-top: 4px;
padding-bottom: 4px;
padding-left: 10px;
padding-right: 10px;
overflow: auto;
}
div#min-requirements {
position: absolute;
left: 580px;
top: 10px;
}
div#chat-history {
position: absolute;
top: 22px;
bottom: 24px;
left: 0px;
right: 0px;
padding-left: 2px;
padding-right: 2px;
overflow: auto;
}
div#chat-input {
position: absolute;
bottom: 4px;
left: 2px;
right: 2px;
height: 20px;
}
div#videos-box {
position: absolute;
top: 10px;
bottom: 10px;
right: 10px;
left: 220px;
border: 1px solid #a8a8a8;
}
div.video {
position: absolute;
background-color: #000000;
}
div#user-list {
position: absolute;
overflow: auto;
top: 20px;
bottom: 0px;
width: 100%;
}
ul.participants {
list-style: none;
margin: 0;
padding: 0;
}
ul.participants li {
clear: left;
padding-left: 4px;
padding-right: 4px;
padding-top: 2px;
padding-bottom: 2px;
margin: 0;
border-bottom: 1px solid #c0c0c0;
color: #808080;
}
img.button-icon {
float: right;
cursor: pointer;
}
button.closebutton {
position: absolute;
top: 2px;
right: 2px;
z-index: 1000;
width: 20px;
height: 20px;
padding: 0px;
cursor: pointer;
background-color: #ffffff;
}
button.prevbutton {
position: absolute;
bottom: 2px;
right: 31px;
z-index: 1000;
width: 30px;
height: 20px;
padding: 0px;
cursor: pointer;
background-color: #ffffff;
}
button.nextbutton {
position: absolute;
bottom: 2px;
right: 2px;
z-index: 1000;
width: 30px;
height: 20px;
padding: 0px;
cursor: pointer;
background-color: #ffffff;
}
div.mouse {
position: absolute;
width: 10px;
height: 10px;
border: 2px solid green;
background-color: #ff0000;
z-index: 500;
}
<!DOCTYPE html>
<html>
<head>
<title>Web Conference</title>
<meta content="text/html; charset=utf-8">
<link rel="stylesheet" href="index.css" type="text/css"></link>
<script type="text/javascript" src="common.js"></script>
<script type="text/javascript" src="wsock.js"></script>
<script type="text/javascript" src="model.js"></script>
<script type="text/javascript" src="controller.js"></script>
</head>
<body>
<div id="div-main">
<ol id="ol-tabs" class="toc">
<li id="li-join" class="current" onclick="controller.on_select_tab('join')">Main</li>
<li id="li-conference" onclick="controller.on_select_tab('conference')">Video Conference</li>
</ol>
<!-- main tab -->
<div id="div-join" class="content has-shadow" style="visibility: visible; overflow: auto;">
<div id="join-conf-box" class="content-box">
<div class="content-header">Join Existing Web Conference</div>
<table>
<tr>
<td align="right">Select a conference:</td>
<td>
<select id="join_room" style="width: 130px;"></select>
</td>
</tr>
<tr>
<td colspan="2" align="center">
<input id="join-button" type="button" value="Join" class="button"
onclick="controller.action_join_or_leave()" title="join/leave a conference"></input>
</td>
</tr>
</table>
</div>
<div id="create-conf-box" class="content-box">
<div class="content-header">Create New Web Conference</div>
<table>
<tr>
<td align="right">Conference name:</td>
<td>
<input type="text" id="create_room"/>
</td>
</tr>
<tr>
<td colspan="2" align="center">
<input id="create-button" type="button" value="Create" class="button"
onclick="controller.action_create_room()" title="create a new conference and join it"></input>
</td>
</tr>
</table>
</div>
<div id="login-box" class="content-box">
<div class="content-header">Login</div>
<table>
<tr>
<td align="right">Your name:</td>
<td>
<input type="text" id="login_name" style="width: 100px;"/>
</td>
</tr>
<tr>
<td colspan="2" align="center">
<input id="login-button" type="button" value="Login" class="button"
onclick="controller.action_login_out()" title="login/logout"></input>
</td>
</tr>
</table>
</div>
<div id="who-is-online-box" class="content-box">
<div class="content-header">Who is Online?</div>
<div id="online-user-list"></div>
</div>
</div>
<!-- conference tab -->
<div id="div-conference" class="content has-shadow" style="visibility: hidden;">
<div id="user-list-box" class="content-box">
<div class="content-header">Participants</div>
<div id="user-list">
<ul id="participants" class="participants"></ul>
</div>
</div>
<div id="text-chat-box" class="content-box">
<div class="content-header">Text Chat</div>
<div id="chat-history"></div>
<div id="chat-input">
<input id="inputText" type="text" style="left: 2px; width: 190px;" autocomplete="off"
title="Type your message here and press enter"
value="Enter your message here"
onclick="if ($('inputText').value == 'Enter your message here') {
$('inputText').value = '';
}"
onkeypress="javascript: if ((event.keyCode || event.which) == 13) {
controller.action_send_chatmsg($('inputText').value);
$('inputText').value = '';
return false;
}"
>
</div>
</div>
<div id="videos-box"></div> <!-- don't put any text node in videos-box -->
</div>
</div>
<script type="text/javascript">
controller.init();
model.init();
</script>
</body>
</html>
// controller(=controller + view in here.)
// - receive action from user, request to model, control display.
var controller = {
//-------------------------------------------
// PROPERTIES
//-------------------------------------------
m_video_num: 0,
m_video_pos: [],
MAX_VIDEO: 8,
init: function() {
controller.m_video_num = 0;
controller.m_video_pos.clear();
var half = controller.MAX_VIDEO/2;
for(var i = 0; i < controller.MAX_VIDEO; i++) {
var pos = {top:0, left:0};
if (i < half) {
pos.top = 0;
pos.left = 240*i;
} else {
pos.top = 180;
pos.left = 240*(i-4);
}
controller.m_video_pos.push(pos);
}
$('join-button').disabled = true;
$('create-button').disabled = true;
},
//-------------------------------------------
// METHODS for user action.
//-------------------------------------------
action_login_out: function() {
var state = $('login-button').value;
var name = $('login_name').value;
if (state == "Login")
{
if (name == "") {
alert("Please input your name.");
return ;
}
var online_users = $('online-user-list').childNodes;
for(var i = 0; i < online_users.length; i++) {
var id = online_users[i].getAttribute('id');
if (('li-'+name) == id) {
alert("Name " + name + " aleady exsit.");
return ;
}
}
model.req_login(name);
}
else
{
model.req_logout(name);
}
},
result_login: function(result, roomlist) {
if (result == "OK") {
for(var i = 0; i < roomlist.length; i++) {
controller.ctrl_add_room(roomlist[i]);
}
} else {
alert("error: " + result);
}
var name = $('login_name').value;
controller.ctrl_add_online_user(name);
$('login-button').value = "Logout";
$('login_name').disabled = true;
$('join-button').disabled = false;
$('join_room').disabled = false;
$('create-button').disabled = false;
},
result_logout: function(user) {
// remove room list.
var element = $('join_room');
while (element.firstChild) {
element.removeChild(element.firstChild);
}
$('login-button').value = "Login";
$('login_name').disabled = false;
$('join-button').value = "Join";
$('create-button').disabled = false;
$('create_room').value = "";
controller.clear_conf();
controller.ctrl_del_online_user(user);
controller.on_select_tab("join");
// init again.
controller.init();
},
action_create_room: function() {
var roomname = $('create_room').value;
if (roomname == "") {
alert("Please input conference name.");
return ;
}
var list = $('join_room').childNodes;
for(var i = 0; i < list.length; i++) {
if (list[i].value == roomname) {
alert("room " + roomname + " is aleady exist.");
return ;
}
}
model.req_create_room(roomname);
},
result_create_room: function(roomname, user) {
controller.ctrl_add_room(roomname);
$('join-button').disabled = false;
$('join-button').value = "Leave";
$('create-button').disabled = true;
$('join_room').disabled = true;
controller.on_select_tab("conference");
},
// chatting: only has action and event.
action_send_chatmsg: function(text) {
model.req_chat(text);
},
action_join_or_leave: function() {
if ($("join-button").value == "Join")
{
var room_name = $('join_room').value;
if (room_name == "") {
alert("Please select roomname.");
return ;
}
model.req_join(room_name);
}
else if ($('join-button').value == "Leave")
{
model.req_leave();
}
},
result_join: function(userlist) {
$('join-button').value = "Leave";
$('create-button').disabled = true;
$('join_room').disabled = true;
controller.on_select_tab("conference");
},
result_leave: function(roomname) {
var party_num = $('participants').childNodes.length;
console.log("roomname: " + roomname + " will be removed.");
if (party_num == 1) {
controller.ctrl_del_room(roomname);
}
controller.clear_conf();
controller.on_select_tab('join');
// init again.
controller.init();
$('join_room').disabled = false;
$('join-button').value = "Join";
$('join-button').disabled = false;
$('create_room').disabled = false;
$('create-button').disabled = false;
},
on_select_tab: function(name) {
var selected = $('li-' + name);
var children = $('ol-tabs').childNodes;
for (var i in children) {
var child = children[i];
child.className = (selected == child ? "current" : null);
}
// show selected tap.
var selected = $('div-' + name);
var children = $('div-main').childNodes;
for (var i=0; i<children.length; ++i) {
var child = children[i];
if (child.nodeType == 1 && child.nodeName.toLowerCase() == "div") {
child.style.visibility = (selected == child ? "visible": "hidden");
}
}
},
//-------------------------------------------
// METHODS for event.
//-------------------------------------------
event_online_user: function(userlist) {
for(var i = 0; i < userlist.length; i++) {
controller.ctrl_add_online_user(userlist[i]);
}
},
event_login: function(user) {
controller.ctrl_add_online_user(user);
},
event_logout: function(user) {
controller.ctrl_del_room_user(user);
controller.ctrl_del_video(user);
controller.ctrl_del_online_user(user);
},
event_room_created: function(roomname) {
controller.ctrl_add_room(roomname);
},
event_room_removed: function(roomname) {
controller.ctrl_del_room(roomname);
},
event_chatmsg: function(sender, text) {
controller.ctrl_add_chatmsg(sender + ": " + text);
},
event_add_room_user: function(user, url) {
controller.ctrl_add_room_user(user);
controller.ctrl_add_video_frame(user);
var video = $('video-webrtc-' + user);
video.setAttribute('src', url);
},
event_leave_room: function(user) {
controller.del_room_user(user);
},
event_close: function(user) {
controller.del_room_user(user);
controller.ctrl_del_online_user(user);
},
//-------------------------------------------
// METHODS to use internal.
//-------------------------------------------
// clear confrence room. remove all videos and participants.
clear_conf: function() {
var element = $('participants');
while (element.firstChild) {
element.removeChild(element.firstChild);
}
element = $('videos-box');
while (element.firstChild) {
element.removeChild(element.firstChild);
}
controller.m_video_num = 0;
element = $('chat-history');
while (element.firstChild) {
element.removeChild(element.firstChild);
}
},
del_room_user: function(user) {
// remove from user-list
controller.ctrl_del_room_user(user);
// remove video
controller.ctrl_del_video(user);
},
//-------------------------------------------
// METHODS for each control.
//-------------------------------------------
// add to online-user list
ctrl_add_online_user: function(username) {
var child = document.createElement('li');
child.appendChild(document.createTextNode(username));
child.setAttribute('id', 'li-'+username);
$('online-user-list').appendChild(child);
},
// delete from online-user list
ctrl_del_online_user: function(username) {
var elem = $('li-' + username);
if (elem) {
$('online-user-list').removeChild(elem);
}
},
// add to conference room list
ctrl_add_room: function(roomname) {
var new_child = document.createElement('option');
new_child.innerHTML = roomname;
$('join_room').appendChild(new_child);
},
// delete from conference room list
ctrl_del_room: function(roomname) {
var root = $('join_room');
var children = $('join_room').childNodes;
for(var i = 0; i < children.length; i++) {
if (children[i].value == roomname) {
root.removeChild(children[i]);
return ;
}
}
},
// add to confrenece chatting list
ctrl_add_room_user: function(username) {
var child = document.createElement('li');
child.appendChild(document.createTextNode(username));
child.setAttribute('id', 'li-'+username);
$('participants').appendChild(child);
},
// delete from conference chatting list
ctrl_del_room_user: function(user) {
var child = $('participants').childNodes;
var userid = 'li-'+user;
for(var i = 0; i < child.length; i++) {
if (child[i].getAttribute('id') == userid) {
$('participants').removeChild(child[i]);
break;
}
}
},
// add to conference videos-box
ctrl_add_video_frame: function(user) {
var id = "user-video-" + user;
if ($(id)) {
console.log(user + " is aleady added.");
return; // already added
}
var child = document.createElement('div');
child.id = id;
child.style.width = "240px";
child.style.height = "180px";
child.style.minWidth = "215px";
child.style.minHeight = "138px";
child.style.position = "absolute";
var videoIdx = controller.m_video_num++;
child.style.top = controller.m_video_pos[videoIdx].top+"px";
child.style.left = controller.m_video_pos[videoIdx].left+"px";
child.style.backgroundColor = "#000000";
child.style.overflow = "hidden";
$('videos-box').appendChild(child);
var video = document.createElement('video');
video.id = "video-webrtc-" + user;
video.style.width = "100%";
video.style.height = "100%";
video.autoplay = "autoplay";
child.appendChild(video);
},
// delete from conference video-box
ctrl_del_video: function(user) {
var elem = $('user-video-' + user);
if (elem) {
$('videos-box').removeChild(elem);
controller.m_video_num--;
var children = $('videos-box').childNodes;
// rearrangement.
for(var i = 0; i < controller.m_video_num; i++) {
children[i].style.top = controller.m_video_pos[i].top+"px";
children[i].style.left = controller.m_video_pos[i].left+"px";
}
}
},
ctrl_add_chatmsg: function(text) {
var child = document.createElement('li');
child.appendChild(document.createTextNode(text));
$('chat-history').appendChild(child);
},
};
// model: do background job.
var model = {
//-------------------------------------------
// PROPERTIES
//-------------------------------------------
// my name.
m_myid: null,
// my room
m_my_room: null,
// peer connection
m_peers: [],
// stream object
m_local_stream: null,
// STUN server address.
STUN_CONF: "NONE",
init: function() {
model.m_peers.map_init();
wsock.init(model.__wsock_open, model.__wsock_close, model.__wsock_notify);
},
//-----------------------------------------------------------------------
// WebSocket
//-----------------------------------------------------------------------
__wsock_open: function() {
console.log("websocket opened.");
model.req_online_user();
},
__wsock_close: function() {
console.log("websocket closed.");
controller.event_close();
},
__wsock_notify: function(message) {
console.log("msg type: " + message.type);
switch(message.type)
{
case ("RSP"):
break;
case ("SDP"):
model.recv_sdp_msg(message);
break;
case ("CHAT"):
model.recv_chat_msg(message);
break;
case ("EVENT"):
model.recv_event(message);
break;
}
},
//--------------------------------------------------------------------------------------------------------
recv_sdp_msg: function(message) {
var pc = model.m_peers.map_get(message.sender);
if (pc == undefined) {
model.create_peer_conn(message.sender);
pc = model.m_peers.map_get(message.sender);
pc.addStream(model.m_local_stream);
}
pc.processSignalingMessage(message.sdp);
},
recv_chat_msg: function(response) {
console.log("model] recv chat message: sender[" + response.sender + "] msg[" + response.msg + "]");
controller.event_chatmsg(response.sender, response.msg);
},
recv_event: function(message) {
switch(message.event)
{
case("LOGIN"):
controller.event_login(message.user);
break;
case("LOGOUT"):
controller.event_logout(message.user);
break;
case("LEAVE"):
controller.event_leave_room(message.user);
model.delete_peer(message.user);
break;
case("ROOM_CREATED"):
if (model.m_myid) { controller.event_room_created(message.roomname); }
break;
case("ROOM_REMOVED"):
controller.event_room_removed(message.roomname);
break;
case("CLOSED"):
model.delete_peer(message.user);
controller.event_close(message.user);
break;
default:
console.log("invalid message event: " + message.event);
break;
}
},
delete_peer: function(user) {
var pc = model.m_peers.map_get(user);
if (pc) {
// WARN: canary를 사용할 경우 close()를 호출하면 다른 peer connection까지 멈춘다.
// 그렇다고 종료를 안할수도 없고.. 일단 주석처리 하고 시험하면 모두 멈추는 문제는 없음.
pc.close();
}
model.m_peers.map_del(user);
},
//-------------------------------------------------------------------------
// request and result.
//-------------------------------------------------------------------------
req_online_user: function() {
var json = {};
json.type = "REQ";
json.req = "ONLINE_USER";
wsock.send(json, model.result_online_user);
},
result_online_user: function(response) {
controller.event_online_user(response.userlist);
},
req_login: function(name) {
var json = {};
json.type = "REQ";
json.req = "LOGIN";
json.user = name;
wsock.send(json, model.result_login);
model.m_myid = name;
},
result_login: function(response) {
controller.result_login(response.code, response.roomlist);
},
req_logout: function(name) {
var json = {};
json.type = "REQ";
json.req = "LOGOUT";
if (model.m_my_room) json.roomname = model.m_my_room;
json.user = name;
wsock.send(json, model.result_logout);
},
result_logout: function(response) {
if (response.code == "OK") {
// close peer connection
var keys = model.m_peers.map_keys();
for(var i = 0; i < keys.length; i++) {
model.delete_peer(keys[i]);
}
}
if (model.m_local_stream) {
model.m_local_stream.stop();
}
controller.result_logout(model.m_myid);
},
req_create_room: function(roomname) {
// send request
var json = {};
json.type = "REQ";
json.req = "MAKE_ROOM";
json.roomname = roomname;
json.master = model.m_myid;
model.m_my_room = roomname;
wsock.send(json, model.result_create_room);
},
result_create_room: function(response) {
model.get_user_media();
controller.result_create_room(model.m_my_room, model.m_myid);
},
req_join: function(room) {
var json = {};
json.type = "REQ";
json.req = "JOIN";
json.roomname = room;
json.user = model.m_myid;
model.m_my_room = room;
wsock.send(json, model.result_join);
},
result_join: function(response) {
if (response.code == "OK") {
controller.result_join(response.userlist);
model.get_user_media();
model.start_rtc(response.userlist);
}
},
req_leave: function() {
var json = {};
json.type = "REQ";
json.req = "LEAVE";
json.roomname = model.m_my_room;
json.user = model.m_myid;
wsock.send(json, model.result_leave);
},
result_leave: function(response) {
if (response.code == "OK") {
// close peer connection
var keys = model.m_peers.map_keys();
for(var i = 0; i < keys.length; i++) {
model.delete_peer(keys[i]);
}
}
controller.result_leave(model.m_my_room);
model.m_my_room = null;
if (model.m_local_stream) {
model.m_local_stream.stop();
}
},
req_chat: function(text) {
var json = {};
json.type = "CHAT";
json.sender = model.m_myid;
json.roomname = model.m_my_room;
json.msg = text;
wsock.send(json);
},
//-----------------------------------------------------------------------
// peer connection
//-----------------------------------------------------------------------
start_rtc: function(userlist) {
for(var i = 0; i < userlist.length; i++) {
var user = userlist[i];
console.log("user["+i+"]="+user);
if (user != model.m_myid) {
model.create_peer_conn(user);
}
}
},
create_peer_conn: function(userId) {
var newpc = null;
try {
newpc = new webkitDeprecatedPeerConnection(model.STUN_CONF,
function(message) { model.on_signal_msg(userId, message); });
console.log("Created webkitDeprecatedPeerConnnection with config");
} catch (e) {
console.log("Failed to create webkitDeprecatedPeerConnection, exception: " + e.message);
try {
newpc = new webkitPeerConnection(model.STUN_CONF,
function(message) { model.on_signal_msg(userId, message); });
console.log("Created webkitPeerConnnection with config.");
} catch (e) {
console.log("Failed to create webkitPeerConnection, exception: " + e.message);
alert("Cannot create PeerConnection object; Is the 'PeerConnection' flag enabled in about:flags?");
return null;
}
}
model.m_peers.map_set(userId, newpc);
newpc.onconnecting = function(message) { model.on_session_connecting(userId, message); };
newpc.onopen = function(message) { model.on_session_opened(userId, message); };
newpc.onaddstream = function(event) { model.on_remote_stream_added(userId, event.stream); };
newpc.onremovestream = function(event) { model.on_remote_stream_removed(userId); };
},
on_signal_msg: function(userId, sdp) {
var json = {};
json.type = "SDP";
json.sender = model.m_myid;
json.receiver = userId;
json.sdp = sdp;
wsock.send(json);
},
on_session_connecting: function(userId, message) {
console.log("sssion connecting... userid: " + userId);
},
on_session_opened: function(userId, message) {
console.log("session opend. userid: " + userId);
},
on_remote_stream_added: function(userId, stream) {
console.log("webrtc - onaddstream(" + userId + ",...)");
var url = webkitURL.createObjectURL(stream);
controller.event_add_room_user(userId, url);
if (model.m_local_stream != null) {
model.m_peers.map_get(userId).addStream(model.m_local_stream);
}
},
on_remote_stream_removed: function(userId) {
console.log(userId + " steram is removed.");
},
//-----------------------------------------------------------------------
// user media
//-----------------------------------------------------------------------
get_user_media: function() {
try {
navigator.webkitGetUserMedia({video:true, audio:true}, model.on_user_media_success,
model.on_user_media_error);
console.log("Requested access to local media with new syntax.");
} catch (e) {
try {
navigator.webkitGetUserMedia("video,audio", model.on_user_media_success,
model.on_user_media_error);
console.log("Requested access to local media with old syntax.");
} catch (e) {
console.log("webkitGetUserMedia failed with exception: " + e.message);
}
}
},
on_user_media_success: function(stream) {
console.log("User has granted access to local media.");
var url = webkitURL.createObjectURL(stream);
model.m_local_stream = stream;
controller.event_add_room_user(model.m_myid, url);
},
on_user_media_error: function(error) {
console.log("Failed to get access to local media. Error code was " + error.code);
},
};
Back-end
#!/usr/bin/env node
require('./common.js');
// key=roomname, value=room object
var roomList = [];
roomList.map_init();
function Room(name, master) {
this.m_name = name;
this.m_users = new Array();
this.m_users.push(master);
}
Room.prototype.addUser = function(userId) {
this.m_users.push(userId);
}
Room.prototype.delUser = function(userId) {
this.m_users.splice(this.m_users.indexOf(userId),1);
}
Room.prototype.getUserList = function() {
return this.m_users;
}
// External API
module.exports = {
createRoom: function(roomname, master) {
var newRoom = new Room(roomname, master);
console.log("user number: " + newRoom.getUserList().length);
roomList.map_set(roomname, newRoom);
console.log("New room [" + roomname + "] was created..roomnum: "+roomList.map_keys().length);
},
join: function(roomname, user) {
if (roomList.map_get(roomname)) {
roomList.map_get(roomname).addUser(user);
}
},
leave: function(roomname, user) {
var room = roomList.map_get(roomname);
if (room) {
room.delUser(user);
if (room.getUserList().length == 0) {
roomList.map_del(roomname);
return true;
}
}
return false;
},
getRoomNameList: function() {
return roomList.map_keys();
},
getUserListByRoom: function(roomname) {
if (roomList.map_get(roomname) != null) {
return roomList.map_get(roomname).getUserList();
} else {
return null;
}
},
getRoomNameByUser: function(user) {
var keys = roomList.map_keys();
for(var i = 0; i < keys.length; i++) {
var list = roomList[keys[i]].getUserList();
if (list.indexOf(user) != -1) {
return keys[i];
}
}
return null;
},
};
#!/usr/bin/env node
var WebSocketServer = require('websocket').server;
var http = require('http');
var roomMgr = require('./roommgr.js');
require('./common.js');
var connArr = [];
var userArr = [];
var keyUserArr = [];
// connection이 생성되면서 값이 추가된다.
// {key:request_key, value:conn object}
connArr.map_init();
// {key:user_id, value:conn object}
userArr.map_init();
// {key:reuqest.key, value:userId}
keyUserArr.map_init();
var server = http.createServer(function(request, response) {
console.log((new Date()) + ' Received request for ' + request.url);
response.writeHead(404);
response.end();
});
server.listen(8080, function() {
console.log((new Date()) + ' Server is listening on port 8080');
});
wsServer = new WebSocketServer({
httpServer: server,
// You should not use autoAcceptConnections for production
// applications, as it defeats all standard cross-origin protection
// facilities built into the protocol and the browser. You should
// *always* verify the connection's origin and decide whether or not
// to accept it.
autoAcceptConnections: false
});
function originIsAllowed(origin) {
// put logic here to detect whether the specified origin is allowed.
return true;
}
//----------------------------------------------------------------------
// functions for request.
//----------------------------------------------------------------------
function procReqOnlineUser(key, recvMsg) {
var sendMsg = {};
if (recvMsg.msg_id) {
sendMsg.msg_id = recvMsg.msg_id;
}
sendMsg.type = "RSP";
sendMsg.code = "OK";
sendMsg.userlist = userArr.map_keys();
sendResponse(key, sendMsg);
}
function procReqLogin(key, recvMsg) {
var sendMsg = {};
if (recvMsg.msg_id) {
sendMsg.msg_id = recvMsg.msg_id;
}
// to sender
sendMsg.type = "RSP";
sendMsg.code = "OK";
sendMsg.roomlist = roomMgr.getRoomNameList();
sendResponse(key, sendMsg);
// to others..
sendMsg = {};
sendMsg.type = "EVENT";
sendMsg.event = "LOGIN";
sendMsg.user = recvMsg.user;
broadcastToOthers(key, sendMsg);
userArr.map_set(recvMsg.user, connArr.map_get(key));
keyUserArr.map_set(key, recvMsg.user);
}
function procReqLogout(key, recvMsg) {
var sendMsg = {};
if (recvMsg.msg_id) {
sendMsg.msg_id = recvMsg.msg_id;
}
// to sender
sendMsg.type = "RSP";
sendMsg.code = "OK";
sendResponse(key, sendMsg);
var isRoomRemoved = false;
// remove from room
if ("roomanme" in recvMsg) {
isRoomRemoved = roomMgr.leave(recvMsg.roomname, recvMsg.user);
}
// to others..
sendMsg = {};
sendMsg.type = "EVENT";
sendMsg.event = "LOGOUT";
sendMsg.user = recvMsg.user;
broadcastToOthers(key, sendMsg);
if (isRoomRemoved) {
sendMsg = {};
sendMsg.type = "EVENT";
sendMsg.event = "ROOM_REMOVED";
sendMsg.roomname = recvMsg.roomname;
broadcastToOthers(key, sendMsg);
}
userArr.map_del(recvMsg.user);
keyUserArr.map_del(key);
}
function procReqMakeRoom(key, recvMsg) {
var sendMsg = {};
if (recvMsg.msg_id) {
sendMsg.msg_id = recvMsg.msg_id;
}
// to room maker
sendMsg.type = "RSP";
sendMsg.code = "OK";
sendResponse(key, sendMsg);
// make a room.
roomMgr.createRoom(recvMsg.roomname, recvMsg.master);
// to others..
sendMsg = {};
sendMsg.type = "EVENT";
sendMsg.event = "ROOM_CREATED";
sendMsg.roomname = recvMsg.roomname;
broadcastToOthers(key, sendMsg);
}
function procReqJoin(key, recvMsg) {
var sendMsg= {};
if (recvMsg.msg_id) {
sendMsg.msg_id = recvMsg.msg_id;
}
sendMsg.type = "RSP";
sendMsg.code = "OK";
roomMgr.join(recvMsg.roomname, recvMsg.user);
sendMsg.userlist = roomMgr.getUserListByRoom(recvMsg.roomname);
// client간 peer connection을 맺으면서 서로 대화상대로 추가할 것이므로
// broadcasting할 필요는 없다.
sendResponse(key, sendMsg);
}
function procReqLeave(key, recvMsg) {
var sendMsg = {};
if (recvMsg.msg_id) {
sendMsg.msg_id = recvMsg.msg_id;
}
// to sender
sendMsg.type = "RSP";
sendMsg.code = "OK";
sendResponse(key, sendMsg);
// remove from a room
var isRoomRemoved = roomMgr.leave(recvMsg.roomname, recvMsg.user);
// to others..
sendMsg = {};
sendMsg.type = "EVENT";
sendMsg.event = "LEAVE";
sendMsg.user = recvMsg.user;
broadcastToRoom(recvMsg.roomname, sendMsg);
if (isRoomRemoved) {
sendMsg = {};
sendMsg.type = "EVENT";
sendMsg.event = "ROOM_REMOVED";
sendMsg.roomname = recvMsg.roomname;
broadcastToOthers(key, sendMsg);
}
}
//----------------------------------------------------------------------
// functions for sending message.
//----------------------------------------------------------------------
// send a message to users in the room.
function broadcastToRoom(roomname, jsonObj) {
var userlist = roomMgr.getUserListByRoom(roomname);
if (userlist == null) {
console.log("Nobody in " + roomname);
return ;
}
var msg = JSON.stringify(jsonObj);
for(var i = 0; i < userlist.length; i++) {
var user = userlist[i];
var conn = userArr[user];
console.log("sending message: " + msg);
conn.sendUTF(msg);
}
}
// send a message to everybody except sender.
function broadcastToOthers(senderKey, jsonObj) {
var keys = connArr.map_keys();
var msg = JSON.stringify(jsonObj);
for(var i = 0; i < keys.length; i++) {
if (keys[i] != senderKey) {
var conn = connArr.map_get(keys[i]);
conn.sendUTF(msg);
}
}
}
// send a message only to sender
function sendResponse(key, jsonObj) {
var conn = connArr[key];
var msg = JSON.stringify(jsonObj);
console.log("sending message: " + msg);
conn.sendUTF(msg);
}
// send a message to specific user.
function sendToPeer(peerId, jsonObj) {
var msg = JSON.stringify(jsonObj);
console.log("sending message: " + msg);
var conn = userArr[peerId];
conn.sendUTF(msg);
}
//----------------------------------------------------------------------
// MAIN procedure
//----------------------------------------------------------------------
wsServer.on('request', function(request) {
if (!originIsAllowed(request.origin)) {
// Make sure we only accept requests from an allowed origin
request.reject();
console.log((new Date()) + ' Connection from origin ' + request.origin + ' rejected.');
return;
}
var connection = request.accept(null, request.origin);
console.log((new Date()) + ' Connection accepted.');
// save client connection
connArr.map_set(request.key, connection);
// receive a message
connection.on('message', function(message) {
if (message.type === 'utf8') {
var recvMsg = JSON.parse(message.utf8Data);
var type = recvMsg.type;
if (type != "SDP") {
console.log('Received Message: ' + message.utf8Data);
}
switch(type)
{
case ("REQ"):
var req = recvMsg.req;
switch(req)
{
case("ONLINE_USER"):
procReqOnlineUser(request.key, recvMsg);
break;
case("LOGIN"):
procReqLogin(request.key, recvMsg);
break;
case("LOGOUT"):
procReqLogout(request.key, recvMsg);
break;
case("MAKE_ROOM"):
procReqMakeRoom(request.key, recvMsg);
break;
case("JOIN"):
procReqJoin(request.key, recvMsg);
break;
case("LEAVE"):
procReqLeave(request.key, recvMsg);
break;
default:
console.log("Unknown request [" + req + "]");
break;
}
break;
case ("CHAT"):
broadcastToRoom(recvMsg.roomname, recvMsg);
break;
case ("SDP"):
sendToPeer(recvMsg.receiver, recvMsg);
break;
default:
console.log("invalid type: " + type);
break;
}
}
else if (message.type === 'binary')
{
console.log('Received Binary Message of ' + message.binaryData.length + ' bytes');
connection.sendBytes(message.binaryData);
}
});
// connection closed
connection.on('close', function(reasonCode, description) {
console.log((new Date()) + ' Peer ' + connection.remoteAddress + ' disconnected.');
var user = keyUserArr.map_get(request.key);
if (user == null) {
console.log("aleady removed.");
} else {
var roomname = roomMgr.getRoomNameByUser(user);
var sendMsg = {};
sendMsg.type = "EVENT";
sendMsg.event = "CLOSED";
sendMsg.user = user;
roomMgr.leave(roomname, user);
broadcastToOthers(request.key, sendMsg);
}
userArr.map_del(user);
connArr.map_del(request.key);
keyUserArr.map_del(request.key);
});
});
공통
// common functions.
Array.prototype.clear = function() {
while(this[0]) {
this.splice(0,1);
}
}
function $(id) {
return document.getElementById(id);
}
//-------------------------------------------------
// add map function to array.
//-------------------------------------------------
Array.prototype.map_init = function() {
this.keys = [];
}
Array.prototype.map_set = function(key, value) {
if (this[key] == undefined) {
this.keys.push(key);
}
this[key] = value;
}
Array.prototype.map_get = function(key) {
var index = this.keys.indexOf(key);
if (index == -1) {
console.log("key: " + key + " is not exist.");
return null;
}
var key = this.keys[index];
return this[key];
}
Array.prototype.map_del = function(key) {
var index = this.keys.indexOf(key);
if (index != -1) {
this.keys.splice(index,1);
}
if (this[key]) {
delete this[key];
}
}
Array.prototype.map_keys = function() {
return this.keys;
}
//-------------------------------------------------
테스트
Front-end 코드가 있는 쪽에서, 웹서버 돌리기
shell> python -m SimpleHTTPServer 8888
Back-end 코드가 있는 쪽에서, 웹소켓 서버 돌리기
shell> node webconf.js
브라우저를 열고 웹서버 주소로 들어가서 테스트 해보면 된다.
혼자놀기 화면
'프로그래밍 > HTML5' 카테고리의 다른 글
| [HTML5] PeerConnection을 이용한 1:1 영상통화 구현 (4) | 2012.06.25 |
|---|---|
| [HTML5] WebSocket을 이용한 초간단 채팅 (0) | 2012.06.20 |
| [HTML5] WebSocket echo 테스트 / 서버설치 (0) | 2012.06.20 |
| [HTML5] 위치정보 사용하기 (0) | 2012.06.18 |
| [HTML5] 캠(cam) 이용하기 (0) | 2012.06.04 |
Posted by DevMoon



