상세 컨텐츠

본문 제목

[HTML5] 웹소켓 채팅, 음성 또는 화상채팅 구현 관련 PeerConnection을 이용한 1:1 영상통화 구현

WEB/html5

by AlrepondTech 2020. 9. 21. 04:30

본문

반응형

 

=================================

=================================

=================================

 

 

전화기, TV, 컴퓨터가 공통 플랫폼위에서 모두 대화할 수 있는 세상을 상상해보십시요. 여러분의 웹어플리케이션에 비디오채팅 기능과 P2P 데이터 공유 기능을 쉽게 추가했다고 상상해보십시요. 이것이 WebRTC의 비전입니다.

확인해보고 싶으십니까? WebRTC는 현재 구글 Chrome과 Opera, Firefox에서 가능합니다. apprtc.appspot.com은 간단한 비디오 채팅 어플리케이션을 시작해볼수 있는 좋은 곳입니다:

  1. apprtc.appspot.com 페이지를 Chrome이나 Opera, Firefox에서 엽니다.
  2. 페이지(웹어플리케이션)에서 Webcam을 사용하도록 허용버튼을 클릭합니다.
  3. 다른 탭 또는 더 좋은 방법으로 다른 컴퓨터에서 페이지 하단에 표시된 URL을 브라우저에서 엽니다.

이 어플리케이션에 대한 설명은 본문 아래에서 다룹니다.

Quick start

본문을 읽기에 시간이 없거나 바로 코드만 보고 싶으십니까?

  1. Google I/O에서 발표되었던 WebRTC에 대한 소개를 시청하시기 바랍니다. (슬라이드 자료는 여기):
  2. getUserMedia를 이전에 사용해보지 않았다면, HTML5 Rocks 기사를 읽어 보시기 바랍니다. 그리고 simpl.info/gum에서 간단한 예제 코드를 확인할 수 있습니다.
  3. 뒤에 나오는 간단한 예제와 WebRTC를 single page로 구현한 데모를 통해서 RTCPeerConnection API를 파악하시기 바랍니다.
  4. apprtc.appspot.com에서 코드와 콘솔로그를 통해 WebRTC가 시그널링, 방화벽, NAT traversal을 위해 서버를 어떻게 사용하는지 더 깊이 공부해보시기 바랍니다.

아니면 저희의 WebRTC codelab으로 바로 들어오시기 바랍니다: 간단한 시그널링 서버를 포함한 비디오 채팅앱을 만드는 법을 step-by-step으로 설명한 가이드입니다.

WebRTC의 아주 짧은 역사

웹에서 남은 주요 도전 과제 중에 남은 하나는 영상과 음성을 통한 인간의 의사소통 - 실시간 대화(Real Time Communication, RTC) 가능하게 하는 것 입니다. 실시간 대화는 문자 입력란에 문자를 입력하는 것처럼 웹 어플리케이션에서 자연스러워야 하는 것입니다. 그렇지 않다면 사람들과 소통하는 새로운 방식들을 혁신하고 개발하는 능력을 우리는 한정 지어버리게 됩니다.

역사적으로 실시간 대화는 집에서 사용하기에 어려웠고, 비싼 음성/영상 기술들이 필요했습니다. 이미 존재하는 콘텐츠와 데이터 서비스들과 함께 실시간 대화 기술을 융합하는 것은 매우 어렵고 시간이 많이 드는 일이었습니다. 특히 웹에서.

Gmail의 영상채팅은 2008년에 출시되었고, 2011년에는 Google Talk 서비스를 (Gamil에서 처럼)사용한 행아웃이 소개되었습니다. 구글은 코덱, Echo cancellation 등의 실시간 통신에 필요한 많은 기술을 개발한 회사인 GIPS를 삽니다. 구글은 GIPS에서 개발한 기술들을 오픈 소스화하면서 업계의 합의를 얻기위한 IETF와 W3C에서 해당 기술과 관련된 표준들의 중심으 뛰어듭니다. 2011년 5월, Ericsson이 첫번째 WebRTC 데모를 개발합니다.

WebRTC는 현재 실시간, 플러그인 필요없는 영상과 음성, data 통신에 대한 공개 표준들을 구현했습니다. 현실의 요구들입니다:

  • 많은 웹서비스들은 이미 실시간 통신을 사용하고 있습니다. 하지만 네이티브 앱이나 플러그인들의 다운로드가 필요합니다. Skype, (Skype를 사용하는)Facebook (Google Talk 플러그인을 사용하는)Google Hangouts등이 그렇습니다.
  • 다운로딩, 설치 그리고 플러그인들을 업데이트 하는 것들은 복잡할 수 있고 애러를 발생시키며 귀찮은 일입니다.
  • 플러그인들을 배포하고, 디버깅하고 문제잡고 테스트하고 유지하는 것은 힘든 일입니다. 그리고 아마도 복잡하고 비싼 기술들에 대한 라이센스나 구현이 필요할 수 도 있습니다. 사용자들에게 플러그인 첫 설치를 독려하는 것은 정말 어려운 일입니다!

WebRTC 프로젝트의 API들은 오픈소스이고, 무료이고, 표준화되어야하고 웹브라우저 안에서 만들어져야고, 이전의 기술들보다 효율적이어야한다는 것이 WebRTC 프로젝트의 안내 지침 입니다.

현재 어디까지?

WebRTC에는 세가지 API가 구현되어 있습니다:

getUserMedia는 Chrome, Opera 그리고 Firefox에서 가능합니다. 크로스 브라우저간의 데모 - simpl.info/gum 와 Web Audio의 입력으로 getUserMedia 사용한 Chris Wilson의 환상적인 예제를 둘러보시기 바랍니다.

RTCPeerConnection은 Chrome(데스크탑과 Android), Opera(데스크탑과 최신 안드로이드 beta)와 Firefox에서 구현되어있습니다. 이름으로 쓰이는 임시 단어가 여러번 반복된 이후에 현재 크롬에서는 RTCPeerConnection을webkitRTCPeerConnection으로 구현되었고, Firefox에서는mozRTCPeerConnection로 구현되었습니다. 다른 이름들과 구현들은 더 이상 사용되지 않게 되었습니다. 표준화 과정이 안정화될 때, 머릿말들은 모두 삭제될 것입니다. 여기 아주 간단한 크롬의 RTCPeerConnection 데모 simpl.info/pc와 아주 훌륭한 비디오챗 프로그램 apprtc.appspot.com이 있습니다. 브라우저간의 차이와 명세 변경에 대한 것을 추상화하는 adapter.js - 구글에서 관리하는 Javascript 코드를 이 프로그램에서 사용하고 있습니다.

RTCDataChannel 은 Chrome 25버전, Opera 18버전, Firefox 22버전 이상에서 지원하고 있습니다.

인터넷 익스플로러에서는 Chrome Frame을 통해 WebRTC기능이 사용 가능하고, (2011년에 Microsoft에 흡수된) Skype에서는 WebRTC를 사용려고 계획 중입니다.WebRTC는 또한 WebKitGTK+ 와 Qt 네이티브 앱에 통합되었습니다.

주의할 것

