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