'영상통화'에 해당되는 글 1건

  1. 2012.06.25 [HTML5] PeerConnection을 이용한 1:1 영상통화 구현 4

HTML5에는 WebRTC라는 기술이 포함되어있다.


웹에서 실시간 커뮤니케이션을 지원한다는 뜻이다 +_+


이미 구글 크롬에는 이 기능이 들어가있다. 실시간 영상채팅을 구현하려고 찾아보니 


자료가 있긴 했는데 딱 내가 필요한만큼만 구현되어 있는 것은 찾지못했다. 


내가 찾기 원하는 것? HTML5 기술만 이용해서(다른 3rd party 코드 없이!!) 1:1 화상통화를 구현해보는 것.


일단 이게 되야 여기에 채팅을 붙이든, 더 많은 클라이언트를 붙이든 확장해볼 수 있을 것이다.


본 포스팅은 여기에 있는 코드를 참고/수정 했음을 밝히는 바이다.



Prerequisite


클라이언트: 웹브라우저 = 구글크롬, 노트북에 카메라와 마이크가 달려있다.


서버: Node.js 설치. 웹서버와 WebSocket 통신을 처리하기 위한 프로세스가 동작하고 있음.(아래 코드 참고)


다음과 같이 chrome 옵션에서 WebRTC 기능을 사용해야 한다.





HTML5 관련 객체


화상통화를 구현하기 위해서는 3가지 객체(?)에 대해 알아야 한다. 다음은 화상통화에서의 각 객체의 기능이다.

(자세한 사용법은 하단의 코드를 참고)


1. webkitGetUserMedia

- 사용자 카메라와 마이크를 이용하기 위해서 사용한다.


2. webkitDeprecatedPeerConnection 또는 webkitPeerConnection

- media(카메라, 마이크) 관련 정보와 데이터를 교환하는데 사용한다.


3. WebSocket

- 서버쪽으로 통신에 필요한 정보를 송신해서 다른 client와 연결을 맺기 위한 통로로 사용한다.

- 채팅기능을 추가한다면 이 통로를 통해 채팅 메시지가 송수신 될 것이다.



테스트 환경 구성도


테스트 환경 구성은 그림과 같다. 동작과정은 크게 2가지로 나뉜다.



처음에는 통신에 필요한 정보를 교환하는 과정인데 여기에 SDP(Session Description Protocol)가 사용된다.


쉽게 말해서 내가 보내려고 하는 정보는 영상이랑 음성이고, 위치는 어디고.. 하는 정보를 교환한다는 말이다.


이 과정이 끝나면 양쪽 클라이언트의 브라우저간에 통신 채널이 열리고 둘이 알아서 영상/음성 데이터를 주고받는다.


물론! 이 과정에서 개발자가 해줘야 할일은 아주 적은 부분에 불과하다. 하지만 짐작했다시피 서버에는 릴레이 기능이 


포함되어있어야 한다. 처음에 클라이언트들은 서버와 WebSocket을 이용해서 채널을 열어놓는다.


한쪽에서 통화요청을 하면 열어둔 채널을 통해 요청 메시지를 전송하고, 서버는 이 메시지를 다른 클라이언트에게 전달한다.


요청 메시지(SDP)를 받은 클라이언트는 이에 대해 응답하고, 자신의 정보도 보내준다. 그럼 최초에 통화요청한 클라이언트는


이 메시지를 받은 후 응답하고 드디어 통화가 시작된다. SDP 메시지는 크롬에서 알아서 만들어주므로 


우리가 할일은 WebSocket을 이용해서 데이터를 서버로 보내는일과 적절한 타이밍에 미디어 정보를 붙이는(?)일 뿐이다.



코드


백문이 불여일견이라. 


Back-end script(broadcast.js)

#!/usr/bin/env node

var WebSocketServer = require('websocket').server;
var http = require('http');