'WebRTC를 지원하는' 플랫폼의 이야기들은 의심을 해봐야합니다. 대부분의 플랫폼들은 RTC 컴포넌트들은 전혀 지원하지 않고 단지 getUserMedia만 지원한다는 뜻 입니다.

My first WebRTC

WebRTC 어플리케이션에는 반드시 해야하는 몇 가지 것들이 있습니다:

  • 스트리밍 오디오, 비디오 또는 데이터를 가져와야 합니다.
  • IP주소, 포트등의 네트워크 정보를 가져와야하고, (peers로 알려진) 다른 WebRTC 클라이언트들과 연결을 위해 이 정보들을 교환해야 합니다. 심지어NATs와 방화벽을 통해서도 교환해야 합니다.
  • 애러들의 보고, 세션들의 초기화와 종료를 위한 신호 통신을 관리해야 합니다.
  • 해상도와 코덱들 같은 미디어와 클라이언트의 capabilty에 대한 정보를 교환해야 합니다.
  • 스트리밍 오디오, 비디오 또는 데이터를 주고 받아야합니다.

스트리밍 데이터를 얻고 통신하기위애 WebRTC에서는 다음과 같은 API들을 제공합니다:

  • MediaStream: 사용자의 카메라와 마이크 같은 곳의 데이터 스트림에 접근합니다.
  • RTCPeerConnection: 암호화 및 대역폭 관리를 하는 기능을 가지고 있고, 오디오 또는 비디오 연결을 합니다.
  • RTCDataChannel: 일반적인 데이터 P2P통신

(뒤에서 WebRTC의 네트워크와 신호에 관해 자세하게 다룹니다.)

MediaStream (aka getUserMedia)

MediaStream API는 미디어의 동기화된 스트림들을 말합니다. 예를 들어, 카메라와 마이크의 입력에서 받아온 스트림은 오디오와 비디오 트랙들로 동기화 됩니다. (MediaStream의 트랙을 완전히 다른의미의 <track> 요소와 헷갈리면 안됩니다.)

아마도 MediaStream을 이해하는 가장 쉬운 방법은 실제로 눈으로 보는 것일 겁니다:

  1. Chrome 또는 Opera로 데모페이지 simpl.info/gum를 엽니다.
  2. 콘솔을 엽니다.
  3. 글로벌 스코프에 위치한 스트림 변수를 검사합니다.

각 MediaStream은 navigator.getUserMedia()에서 생성된 입력과, <video> 태그 또는 RTCPeerConnection로 넘겨주는 출력을 가지고 있습니다.

getUserMedia() 함수는 3개의 매개 변수를 받습니다:

  • 제약 오브젝트.
  • 성공시, MediaStream을 넘겨 받는 콜백.
  • 실패시, 애러 오브젝트를 넘겨 받는 콜백.

각 MediaStream은 'Xk7EuLhsuHKbnjLWkW4yYGNJJ8ONsgwHBvLQ'과 같은 라벨을 가집니다. getAudioTracks() 과 getVideoTracks()함수는 MediaStreamTracks의 배열을 반환합니다.

simpl.info/gum 예제에서, (오디오가 없기때문에) stream.getAudioTracks()은 빈 배열을 반한다. 그리고 웹캠이 연결되어있다고 가정하면,stream.getVideoTracks()은 웹캠 스트림을 나타내는 MediaStreamTrack 하나의 배열을 반환합니다. 각 MediaStreamTrack은 (비디오 또는 오디오) 종류와 ('FaceTime HD Camera (Built-in)'과 같은) 라벨을 가지고, 오디오 또는 비디오의 하나 이상의 채널을 나타냅니다. 이런 경우는, 비디오 트랙 하나가 있고 오디오가 없는 경우이지만, 여러 사용처를 쉽게 상상할 수 있습니다: 예를 들어, 전면/후면 카메라, 마이크와 화면 공유 프로그램으로 부터 스트림들을 얻는 채팅 프로그램.

Chrome 또는 Opera에서는 URL.createObjectURL() 함수가 MediaStream을 video 요소의 src에 설정 가능한 Blob URL로 변경합니다. (Firefox와 Opera에서는, video의 src에 스트림 자체를 설정할 수 있습니다. ) 25버전 이후부터, Chrome은getUserMedia에서 얻은 오디오 데이터를 audio 또는 video 태그에 넘겨줄 수 있습니다.(그러나 이경우에는 기본적으로 미디어 요소는 묵음 상태임을 알아야 합니다.)

getUserMedia 또한 Web Audio API의 입력노드로 사용될 수 있습니다:

function gotStream(stream) {
    window.AudioContext = window.AudioContext || window.webkitAudioContext;
    var audioContext = new AudioContext();

    // Create an AudioNode from the stream
    var mediaStreamSource = audioContext.createMediaStreamSource(stream);

    // Connect it to destination to hear yourself
    // or any other node for processing!
    mediaStreamSource.connect(audioContext.destination);
}

navigator.getUserMedia({audio:true}, gotStream);

크로미엄 기반의 앱과 확장 프로그램에도 getUserMedia가 포함되어있습니다.audioCapture 와 videoCapture 권한들을 manifest에 추가하면 설치시 단한번의 요청을 수락하는 것으로 권한을 활성화 시킵니다. 그 이후에는 카메라와 마이크의 접속 권한에 대하여 묻지않습니다.

비슷하게, 페이지에서 HTTPS를 사용하는 경우: (적어도 Chrome에서는)getUserMedia()에 대한 권한을 한번만 혀용하면 됩니다. 처음에는 브라우저의 상태바에 '항상 허용' 버튼이 표시됩니다.

결국 의도는 카메라와 마이크 뿐만 아니라 모든 스트리밍 데이터 소스에 대한 MediaStream을 활성화하는 것입니다. 이것은 디스크로부터 스트리밍을 활성화하거나, 센서들 또는 다름 입력기기와 같은 임의의 데이터 소스들로부터 스트리밍을 활성화하는 것입니다.

getUserMedia()는 반드시 로컬 파일 시스템이 아닌 서버에서 사용되어야하며, 이외의 경우에는 PERMISSION_DENIED: 1 에러가 발생한다는 점을 숙지해야합니다.

getUserMedia()은 다른 Javascript API, 라이브러리들과 함께 우리앞에 바짝 다가와 있습니다:

  • Webcam Toy 은 WebGL을 사용한 포토부스 앱으로, 특이하고 아름다운 효과들이 적용된 사진을 공유하거나 로컬에 저장할 수 있습니다.
  • FaceKat 은 headtrackr.js를 사용하여 '얼굴 추적'게임을 만들었습니다.
  • ASCII Camera 는 Canvas API 를 사용하여 ASCII 이미지를 생성합니다.

 


gUM ASCII art!

Constraints

Constraints Chrome 24버전, Opera 18버전 이후 부터 구현이 되어있습니다. 이것은 getUserMedia() 와 RTCPeerConnection addStream() 호출의 video 해상도를 위한 값들을 설정하기 위해 사용할 수 있습니다. 목표는 applyConstraints() 함수와 함께 화면비율, 화면모드(앞 또는 뒤 카메라), 프레임 속도, 높이와 너비와 같은다른 constraints를 지원하도록 구현하는 것입니다.