var clients = [];
var idlist = [];
var id = 0;

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;
}

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);

    clients.push(connection);
    idlist.push(request.key);

    console.log((new Date()) + ' Connection accepted.');

    connection.on('message', function(message) {
        if (message.type === 'utf8') {
            console.log('Received Message: ' + message.utf8Data);

            for(var i = 0; i < idlist.length; i++) {
                if (idlist[i] != request.key) {
                    cli = clients[i];
                    msg = message.utf8Data;
                    cli.sendUTF(msg);
                }
            }
        }
        else if (message.type === 'binary') {
            console.log('Received Binary Message of ' + message.binaryData.length + ' bytes');
            connection.sendBytes(message.binaryData);
        }
    });

    connection.on('close', function(reasonCode, description) {
        console.log((new Date()) + ' Peer ' + connection.remoteAddress + ' disconnected.');
    });
});



Front-end script(webconf.html)

<!DOCTYPE html>
<html>
<head>

<script type="text/javascript">
  	var localVideo;
  	var remoteVideo;
  	var localStream;
  	var pc = null;
	var wsock;
	var remoteSrc = null;

  	var STUN_CONF = "NONE";

	// 초기화 함수. 연결요청, 연결수신 client 공통.
	initialize = function() {
    	console.log("Initializing");
		
    	localVideo = document.getElementById("localVideo");
    	remoteVideo = document.getElementById("remoteVideo");

    	resetStatus();
    	// 카메라, 마이크 열기
		getUserMedia();
		// server와 websocket을 열어둔다.
		openChannel();
		
		// 연결요청 버튼 click시 SDP정보 교환 시작
		document.getElementById("join").onclick = function() {
			rtcStart();
		}
  	}

  	resetStatus = function() {
    	setStatus("Initializing...");
  	}

	function openChannel() {
		// SDP 정보 교환을 위해, 서버와 연결한다.
    	url = "ws://192.168.25.114:8080";
	    wsock = new WebSocket(url);

    	wsock.onopen = function() {
        	console.log("open");
    	}
		
		// 서버로부터 메시지를 받을 때 처리.
    	wsock.onmessage = function(e) {
			console.log("S->C:");
        	console.log(e.data);

			// 연결 요청을 받는 client를 위한 코드. 수신측 client는 일단 메시지를 받아야 한다.
			// peerConnection 객체를 생성하고 자신의 media stream을 연결한다.
			if (pc == null) {
				createPeerConnection();
      			pc.addStream(localStream);
			}
				
			// SDP message는 아래 함수로 넘겨주면 된다. 응답은 알아서 해주므로..
        	pc.processSignalingMessage(e.data);
    	}

    	wsock.onclose = function(e) {
        	console.log("closed");
    	}
	}
	
	// 카메라, 마이크 자원을 얻는다.
  	getUserMedia = function() {
    	try {
      		navigator.webkitGetUserMedia({audio:true, video:true}, onUserMediaSuccess,
                                   onUserMediaError);
      		console.log("Requested access to local media with new syntax.");
    	} catch (e) {
      		try {
        		navigator.webkitGetUserMedia("video,audio", onUserMediaSuccess,
                		                     onUserMediaError);
        		console.log("Requested access to local media with old syntax.");
      		} catch (e) {
        		alert("webkitGetUserMedia() failed. Is the MediaStream flag enabled in about:flags?");
        		console.log("webkitGetUserMedia failed with exception: " + e.message);
      		}
    	}
  	}

	// peerConnection 생성
  	createPeerConnection = function() {
    	try {
			// STUN서버 주소와 SDP 메시지를 보내는 함수를 넣어준다.
      		pc = new webkitDeprecatedPeerConnection(STUN_CONF,
                                              onSignalingMessage);
      		console.log("Created webkitDeprecatedPeerConnnection with config");
    	} catch (e) {
      		console.log("Failed to create webkitDeprecatedPeerConnection, exception: " + e.message);
      		try {
        		pc = new webkitPeerConnection(STUN_CONF,
                                      onSignalingMessage);
        		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;
      		}
    	}

    	pc.onconnecting = onSessionConnecting;
    	pc.onopen = onSessionOpened;
    	pc.onaddstream = onRemoteStreamAdded;
    	pc.onremovestream = onRemoteStreamRemoved;
  	}

	// 연결을 요청하는 client를 위한 함수
	// join 버튼을 누르면 동작한다.
	rtcStart = function() {

    	setStatus("Connecting...");
      	console.log("Creating PeerConnection.");
      	createPeerConnection();

      	console.log("Adding local stream.");
		
		// 자신의 media를 연결시킨다. peerConnection을 생성한 이후에 반드시 해줄 것.
      	pc.addStream(localStream);
	}

  	setStatus = function(state) {
    	footer.innerHTML = state;
  	}
	
	// media관련 함수들
	
  	onUserMediaSuccess = function(stream) {
    	console.log("User has granted access to local media.");
    	var url = webkitURL.createObjectURL(stream);
    	localVideo.style.opacity = 1;
    	localVideo.src = url;
    	localStream = stream;
  	}

  	onUserMediaError = function(error) {
    	console.log("Failed to get access to local media. Error code was " + error.code);
    	alert("Failed to get access to local media. Error code was " + error.code + ".");
  	}

	// signaling 관련함수들
	
  	onSignalingMessage = function(message) {
   		console.log('C->S: ' + message);
		// 열어둔 websocket으로 SDP 메시지를 전송한다.
		// peerConnection을 생성하자마자 바로 SDP 메시지를 보낸다.
		wsock.send(message);	
  	}

  	onSessionConnecting = function(message) {
    	console.log("Session connecting.");
  	}

  	onSessionOpened = function(message) {
   		console.log("Session opened.");
  	}

  	onRemoteStreamAdded = function(event) {
    	console.log("Remote stream added.");
    	var url = webkitURL.createObjectURL(event.stream);
    	remoteVideo.style.opacity = 1;
    	remoteVideo.src = url;
		remoteSrc = url;
    	setStatus("<input type=\"button\" id=\"hangup\" value=\"Hang up\" onclick=\"onHangup()\" />");
  	}

  	onRemoteStreamRemoved = function(event) {
    	console.log("Remote stream removed.");
  	}

  	onHangup = function() {
    	console.log("Hanging up.");
    	localVideo.style.opacity = 0;
    	remoteVideo.style.opacity = 0;
    	pc.close();
    	pc = null;
    	setStatus("You have left the call.");
  	}