simpl.info/getusermedia/constraints에 예제가 있습니다.

One gotcha: getUserMedia 의 constraints를 한 탭에서 설정을 하면 이후 열리는 탭들에도 적용이됩니다. 값을 '거부'로 설정하게되면 애러 메세지를 얻게됩니다:

navigator.getUserMedia error: NavigatorUserMediaError {code: 1, PERMISSION_DENIED: 1}Screen and tab capture

화면 캡쳐 역시 MediaStream처럼 사용할 수 있습니다. 이 기능은 현재 이 데모 처럼 experimental chromeMediaSource constraint가 설정된 크롬에서만 동작하고 있습니다. 이 기능은 Opera에서는 아직 지원하지 않습니다. 그리고 화면 캡쳐기능은 HTTPS상에서만 동작합니다.

크롬앱에서 또한 experimental chrome.tabCapture API를 통해서 단일 탭의 화면을 라이브 '비디오'로 공유가 가능합니다. 화면 공유에 관한 코드와 더 많은 정보를 확인하려면 HTML5Rocks의 Screensharing with WebRTC을 참고하시기 바랍니다. 이기능은 Opera에서는 아직 지원되지 않습니다.

Signaling: session control, network and media information

WebRTC (피어들로 불리우는) 브라우저들 사이에 스트리밍 데이터를 주고 받는 RTCPeerConnection를 사용합니다, 그리고 또한 통신을 조율하고 조장할 메세지를 주고 받기 위해 Signaling으로 알려진 일련의 과정이 필요합니다. Signaling을 위한 방법들과 프로토콜들은 WebRTC에는 명세되어있지 않습니다 : Signaling은 RTCPeerConnection API에 포함되지 않습니다.

대신, WebRTC 개발자들은 SIP,XMPP 도는 적절한 쌍방통신 채널 등 자신들에게 편한 방식을 선택할 수 있습니다. apprtc.appspot.com 예제는 Signaling 방식으로 XHR과 Channel API를 사용하였습니다. codelab은 Node server위에 Socket.io를 이용해서 만들여졌습니다.

Signaling은 3가지 종류의 정보를 교환합니다.

  • Session control messages: 통신의 초기화, 종료 그리고 애러 리포트를 위해.
  • Network configuration: 외부 세상으로 내 컴퓨터의 IP 주소와 Port는 어떻게되지 ?
  • Media capabilities: 내 브라우저와 상대브라우저가 사용 가능한 코덱들과 해상도들은 어떻게 되지?

Signaling을 통한 정보 교환은 p2p streaming이 시작되기 전에 반드시 성공적으로 완료되어야 합니다.

예를들어 Alice가 Bob과 통신하기를 원한다고 상상해봅시다. Signaling 과정을 보여주는 WebRTC W3C Working Draft에 따라 예제 코드는 아래와 같습니다. 이 코드는 이미 특정 Signaling 방식이 존재한다고 가정하에 createSignalingChannel() 함수를 만들었습니다. 또한 Chrome과 Opera에서는 RTCPeerConnection가 prefix를 가지고 있다는 점을 유의해야합니다.

var signalingChannel = createSignalingChannel();
var pc;
var configuration = ...;

// run start(true) to initiate a call
function start(isCaller) {
    pc = new RTCPeerConnection(configuration);

    // send any ice candidates to the other peer
    pc.onicecandidate = function (evt) {
        signalingChannel.send(JSON.stringify({ "candidate": evt.candidate }));
    };

    // once remote stream arrives, show it in the remote video element
    pc.onaddstream = function (evt) {
        remoteView.src = URL.createObjectURL(evt.stream);
    };

    // get the local stream, show it in the local video element and send it
    navigator.getUserMedia({ "audio": true, "video": true }, function (stream) {
        selfView.src = URL.createObjectURL(stream);
        pc.addStream(stream);

        if (isCaller)
            pc.createOffer(gotDescription);
        else
            pc.createAnswer(pc.remoteDescription, gotDescription);

        function gotDescription(desc) {
            pc.setLocalDescription(desc);
            signalingChannel.send(JSON.stringify({ "sdp": desc }));
        }
    });
}

signalingChannel.onmessage = function (evt) {
    if (!pc)
        start(false);

    var signal = JSON.parse(evt.data);
    if (signal.sdp)
        pc.setRemoteDescription(new RTCSessionDescription(signal.sdp));
    else
        pc.addIceCandidate(new RTCIceCandidate(signal.candidate));
};

우선 Alice와 Bob은 네트워크 정보를 교환합니다. ('finding candidates'란 표현은ICE framework을 사용해 네트워크 인터페이스와 포트를 찾는 과정을 뜻합니다.)

  1. Alice가 onicecandidate 핸들러를 가진 RTCPeerConnection object 를 생성합니다.
  2. 핸들러는 네트워크 candidates가 가능해지면 실행됩니다.
  3. Alice는 serialized된 candidate data를 WebSocket 또는 어떤 특정 방식등 그 들만의 Signaling 채널을 통해 Bob에게 전송합니다.
  4. Bob이 Alice로부터 candidate 메세지를 받으면 Bob은 상대방 peer description을 위한 candidate를 추가하기위해 addIceCandidate를 호출합니다.

(Alice와 Bob 같은 피어들로 불리우는) WebRTC 클라이언트들은 해상도와 코덱 기능 같은 미디어 정보들을 서로 교환하고 확인해야합니다. 미디어 정보의 교환을 위한 Signaling은 Session Description Protocol (SDP)를 사용하는 offer와 answer를 통해서 진행됩니다:

  1. Alice가 RTCPeerConnection의 createOffer() 함수를 호출합니다. 이 함수의 argument로 입력된 callback을 통해 Alice의 Session description 정보를 나타내는 RTCSessionDescription을 얻습니다.
  2. callback 안에서 Alice는 자신의 description을 setLocalDescription() 함수로 설정하고 이 session description을 signaling 채널을 통해 Bob에게 전달합니다. setLocalDescription()가 호출되기 전까지는 candidates 수집이 시작되지 않는 다는 점을 주의해야 합니다: 이것은 JSEP IETF draft에 문서화 되어있습니다.
  3. Bob은 Alice가 보낸 session description 정보를 setRemoteDescription()함수를 통해 설정합니다.
  4. Bob은 RTCPeerConnection의 createAnswer() 함수에 Alice로 부터 받은 remote description을 전달하고 실행합니다. 그러면 그녀의 정보를 기반으로 Local Session이 생성됩니다. createAnswer()의 callback은 RTCSessionDescription을 전달해줍니다: Bob은 이것을 local description으로 설정하고 Alice에게 전달합니다.
  5. Alice가 Bob의 Session description을 받으면 setRemoteDescription을 이용해 remote descirption으로 설정합니다.
  6. Ping!

RTCSessionDescription 객채들은 Session Description Protocol, SDP를 따르는 blob입니다. 직렬화된 SDP 객체는 다음과 같이 보입니다:

v=0
o=- 3883943731 1 IN IP4 127.0.0.1
s=
t=0 0
a=group:BUNDLE audio video
m=audio 1 RTP/SAVPF 103 104 0 8 106 105 13 126

// ...

a=ssrc:2223794119 label:H4fjnMzxy3dPIgQ7HxuCTLb4wLLLeRHnFxh810

네트워크와 미디어 정보의 교환과 획득은 동시에 완료됩니다, 그러나 이 두가지 과정들은 피어간의 오디오/비디오 스트리밍 시작전에 반드시 완료 되어야합니다.

위에서 설명된 offer/answer 아키텍쳐는 JSEP, JavaScript Session Establishment Protocol 이라고 불리웁니다. (Ericsson's demo video은 Ericsson에서 첫번째 WebRTC 구현할 당시 signaling과 streaming 과정을 아주 훌륭하게 설명한 애니메이션 입니다.)

 


JSEP architecture

일단 signaling 과정이 성공적으로 마치면 data는 peer와 peer 끼리 또는 송신자와 수신자가 직접 주고 받게됩니다.—만약 이것이 실패하게되면, 중계서버를 통하게 됩니다.(뒤에 자세하게 다루겠습니다.) 스트리밍이 RTCPeerConnection의 역할입니다.

RTCPeerConnection

RTCPeerConnection은 Peer들 간의 데이터를 안정적이고 효율적으로 통신하게 처리하는 WebRTC 컴포넌트입니다.

아래는 RTCPeerConnection의 역할을 보여주는 WebRTC 아키텍쳐 다이어그램입니다. 보시는 것처럼 녹색 부분들은 복잡합니다!

 


WebRTC architecture (from webrtc.org)

JavaScript 측면에서 보면, 이 다이어그램을 통해 알 수 있는 중요한 점은 RTCPeerConnection이 뒤에 숨겨진 매우 복잡한 것들을 웹개발자로부터 지켜주고 있다는 것 입니다. WebRTC에서 사용되는 코덱들과 프로토콜들은 불안정한 네트워크 위에서도 실시간 통신이 가능하게 하기위해 엄청난 양의 일들을 하고 있습니다:

  • packet loss concealment
  • echo cancellation
  • bandwidth adaptivity
  • dynamic jitter buffering
  • automatic gain control
  • noise reduction and suppression
  • image 'cleaning'.

위에 나온 W3C 코드는 Signaling 측면에서 WebRTC의 간소화된 예를 보여줍니다. 다음은 현재 동작하고 있는 두가지 WebRTC 어플리케이션 입니다: 첫 번째는 단순한 RTCPeerConnection의 데모 예제이고, 두번째는 온전히 동작하는 화상채팅 클라이언트 입니다.

RTCPeerConnection without servers

다음의 코드는 '단일페이지' WebRTC 데모 - webrtc-demos.appspot.com에서 발췌한 것입니다, 이것은 로컬과 원격지의 RTCPeerConnection (그리고 로컬과 원격지의 비디오)가 같은 웹페이지 위에 있습니다. 이 예제안에는 유용한 것들은 없습니다, 하지만 RTCPeerConnection 간의 중간 signaling 과정을 사용하지 않고 직접 페이지에서 데이터와 메세지를 주고 받기 때문에 RTCPeerConnection API의 동작을 좀 더 단순하게 보여줍니다.

One gotcha: RTCPeerConnection()의 생성자에서 두번째 파라메터로 사용되는 constraints는 getUserMedia()에서 사용되는 것과는 차이가 있습니다:w3.org/TR/webrtc/#constraints에서 더 많은 정보를 확인바랍니다.

이 예제에서, pc1은 local 피어(Caller)를 나타내고, pc2는 원격지 peer(Callee)를 나타냅니다.

Caller

  1. RTCPeerConnection을 새로 생성하고 getUserMedia()로 부터 스트림을 얻습니다:// servers is an optional config file (see TURN and STUN discussion below) pc1 = new webkitRTCPeerConnection(servers); // ... pc1.addStream(localstream);
  2. offer를 생성하고 pc1의 description을 설정합니다. 그리고 pc2의 것을 remote description을 설정합니다. 여기서는 Caller와 Callee가 같은 페이지에 있기 때문에 signaling없이 직접 코드로 연결할 수 있습니다:pc1.createOffer(gotDescription1); //... function gotDescription1(desc){ pc1.setLocalDescription(desc); trace("Offer from pc1 \n" + desc.sdp); pc2.setRemoteDescription(desc); pc2.createAnswer(gotDescription2); }

Callee

  1. pc2를 생성하고, pc1에 스트림이 추가되면 그것을 video 요소에 표시합니다:pc2 = new webkitRTCPeerConnection(servers); pc2.onaddstream = gotRemoteStream; //... function gotRemoteStream(e){ vid2.src = URL.createObjectURL(e.stream); }

RTCPeerConnection plus servers

현실 세계에서는 WebRTC는 서버가 필요하지만 단순합니다. 그래서 다음과 같은 것이 가능합니다:

  • 사용자들은 상대방을 발견하고 이름과 같은 '현실 세계'의 상세를 교환합니다.
  • WebRTC 클라이언트 어플리케이션들(피어들)은 네트워크 정보를 교환합니다.
  • 피어들은 비디오 포맷과 해상도 같은 미디어에 관한 데이터를 교환합니다.
  • WebRTC 클라이언트 어플리케이션들은 NAT gateways와 방화벽을 넘나듭니다.

다른 말로, WebRTC는 4가지 종류의 서버측 기능들이 필요합니다:

  • 사용자 탐색과 통신.
  • Signaling.
  • NAT/firewall 탐색.
  • P2P 실패시의 중계서버들.

NAT 탐색, peer-to-peer 네트워크, 사용자 탐색과 Signaling의 서버앱을 만들기 위한 요구사항들은 본문에서는 다루지 않습니다. STUN 프로토콜과 그 확장인 TURN은 NAT 탐색과 다른 네트워크 문제들을 처리하기위해 RTCPeerConnection을 사용 가능하게 하는 ICE framework를 사용한다는 설명으로 충분합니다.

ICE 비디오 채팅 클라이언트 같은 피어들을 연결하기위한 프레임워크입니다. 우선, ICE는 가장 대기시간이 적은 UDP를 통해 피어들 끼리 directly 연결 가능한지 시도합니다. 여기에서 STUN 서버들은 한가지일을 합니다: NAT 뒤에 있는 피어들이 연결 가능하도록 자신들의 공용(public)주소와 포트를 찾아줍니다. (Google은 몇개의 STUN 서버를 가지고 있고,the apprtc.appspot.com 예제에서 그 중 하나를 사용중입니다.)

 


Finding connection candidates

만약 UDP가 실패하면, ICE는 HTTP상의 TCP로 시도를 하고 그 다음엔 HTTPS상에서 시도합니다. 만약 직접 연결이 실패하면 —간혹 NAT와 방화벽으로 인해—ICE는 중계를 위해 TURN 서버를 사용합니다. 다시말해, ICE는 ICE는 처음에는 피어들간에 직접연결을 위해 UDP를 이용한 STUN서버를 사용하다가 실패하면 TURN 중계 서버를 통합니다. 'finding candidates'란 표현은 네트워크 장치와 포트들을 찾는 과정을 의미합니다.

 


WebRTC data pathways

WebRTC 개발자 Justin Uberti는 2013 Google I/O WebRTC presentation에서 ICE, STUN 그리고 TURN에 관한 더 많은 정보를 알려주었습니다. (발표 슬라이드 TURN과 STUN 서버 구현 예제들을 보여줍니다.)

A simple video chat client

뒤에 나오는 signaling 매커니즘은 apprtc.appspot.com에서 사용된 것 입니다.

만약 여기서 이해할 수 없는 부분이 있다면, WebRTC codelab을 참고하는 것이 더 좋을 것입니다. 이 단계별 가이드는 어떻게 화상 채팅 어플리케이션을 만드는지 설명합니다, 그리고 Socket.io를 사용해서 Node server에서 구현한 Signaling 서버도 포함되어 있습니다.

WebRTC를 체험해보기 좋은 곳은 signaling도 구현되었고 STUN을 사용해 NAT/방화멱 탐색까지 구현된 apprtc.appspot.com입니다. 이 어플리케이션에서는 RTCPeerConnection과 getUserMedia()의 브라우저별 구현 차이를 처리하기 위해adapter.js를 사용 했습니다.

코드에서는 의도적으로 장황하게 로그를 남겼습니다: 이벤트들의 순서를 이해하기위해서는 콘솔을 확인하시기 바랍니다. 뒤에 자세한 코드 예제들을 보여드리겠습니다.

What's going on?

데모는 initalize() 함수를 시작으로 실행됩니다:

function initialize() {
    console.log("Initializing; room=99688636.");
    card = document.getElementById("card");
    localVideo = document.getElementById("localVideo");
    miniVideo = document.getElementById("miniVideo");
    remoteVideo = document.getElementById("remoteVideo");
    resetStatus();
    openChannel('AHRlWrqvgCpvbd9B-Gl5vZ2F1BlpwFv0xBUwRgLF/* ...*/');
    doGetUserMedia();
  }

openChannel()에서 사용되는 room 값과 token은 구글 앱엔진에서 제공되는 것들 입니다: 어떤 값들이 추가되었는지를 확인하기 위해서는 레포지토리 안에 있는index.html template를 확인하시기 바랍니다.

이 코드는 로컬 카메라의 (localVideo)와 원격지 카메라의 (remoteVideo)가 표시될 HTML video 요소들을 변수들을 초기화합니다. resetStatus()는 단순하게 상태 메세지를 설정합니다.

openChannel() 함수는 WebRTC 클라이언트 간의 메세지 환경을 구축합니다:

function openChannel(channelToken) {
  console.log("Opening channel.");
  var channel = new goog.appengine.Channel(channelToken);
  var handler = {
    'onopen': onChannelOpened,
    'onmessage': onChannelMessage,
    'onerror': onChannelError,
    'onclose': onChannelClosed
  };
  socket = channel.open(handler);
}

이 데모에서는 signaling을 위해 polling 없이 JavaScript 클라이언트 간에 메세징이 가능한 Google App Engine의 Channel API를 사용합니다. (WebRTC Signaling에 대한 자세한 내용은 이미 위에서 설명하였습니다).

 


Architecture of the apprtc video chat application

Channel API를 통해 채널을 생성하는 방식은 다음과 같습니다:

  1. 클라이언트 A가 하나의 유니크 ID를 생성합니다.
  2. 클라이언트 A는 자신의 ID를 App Engine 앱에 넘겨주면서 채널 토큰을 요청합니다.
  3. App Engine 앱은 Channel API로 부터 채널과 클라이언트 ID에 해당하는 토큰을 요청합니다.
  4. 앱은 토큰을 클라이언트 A에게 전달합니다.
  5. 클라이언트 A는 소켓을 열고 서버에 설정된 채널로부터 메세지를 기다립니다.

 


The Google Channel API: establishing a channel

메세지 전송하는 방법은 다음과 같습니다:

  1. 클라이언트 B는 변경사항과 함께 App Engine 앱으로 POST 요청을 보냅니다.
  2. App Engine 앱은 요청을 channel로 전달합니다.
  3. 채널은 메세지를 클라이언트 A에게 전달합니다.
  4. 클라이언트 A의 onmessage 콜백함수가 호출됩니다.

 


The Google Channel API: sending a message

다시 말하지만: Signaling 메세지는 개발자가 선택한 매커니즘을 통해서 통신하게됩니다. Signaling 메커니즘은 WebRTC 명세에 포함되어 있지 않습니다. Channel API를 데모에서 사용된 방식이고, (웹소켓과 같은) 다른 방식을 사용할 수 도 있습니다.

openChannel() 함수가 호출된 이후에, initialize()에서 호출되는getUserMedia() 함수는 브라우저에서 getUserMedia API가 지원되는지 확인합니다. (getUserMedia에 대한 자세한 내용은 HTML5 Rocks에서 확인 바랍니다.) 지원이 된다면, onUserMediaSuccess 콜백이 호출됩니다:

function onUserMediaSuccess(stream) {
  console.log("User has granted access to local media.");
  // Call the polyfill wrapper to attach the media stream to this element.
  attachMediaStream(localVideo, stream);
  localVideo.style.opacity = 1;
  localStream = stream;
  // Caller creates PeerConnection.
  if (initiator) maybeStart();
}

이를 통해 로컬 카메라로 부터 받은 비디오를 localVideo에 표시하게 됩니다, 이때 카메라의 데이터 스트림으로 부터 object (Blob) URL가 만들어지고 요소의 src에 URL을 설정합니다. (createObjectURL이 이때 사용되어져 '메모리상'에 있는 바이너리 리소스,즉, 비디오를 위한 LocalDataStream의 URI를 얻게됩니다.) 데이터 스트림은 또한 localStream 변수에 설정되어 이후 원격지 사용자를 위해 사용할 수 있게 됩니다.

이 시점에, initiator의 값에 1이 설정됩니다. (그리고 이 값은 Caller의 세션이 종료될 때까지 유지됩니다.) 그래서 maybeStart()가 호출됩니다:

function maybeStart() {
  if (!started && localStream && channelReady) {
    // ...
    createPeerConnection();
    // ...
    pc.addStream(localStream);
    started = true;
    // Caller initiates offer to peer.
    if (initiator)
      doCall();
  }
}

이 함수는 비동기 다중 콜백들을 처리할때 편리합니다: maybeStart()은 여러 함수들 중 하나로 부터 호출될 것이지만, 코드는 localStream이 정의되고channelReady의 값이 true가 되고 통신이 아직 시작되지 않은 경우에만 실행됩니다. 그래서—만약 연결이 이미 되었고, 로컬 스트림이 사용가능하고, Signaling을 위한 채널이 준비가 되었다면, 연결이 생성되고 로컬 비디오 스트림이 전달됩니다. 이것이 한번 진행되면, started는 true가 되어, 연결은 더이상 시작되지 않습니다.

RTCPeerConnection: making a call

maybeStart()에서 호출된createPeerConnection()이 진짜 액션의 시작입니다:

function createPeerConnection() {
  var pc_config = {"iceServers": [{"url": "stun:stun.l.google.com:19302"}]};
  try {
    // Create an RTCPeerConnection via the polyfill (adapter.js).
    pc = new RTCPeerConnection(pc_config);
    pc.onicecandidate = onIceCandidate;
    console.log("Created RTCPeerConnnection with config:\n" + "  \"" +
      JSON.stringify(pc_config) + "\".");
  } catch (e) {
    console.log("Failed to create PeerConnection, exception: " + e.message);
    alert("Cannot create RTCPeerConnection object; WebRTC is not supported by this browser.");
      return;
  }

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

다음은 onIceCandidate() 콜백을 통해 STUN 서버를 사용한 연결을 설정하기 위한 것입니다.(위에서 설명한 ICE, STUN ,'candidate'을 참고하시기 바랍니다). 핸들러들은 RTCPeerConnection에서 발생하는 이벤트들을 위해 설정됩니다: 세션이 연결되고 열렸을때, 그리고 원격지 스트림이 추가되고 삭제되었을때. 사실 이 예제에서의 핸들러들은 단순히 로그 메세지만 출력하고 있습니다.—remoteVideo의 소스를 설정하는 onRemoteStreamAdded()만 제외하고:

function onRemoteStreamAdded(event) {
  // ...
  miniVideo.src = localVideo.src;
  attachMediaStream(remoteVideo, event.stream);
  remoteStream = event.stream;
  waitForRemoteVideo();
}

maybeStart()에서 createPeerConnection()이 한번 호출이 되면, 연결이 생성되어 callee에게 전달이 됩니다:

function doCall() {
  console.log("Sending offer to peer.");
  pc.createOffer(setLocalAndSendMessage, null, mediaConstraints);
}

offer가 생성되는 과정은 위에서나온 no-signaling 예제와 비슷하지만 추가적으로 offer를 위해 serialized된 SessionDescription을 원격지 피어에게 메세지로 전달 합니다. 이 과정은 setLocalAndSendMessage()를 통해 진행됩니다:

function setLocalAndSendMessage(sessionDescription) {
  // Set Opus as the preferred codec in SDP if Opus is present.
  sessionDescription.sdp = preferOpus(sessionDescription.sdp);
  pc.setLocalDescription(sessionDescription);
  sendMessage(sessionDescription);
}

 

Signaling with the Channel API

onIceCandidate() createPeerConnection()에서 성공적으로 RTCPeerConnection이 생성되었을 떄 호출되는 onIceCandidate() 콜백은 candidates에 관해 '획득한' 정보를 전달합니다:

function onIceCandidate(event) {
    if (event.candidate) {
      sendMessage({type: 'candidate',
        label: event.candidate.sdpMLineIndex,
        id: event.candidate.sdpMid,
        candidate: event.candidate.candidate});
    } else {
      console.log("End of candidates.");
    }
  }

클라이언트에서 서버로 송출되는 메세지는 XHR을 이용한 sendMessage()가 처리합니다:

function sendMessage(message) {
  var msgString = JSON.stringify(message);
  console.log('C->S: ' + msgString);
  path = '/message?r=99688636' + '&u=92246248';
  var xhr = new XMLHttpRequest();
  xhr.open('POST', path, true);
  xhr.send(msgString);
}

XHR은 클라이언트에서 서버로 signaling 메세지들을 잘 전달합니다, 하지만 일부 메커니즘은 서버에서 클라이언트로의 메세지처리가 필요합니다: 이 어플리케이션에서는 Google App Engine Channel API를 사용합니다. API(즉, App Engine Server)로 부터 받은 메세지는 processSignalingMessage()에서 처리됩니다:

function processSignalingMessage(message) {
  var msg = JSON.parse(message);

  if (msg.type === 'offer') {
    // Callee creates PeerConnection
    if (!initiator && !started)
      maybeStart();

    pc.setRemoteDescription(new RTCSessionDescription(msg));
    doAnswer();
  } else if (msg.type === 'answer' && started) {
    pc.setRemoteDescription(new RTCSessionDescription(msg));
  } else if (msg.type === 'candidate' && started) {
    var candidate = new RTCIceCandidate({sdpMLineIndex:msg.label,
                                         candidate:msg.candidate});
    pc.addIceCandidate(candidate);
  } else if (msg.type === 'bye' && started) {
    onRemoteHangup();
  }
}

만약 피어로부터 (offer에 대한 응답으로) 온 메세지가 answer 라면, RTCPeerConnection은 원격지 SessionDescription을 설정하고 통신을 시작합니다. 만약 메세지가 (즉 callee로 부터온) offer라면, RTCPeerConnection는 원격지 SessionDescription을 설정하고 the callee에게 answer를 보냅니다. 그리고 RTCPeerConnection startIce()함수를 호출하여 연결을 시작합니다:

function doAnswer() {
  console.log("Sending answer to peer.");
  pc.createAnswer(setLocalAndSendMessage, null, mediaConstraints);
}

이게 끝입니다! caller와 callee는 서로를 발견하고 자신들의 기능에 대한 정보를 교환하고, 통화 세션을 초기화합니다. 그러면 실시간 데이터 통신이 시작될 수 있습니다.

Network topologies

WebRTC는 재 일대일 통신에 대한 구현만 되어있습니다, 하지만 좀 더 복잡한 네트워크 시나리오 상에서 사용될 수 있습니다: 예를 들어 여러개의 피어들이 각각 P2P로 직접 통신할 수 있고 , 또는 대량의 참가자들을 처리할 수 있는 Multipoint Control Unit (MCU) 서버를 사용하여 선택적인 스트림을 전달하고, 오디오와 비디오를 믹싱 또는 녹화할 수도 있습니다:

 


Multipoint Control Unit topology example

존재하는 많은 WebRTC앱들은 단지 웹 브라우저간의 통신만 보여주고 있습니다, 하지만 게이트웨이 서버들도 브라우저 상에 WebRTC 앱을 실행시켜 전화기 (PSTN으로 불리우는) 장비들 또는 VOIP 시스템들과 동작할 수 있습니다. 2012년 5월에는, sipml5 SIP client를 오픈 소스화한 Doubango Telecom에서 WebRTC와 (다른 용도로도 사용가능한) WebSocket을 이용해 iOS와 Android에서 실행되는 앱과 브라우저들간에 화상통화가 가능하게 했습니다. Google I/O에서, Tethr와 Tropo는 WebRTC를 통해 피쳐폰과 컴퓨터가 통신할 수 있게 해주는 OpenBTS cell를 사용해 '간단하게' a framework for disaster communications를 전시하였습니다. 캐리어 없는 전화 통신!

 


Tethr/Tropo: disaster communications in a briefcase

RTCDataChannel

오디오와 비디오처럼, WebRTC는 실시간으로 다른 형태의 데이터 통신도 지원합니다.

RTCDataChannel API는 피어와 피어간 임의의 데이터 교환을 빠른 반응속도와 높은 처리량으로 가능하게 합니다. 여기 단순한 '단일 페이지' 데모 simpl.info/dc가 있습니다.

이 API에 대한 많은 잠재적 사용 예가 있습니다:

  • 게임
  • 원격 데스크탑 어플리케이션
  • 실시간 문자 채팅
  • 파일 전송
  • 분산 네트워크

이 API는 RTCPeerConnection의 대부분의 기능들을 활용하여 강력하고 유연한 P2P통신을 가능하게 하는 몇가지 기능을 가지고 있습니다:

  • RTCPeerConnection 세션 설정의 레버리징.
  • 우선순위가 있는 여러개의 동시 채널
  • 신뢰/비신뢰 전달 시멘틱.
  • 빌트인 보안(DTLS) 과 혼잡 제어.
  • 오디오 또는 비디오 유/무 사용.

문법은 send() 함수와 message 이벤트를 사용하는 것이 WebSocket과 거의 유사합니다:

var pc = new webkitRTCPeerConnection(servers, {optional: [{RtpDataChannels: true}]}); pc.ondatachannel = function(event) { receiveChannel = event.channel; receiveChannel.onmessage = function(event){ document.querySelector("div#receive").innerHTML = event.data; }; }; sendChannel = pc.createDataChannel("sendDataChannel", {reliable: false}); document.querySelector("button#send").onclick = function (){ var data = document.querySelector("textarea#send").value; sendChannel.send(data); };

통신은 브라우저간 직접 연결됩니다 , 그래서 RTCDataChannel은 WebSocket보다 매우 빠릅니다. 심지어 방화벽과 NAT의 방해로 '구멍내기'가 실패하여 중계(TURN) 서버와 연결이 되더라도 빠릅니다.

RTCDataChannel은 Chrome과 Opera, Firefox에서 가능합니다. 멋진 Cube Slam게임에서는 게임 상태 통신을 위해 이 API를 사용하였습니다: 친구 또는 곰과 플레이 해봅시다! Sharefest enables file sharing via RTCDataChannel, and peerCDNoffers a glimpse of how WebRTC could enable peer-to-peer content distribution.

RTCDataChannel에 관한 더 자세한 내용은 IETF의 draft protocol spec문서를 참고하기 바랍니다.

Security

실시간 통신 어플리케이션이나 플러그인에서 보안 문제가 발생하는 경우가 몇가지가 있습니다. 예를 들면:

  • 암호화 되지 않은 미디어나 데이터는 브라우저간 사이에서 또는 브라우저와 서버 사이에서 도둑질 당할수 있습니다.
  • 어떤 어플리케이션은 상대방이 모르는 사이에 비디오 또는 오디오가 녹화되거나 공유될 수 있습니다.
  • 말웨어나 바이러스가 플러그인 또는 어플리케이션안에 숨어 설치될 수 있습니다.

WebRTC는 이 문제들을 피할 수 있는 몇가지 기능들을 가지고 있습니다:

  • WebRTC는 DTLS와 SRTP등의 보안 프로토콜을 사용하여 구현되었습니다.
  • 암호화는 Signlaing 메커니즘을 포함한 모든 WebRTC Components의 필수 조건입니다.
  • WebRTC는 플러그인이 아닙니다: 컴포넌트들은 브라우저의 샌드박스위에서 실행되고 별도의 프로세스로 나눠지지 않습니다. 컴포넌트들은 별도의 설치가 필요하지 않고 브라우저 없데이트시에 업데이트 됩니다.
  • 카메라와 마이크에 접근은 반드시 허가를 통합니다. 그리고 카메라와 마이크가 사용 중인 경우에는, UI를 통해 명확하게 알려줍니다.

보안에 관한 전체적인 논의는 본문의 범위에 포함되지 않습니다. 더 자세한 정보를 위해서는 IETF에서 제공한 WebRTC Security Architecture를 참고 하시기 바랍니다.

결론

WebRTC의 API들과 표준들은 대중화될 것이고 컨텐츠 생산과 통신을위한 도구를 분산시킬 수 있습니다.—전화, 게임, 영상 제작, 음악 작곡, 뉴스 수집 과 많은 다른 어플리케이션들을 위해서.

기술은 이 이상 분열되지 않을 것 입니다.

우리는 JavaScript 개발자들이 WebRTC를 사용하여 많은 것들을 구현할 것으로 바라보고 있습니다. 블로거 Phil Edholm가 쓴 것 처럼, '잠재적으로, WebRTC와 HTML5은 브라우저가 정보 분야에 줬던 변화만큼 실시간 통신 분야에 변화를 가능하게 할 것이다.'

개발자 도구들

  • 크롬에서 chrome://webrtc-internals 페이지 (또는 오페라에서opera://webrtc-internals 페이지)는 WebRTC session이 진행중일때 자세한 상태와 차트를 제공합니다:

  • chrome://webrtc-internals screenshot
  • 크로스 브라우저간의 상호동작에 대한 내용
  • adapter.js는 Google에서 관리하는 WebRTC를 위한 JavaScript 코드 입니다. 이것은 벤더들의 prefix와 브라우저간의 차이들, 명세 구현의 차이들을 추상화합니다.
  • WebRTC Signlaing 과정을 자세하게 확인하려면, apprtc.appspot.com의 콘솔로그를 확인하시기 바랍니다.
  • 만약 이 모든게 너무 많다고 느껴진다면, WebRTC framework 또는 완성된WebRTC service를 사용하는 것이 좋습니다.
  • 버그 리포트와 기능 요청은 언제나 환영합니다: Chrome 버그들은crbug.com/new 로, Opera 버그들은 bugs.opera.com/wizard/로, Firefox 버그들은 bugzilla.mozilla.org

더 볼 것들

Standards and protocols

WebRTC 지원 상황 요약

MediaStream and getUserMedia

  • Chrome desktop 18.0.1008+; Chrome for Android 29+
  • Opera 18+; Opera for Android 20+
  • Opera 12, Opera Mobile 12 (based on the Presto engine)
  • Firefox 17+

RTCPeerConnection

  • Chrome desktop 20+ (현재 'flagless', 다시말해 about:flags에 들어갈 필요없음); Chrome for Android 29+ (flagless)
  • Opera 18+ (기본으로 포함됨); Opera for Android 20+ (기본으로 포함됨)
  • Firefox 22+ (기본으로 포함됨)

RTCDataChannel

  • Chrome 25에서 실험버전, Chrome 26+에서 더 안정적이며 (Firefox와 연동됨); Chrome for Android 29+
  • 안정화 버전 (and with Firefox interoperability) in Opera 18+; Opera for Android 20+
  • Firefox 22+ (기본으로 포함됨)

WebRTC는 Internet Explorer에서 Chrome Frame을 통해 사용가능 합니다: demo screencast and links to documentation.

RTCPeerConnection를 위한 Native API들도 있습니다: documentation on webrtc.org.

 

getUserMedia같은 API들의 크로스 플랫폼 지원 관련 정보는, caniuse.com에서 참고 바랍니다.

 

 

 

=================================

=================================

=================================

 

 

출처: http://www.devpia.com/MAEUL/Contents/Detail.aspx?BoardID=69&MAEULNO=157&no=17641&page=47

[코멘트] 

2012-03-06 10:31무플방지요원

php 로는 fsockopen 으로 원래되고 있었고 

ajax XMLHttpRequest 이용해서 비슷하게 구현방식도 있고 웹 서버에서 푸쉬해서 비슷하게 

되는 comet 이라는 방식도 있음.. asp.net 으로 comet 구현해서 채팅방 만들어봤는데 그럭저럭

잘 되더라구요 

html5 에도 웹소캣 개념 있는데 브라우저가 지원 안하는게 많아서 아직까진 시기상조..

web socket 검색하면 자료 꽤 나와요

[코멘트] 

2012-03-06 10:34gura2013

http://ihelpers.x2soft.co.kr/programming/lec.php?CMD=view&IDX=239

웹은 잘모르지만 제가 알기로 php에서는 쓰레드를 지원하지않는걸로 알고있습니다 fork로 구현해야할듯한대 
아마도 아직은 불안하지않나싶습니다. 하지만 여러가지 방법으로 다양하게 시도해볼만은 하다고 생각되어집니다. 

그리고 여기는 고충상담개시판입니다.

[코멘트] 

2012-03-06 10:41kunam

아.. 죄송합니다. 질문글을 어디에다가 올려야 될지 몰라서요..
필요하다면 자삭하도록 하겠습니다.

[코멘트] 

2012-03-06 10:42어처구니

"접속한 클라이언트들의 움직임을 동시제어할 목적으로 브로드캐스팅을 하는걸로 압니다.. "
--> 꼭 이런것은 아닙니다.

[코멘트] 

2012-03-06 10:52

우희준 (hased99)  

 

flash로도 가능하죠........

[코멘트] 

2012-03-06 10:56

허준행 (TohnoKanna)  

 

flash나 silverlight 쓰는게 간단

[코멘트] 

2012-03-06 11:01

이상준 (iduser)  

 

flash가 답인듯...

[코멘트] 

2012-03-06 11:03인생적당

node.js로 가장 간단하게 할 수 있어요~~

[코멘트] 

2012-03-06 11:13

허준행 (TohnoKanna)  

 

왠 node.js (...)

[코멘트] 

2012-03-06 11:33채진아

node.js의 socket.io모듈 말씀하시는 것 같은데요

코멘트] 

2012-03-06 11:38

허준행 (TohnoKanna)  

 

그게 웹에서 소켓쓰는거랑 뭔 관계가 ㅡ.ㅡ;

[코멘트] 

2012-03-06 13:19촬스우리촬스

HTML5 의 WebSocket 으로 구현 가능 합니다.

[코멘트] 

2012-03-06 13:19인생적당

아 HTML5는 익스8에서 안되지 죄송해여~~
node.js에 socket.io 모듈이랑 express모듈 사용하면 
간단하게 웹페이지에서 웹소켓 사용가능한 시스템 구현이 가능해서
말씀드린거에요~~ 일부 브라우저에서만 동작하니
문제가 있네요~~ 다른 방법이 좋을것 같아요~~

[코멘트] 

2012-03-06 13:34촬스우리촬스

어떤 시스템을 구상하는지 구체적으로는 모르지만, 일반 client 어플 처럼 브라우져 상에 구성 하기에는 HTML5말고는 답이 별로 없습니다.
브라우져가 polling 하는 구조적인 한계를 넘을 수 없기때문입니다.

[코멘트] 

2012-03-06 13:53deathpearl

클라이언트를 제어하시겠다고요? 
샌드박스 모델(뚫리면 가능하겠지만)에서 가능한 얘기가 아닙니다.
악성코드를 심으셔야죠 ㅡㅡ;

[코멘트] 

2012-03-06 14:07kunam

제가 만들려는 프로그램은 소켓통신으로 예를 들이면 특정한 서버에 
특정 갯수의 클라이언트가 접근을 합니다. 서버와 클라이언트는 
특정 갯수의 행과 열로 이루어진 네모박스(GUI에서 라벨)들로 채워
집니다.(서버와 클라이언트 화면 동일) 그럼 특정 클라이언트에서 
버튼을 누르면 해당 명령어가 서버로 전달되고, 전달된 명령어를 
기반으로 서버에서는 이동할 좌표값을 계산하여 접속한 모든 클라이
언트로 브로드캐스팅을 합니다. 한마디로 서버와 클라이언트의 
움직임이 동시에 제어되는 겁니다.. 이걸 웹으로 구현하고 싶은데
방법을 몰라서 질문드린겁니다..

[코멘트] 

2012-03-06 14:09

허준행 (TohnoKanna)  

그걸 구현하려면 플래시를 써야한다고 위에 다 얘기 됫는데요 (...)

[코멘트] 

2012-03-06 14:22데르피언

플래쉬 이용해 보세요.

[코멘트] 

2012-03-06 15:44좋은아침

답이 이미나왔네요 ㅋ

[코멘트] 

2012-03-06 16:26

김성영 (rioksy)  

플래쉬 ㄱㄱ

[코멘트] 

2012-03-06 16:53durac

아작스나 플래쉬..@@

[코멘트] 

2012-03-06 18:10

오새롬 (diebuster)  

 

-_-

[코멘트] 

2012-03-06 19:11

권오창 (win3200)  

 

Flex 로 구현하던지 PHP에서 소켓통신 함수 지원되는걸로 알고 있음...

[코멘트] 

2012-03-06 20:02

이승우 (lswitch)  

 

답이 이미나왔네요

[코멘트] 

2012-03-07 09:49불꽃개발자

웹에서 소켓 당연히 쓰져.

[코멘트] 

2012-03-20 18:36헤더tm

node.js socket.io 모듈로 해결 가능합니다. 
HTML5 API로 클라이언트측 소켓은 제어가 가능하지만 서버측에서도 다른클라이언트들과 커넥션을 맺어줘야 합니다.

서버측 웹소켓 구현으로는 위에서 말한 socket.io모듈도 있구요.( 서버와 클라이언트 라이브러리 둘다 제공 ) 
Jetty WAS 7.4 버젼인가부터도 웹소켓통신을 구현할수 있도록 지원 하고 있습니다.

[코멘트] 

2012-03-20 18:40헤더tm

또한 윗분들이 말씀하시는 플래쉬를 이용한 소켓통신도 가능하나 어도비에서는 더이상 플래쉬 개발과정에 지원이 없다고 
기사가 났었습니다. node.js 로 구현한 socket.io 모듈을 이용하시면 웹소켓이 지원되지 않는 하위버젼 브라우져에서도 
웹소켓 통신이 가능하도록 구현되어있습니다. websocket 객체 자체가 없는 브라우져라면 ajax , commet 를 이용하여 통신이 가능하게끔
지원한다지요.

[코멘트] 

2012-03-20 18:42헤더tm

참고하시면 좋을듯한 관련기사 링크 걸어드립니다.

http://helloworld.naver.com/helloworld/1336

 

 

 

 

반응형

 

 

728x90

 

 

=================================

=================================

=================================

 

 

출처: http://cmohgate.tistory.com/12

 

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 메시지를 볼 수 있다. 구글 크롬 참 좋아요.

 

 

=================================

=================================

=================================

 

 

 

기타 관련서적

https://www.devicemart.co.kr/1111890

http://book.interpark.com/product/BookDisplay.do?_method=detail&sc.shopNo=0000400000&sc.prdNo=231124417&sc.saNo=007&bnid1=kbook&bnid2=inform&bnid3=series&bnid4=01

 

 

=================================

=================================

=================================

 

 

 

반응형


관련글 더보기

댓글 영역