</script>
</head>

<body onload="initialize();">
<div id="container">

  <div id="local">
    <video
     width="25%" height="25%" id="localVideo" autoplay="autoplay" style="opacity: 0;
     -webkit-transition-property: opacity;
     -webkit-transition-duration: 2s;">
    </video>
  </div>

  <div id="remote">
    <video width="25%" height="25%" id="remoteVideo" autoplay="autoplay"
     style="opacity: 0;
     -webkit-transition-property: opacity;
     -webkit-transition-duration: 2s;">
    </video>
  </div>

  <div id="footer"></div>

</div>

<button id="join">join</button>
</body>
</html>





테스트


서버쪽에서 해줘야 할 일. 미리 설치할 것은 링크걸린 포스팅 참고.


1. webserver 돌리기. front-end script가 있는 곳에서 다음명령 수행


shell> python -m SimpleHTTPServer 8888


2. WebSocket server 돌리기. back-end script가 있는 곳에서 다음명령 수행


shell> node broadcast.js


이제 크롬을 열고 웹서버에 접속하면 된다. 2개의 브라우저에서 서버에 접속해있도록 한다.


(주의 할 것은 지금 구현해 놓은 것이 딱 1:1만 되도록 해놨기 때문에 2명 이상이 접속하면 안되요!! 나는 꼼수다...)


카메라를 사용할꺼냐고 물어보면 당근 사용한다고 하자. 이제 자신의 얼굴이 보인다.


두 개의 브라우저 중 하나에서 하단에 보이는 join 버튼을 누른다. 그럼 상대방 얼굴도 보인다. 다음은 혼자놀기 인증샷.


(자신의 얼굴이 두 개라 썩 유쾌하지는 않지만 ..)





F12 버튼을 누르면 하단에 보이는, 주고받은 SDP 메시지를 볼 수 있다. 구글 크롬 참 좋아요.


Posted by DevMoon
,