WebRTC와의 실시간 통신 사용 설정

1. 시작하기 전에

이 Codelab에서는 동영상을 제작하여 웹캠으로 스냅샷을 촬영하고 WebRTC를 사용하여 P2P 이미지를 공유하는 앱을 빌드하는 방법을 설명합니다. 또한 핵심 WebRTC API를 사용하고 Node.js로 메시지 서버를 설정하는 방법도 알아봅니다.

기본 요건

  • HTML, CSS, 자바스크립트에 대한 기본 지식

빌드할 항목

  • 웹캠에서 동영상을 가져옵니다.
  • RTCPeerConnection에서 동영상을 스트리밍하세요.
  • RTCDataChannel를 사용하여 데이터를 스트리밍합니다.
  • 메시지 교환을 위한 신호 서비스를 설정합니다.
  • 피어 연결 및 신호를 결합합니다.
  • 사진을 찍고 데이터 채널을 사용하여 공유하세요.

필요한 항목

  • Chrome 47 이상
  • Chrome의 웹 서버 또는 원하는 웹 서버
  • 원하는 텍스트 편집기
  • Node.js

2. 샘플 코드 가져오기

코드 다운로드

  1. Git에 익숙하다면 다음 명령어를 실행하여 GitHub에서 이 Codelab의 코드를 클론합니다.
git clone https://github.com/googlecodelabs/webrtc-web

또는 다음 링크를 클릭하여 코드의 ZIP 파일을 다운로드합니다.

  1. 다운로드한 ZIP 파일을 열어 이 Codelab의 각 단계마다 한 개의 폴더와 필요한 모든 리소스가 포함된 webrtc-web-master라는 프로젝트 폴더를 압축 해제합니다.

모든 코드는 work라는 디렉터리에서 진행합니다.

step-nn 폴더에는 이 Codelab의 각 단계에 완성된 버전이 포함되어 있습니다. 참조할 수 있습니다.

웹 서버 설치 및 확인

자체 웹 서버를 사용해도 되지만, 이 Codelab은 Chrome용 웹 서버에서 잘 작동하도록 설계되었습니다.

  1. Chrome용 웹 서버가 없다면 다음 링크를 클릭하여 Chrome 웹 스토어에서 설치하세요.

D0a4649b4920cf3.png

  1. Chrome에 추가를 클릭하면 Chrome용 웹 서버가 설치되며 새 탭에서 Google 앱이 자동으로 열립니다.
  2. 웹 서버를 클릭합니다.

27fce4494f641883.png

로컬 웹 서버를 구성할 수 있는 대화상자가 나타납니다.

A300381a486b9e22.png

  1. 폴더 선택을 클릭합니다.
  2. 만든 work 폴더를 선택합니다.

웹 서버 URL 아래에 진행 중인 작업을 볼 수 있는 URL이 표시됩니다.

Chrome

  1. 옵션 (다시 시작해야 할 수 있음)에서 index.html 자동 표시 체크박스를 선택합니다.
  2. 웹 서버: 시작됨을 두 번 전환하여 서버를 중지하고 다시 시작합니다.

f23cafb3993dfac1.png

  1. 웹 서버 URL 아래에 있는 URL을 클릭하여 웹브라우저에서 작업을 확인합니다.

work/index.html에 해당하는 다음과 같은 페이지가 표시됩니다.

18a705cb6ccc5181.png

아직 이 앱은 아직 무언가 흥미로운 작업을 하고 있지 않습니다. 웹 서버가 제대로 작동하는지 확인하기 위한 최소한의 골격일 뿐입니다. 이후 단계에서 기능과 레이아웃 기능을 추가합니다.

3. 웹캠에서 동영상 스트리밍

이 단계의 전체 버전은 step-01 폴더에 있습니다.

대시 추가

다음 코드를 복사하여 work 디렉터리의 index.html 파일에 붙여넣어 videoscript 요소를 추가합니다.

<!DOCTYPE html>
<html>

<head>

  <title>Real-time communication with WebRTC</title>

  <link rel="stylesheet" href="css/main.css" />

</head>

<body>

  <h1>Real-time communication with WebRTC</h1>

  <video autoplay playsinline></video>

  <script src="js/main.js"></script>

</body>

</html>

자바스크립트 코드 추가

다음 코드를 복사하여 js 폴더의 main.js 파일에 붙여넣습니다.

'use strict';

// In this codelab, you  only stream video, not audio (video: true).
const mediaStreamConstraints = {
  video: true,
};

// The video element where the stream is displayed
const localVideo = document.querySelector('video');

// The local stream that's displayed on the video
let localStream;

// Handle success and add the MediaStream to the video element
function gotLocalMediaStream(mediaStream) {
  localStream = mediaStream;
  localVideo.srcObject = mediaStream;
}

// Handle error and log a message to the console with the error message
function handleLocalMediaStreamError(error) {
  console.log('navigator.getUserMedia error: ', error);
}

// Initialize media stream
navigator.mediaDevices.getUserMedia(mediaStreamConstraints)
  .then(gotLocalMediaStream).catch(handleLocalMediaStreamError);

직접 해 보기

브라우저에서 index.html 파일을 열면 다음과 같이 웹캠이 보이는 모습이 표시됩니다.

9297048e43ed0f3d.png

사용 방법

getUserMedia() 호출 후에 브라우저는 현재 출처의 카메라 액세스 요청이 처음 있을 경우 카메라 액세스 권한을 요청합니다.

성공하면 media 요소가 srcObject 속성을 통해 사용할 수 있는 MediaStream이 반환됩니다.

navigator.mediaDevices.getUserMedia(mediaStreamConstraints)
  .then(gotLocalMediaStream).catch(handleLocalMediaStreamError);


}
function gotLocalMediaStream(mediaStream) {
  localVideo.srcObject = mediaStream;
}

constraints 인수를 사용하면 가져올 미디어를 지정할 수 있습니다. 이 예에서는 미디어가 오디오만 기본적으로 사용 중지되어 있으므로 동영상만 제공합니다.

const mediaStreamConstraints = {
  video: true,
};

동영상 해상도와 같은 추가 요구사항에 따라 제약 조건을 사용할 수 있습니다.

const hdConstraints = {
  video: {
    width: {
      min: 1280
    },
    height: {
      min: 720
    }
  }
}

MediaTrackConstraints 사양에는 가능한 모든 제약조건 유형이 나열되어 있지만, 모든 옵션이 모든 브라우저에서 지원되는 것은 아닙니다. 요청된 해상도가 현재 선택된 카메라에서 지원되지 않는 경우 getUserMedia() OverconstrainedError에서 거부되며 카메라에 대한 액세스 권한을 부여하라는 메시지가 표시됩니다.

getUserMedia()에 성공하면 웹캠의 동영상 스트림이 동영상 요소의 소스로 설정됩니다.

function gotLocalMediaStream(mediaStream) {
  localVideo.srcObject = mediaStream;
}

보너스 점수

  • getUserMedia()에 전달된 localStream 객체는 전역 범위에 있으므로 브라우저 콘솔에서 객체를 검사할 수 있습니다. 콘솔을 열고 stream,를 입력한 후 Enter(Mac은 Return)를 누릅니다. Chrome에서 콘솔을 보려면 Control+Shift+J(Mac은 Command+Option+J)를 누릅니다.
  • localStream.getVideoTracks()는 무엇을 반환하나요?
  • localStream.getVideoTracks()[0].stop()를 호출합니다.
  • 제약 조건 객체를 확인합니다. {audio: true, video: true}로 변경하면 어떻게 되나요?
  • 동영상 요소의 크기는 얼마인가요? 어떻게 하면 디스플레이 크기가 아닌 자바스크립트에서 동영상의 자연스러운 크기를 얻을 수 있나요? 확인하려면 Chrome 개발자 도구를 사용하세요.
  • 동영상 요소에 다음과 같은 CSS 필터를 추가합니다.
video {
  filter: blur(4px) invert(1) opacity(0.5);
}
  • 다음과 같이 SVG 필터를 추가합니다.
video {
   filter: hue-rotate(180deg) saturate(200%);
 }

  • video 요소의 autoplay 속성을 잊지 마세요. 그러지 않으면 프레임이 하나만 표시됩니다.
  • getUserMedia() 제약 조건에는 더 많은 옵션이 있습니다. 더 많은 예시는 제약 조건 및추가 제약 조건WebRTC 샘플을 참조하세요.

권장사항

동영상 요소가 컨테이너를 오버플로하지 않는지 확인합니다. 이 Codelab에서는 widthmax-width를 추가하여 동영상의 기본 크기와 최대 크기를 설정했습니다. 브라우저에서 높이를 자동으로 계산합니다.

video {
  max-width: 100%;
  width: 320px;
}

4. RTCPeerConnection API로 동영상 스트리밍

이 단계의 전체 버전은 step-2 폴더에 있습니다.

동영상 요소 및 컨트롤 버튼 추가

index.html 파일에서 단일 video 요소를 두 개의 video 및 세 개의 button 요소로 바꿉니다.

<video id="localVideo" autoplay playsinline></video>
<video id="remoteVideo" autoplay playsinline></video>


<div>
  <button id="startButton">Start</button>
  <button id="callButton">Call</button>
  <button id="hangupButton">Hang Up</button>
</div>

동영상 요소 하나는 getUserMedia()의 스트림을, 다른 동영상 요소는 RTCPeerconnection를 통해 스트리밍되는 동일한 동영상을 보여 줍니다. 실제 앱에서는 video 요소 중 하나가 로컬 스트림을, 다른 하나는 원격 스트림을 표시합니다.

어댑터.js shim 추가하기

이 스크립트 요소를 복사하여 main.js의 스크립트 요소 위에 붙여넣습니다.

<script src="https://webrtc.github.io/adapter/adapter-latest.js"></script>

이제 index.html 파일이 다음과 같이 표시됩니다.

<!DOCTYPE html>
<html>

<head>
  <title>Real-time communication with WebRTC</title>
  <link rel="stylesheet" href="css/main.css" />
</head>

<body>
  <h1>Real-time communication with WebRTC</h1>

  <video id="localVideo" autoplay playsinline></video>
  <video id="remoteVideo" autoplay playsinline></video>

  <div>
    <button id="startButton">Start</button>
    <button id="callButton">Call</button>
    <button id="hangupButton">Hang Up</button>
  </div>

  <script src="https://webrtc.github.io/adapter/adapter-latest.js"></script>
  <script src="js/main.js"></script>
</body>
</html>

RTCPeerConnection 코드 설치

main.jsstep-02 폴더의 버전으로 바꿉니다.

전화 걸기

  1. index.html 파일을 엽니다.
  2. 시작을 클릭하여 웹캠에서 동영상을 가져옵니다.
  3. 통화를 클릭하여 피어 연결을 설정합니다.

video 요소에 웹캠의 동일한 동영상이 표시됩니다.

  1. WebRTC 로깅을 보려면 브라우저 콘솔을 확인합니다.

사용 방법

이 단계는 매우 유용합니다.

WebRTC는 RTCPeerConnection API를 사용하여 WebRTC 클라이언트(동종 피어) 간에 동영상을 스트리밍하는 연결을 설정합니다.

이 예에서 두 RTCPeerConnection 객체(pc1pc2)는 동일한 페이지에 있습니다.

WebRTC 피어 간 통화 설정에는 다음 세 가지 작업이 포함됩니다.

  1. 각 통화 종료마다 RTCPeerConnection를 만들고 각 끝에 getUserMedia()의 로컬 스트림을 추가합니다.
  2. 네트워크 정보를 가져와 공유합니다.

잠재적인 연결 엔드포인트를 ICE 후보라고 합니다.

  1. 로컬 및 원격 설명을 가져와 공유합니다.

로컬 미디어의 메타데이터는 세션 설명 프로토콜 (SDP) 형식입니다.

앨리스와 밥이 RTCPeerConnection을 사용하여 영상 채팅을 설정한다고 가정해 보겠습니다.

첫째, 앨리스와 밥이 네트워크 정보를 교환합니다. 후보자 찾기 표현식은 ICE 프레임워크를 사용하여 네트워크 인터페이스 및 포트를 찾는 프로세스를 나타냅니다.

  1. 앨리스는 onicecandidate (addEventListener('icecandidate')) 핸들러로 RTCPeerConnection 객체를 만듭니다.

main.js의 다음 코드에 해당합니다.

let localPeerConnection;
localPeerConnection = new RTCPeerConnection(servers);
localPeerConnection.addEventListener('icecandidate', handleConnection);
localPeerConnection.addEventListener(
    'iceconnectionstatechange', handleConnectionChange);
  1. Alice는 getUserMedia()를 호출하고 여기에 전달된 스트림을 추가합니다.
navigator.mediaDevices.getUserMedia(mediaStreamConstraints).
  then(gotLocalMediaStream).
  catch(handleLocalMediaStreamError);
function gotLocalMediaStream(mediaStream) {
  localVideo.srcObject = mediaStream;
  localStream = mediaStream;
  trace('Received local stream.');
  callButton.disabled = false;  // Enable call button.
}
localPeerConnection.addStream(localStream);
trace('Added local stream to localPeerConnection.');
  1. 네트워크 후보가 사용 가능해지면 1단계의 onicecandidate 핸들러가 호출됩니다.
  2. 앨리스는 직렬화된 후보 데이터를 밥에게 보냅니다.

실제 앱에서는 신호로 알려진 이 과정이 메시지 서비스를 통해 이루어집니다. 이 작업을 수행하는 방법은 이후 단계에서 알아봅니다. 물론 이 단계에서 두 RTCPeerConnection 객체는 같은 페이지에 있으며 외부 메시지 없이도 직접 통신할 수 있습니다.

  1. 앨리스는 앨리스로부터 후보 메시지를 받으면 addIceCandidate()를 호출하여 원격 피어 설명에 후보를 추가합니다.
function handleConnection(event) {
  const peerConnection = event.target;
  const iceCandidate = event.candidate;

  if (iceCandidate) {
    const newIceCandidate = new RTCIceCandidate(iceCandidate);
    const otherPeer = getOtherPeer(peerConnection);

    otherPeer.addIceCandidate(newIceCandidate)
      .then(() => {
        handleConnectionSuccess(peerConnection);
      }).catch((error) => {
        handleConnectionFailure(peerConnection, error);
      });

    trace(`${getPeerName(peerConnection)} ICE candidate:\n` +
          `${event.candidate.candidate}.`);
  }
}

또한 WebRTC 피어는 해상도 및 코덱 기능과 같은 로컬 및 원격 오디오 및 동영상 미디어 정보를 찾고 교환해야 합니다. 미디어 구성 정보를 교환하라는 신호는 SDP 형식을 사용하여 혜택과 답변이라고 알려진 메타데이터 blob 교환으로 진행됩니다.

  1. 앨리스는 RTCPeerConnection createOffer() 메서드를 실행합니다.

반환된 프라미스는 RTCSessionDescription - Alice&의 로컬 세션 설명을 제공합니다.

trace('localPeerConnection createOffer start.');
localPeerConnection.createOffer(offerOptions)
  .then(createdOffer).catch(setSessionDescriptionError);
  1. 성공하면 앨리스는 setLocalDescription()를 사용하여 로컬 설명을 설정한 후 신호 채널을 통해 이 세션 설명을 밥에게 보냅니다.
  2. 밥은 앨리스가 보낸 원격 설명으로 setRemoteDescription()를 설정합니다.
  3. 밥은 RTCPeerConnection createAnswer() 메서드를 실행한 후
  4. createAnswer() 프라미스는 RTCSessionDescription을 전달하며, 밥은 로컬 설명으로 설정하고 윤아에게 보냅니다.
  5. 앨리스가 밥의 세션 설명을 받으면 setRemoteDescription()
// Logs offer creation and sets peer connection session descriptions
function createdOffer(description) {
  trace(`Offer from localPeerConnection:\n${description.sdp}`);

  trace('localPeerConnection setLocalDescription start.');
  localPeerConnection.setLocalDescription(description)
    .then(() => {
      setLocalDescriptionSuccess(localPeerConnection);
    }).catch(setSessionDescriptionError);

  trace('remotePeerConnection setRemoteDescription start.');
  remotePeerConnection.setRemoteDescription(description)
    .then(() => {
      setRemoteDescriptionSuccess(remotePeerConnection);
    }).catch(setSessionDescriptionError);

  trace('remotePeerConnection createAnswer start.');
  remotePeerConnection.createAnswer()
    .then(createdAnswer)
    .catch(setSessionDescriptionError);
}

// Logs answer to offer creation and sets peer-connection session descriptions
function createdAnswer(description) {
  trace(`Answer from remotePeerConnection:\n${description.sdp}.`);

  trace('remotePeerConnection setLocalDescription start.');
  remotePeerConnection.setLocalDescription(description)
    .then(() => {
      setLocalDescriptionSuccess(remotePeerConnection);
    }).catch(setSessionDescriptionError);

  trace('localPeerConnection setRemoteDescription start.');
  localPeerConnection.setRemoteDescription(description)
    .then(() => {
      setRemoteDescriptionSuccess(localPeerConnection);
    }).catch(setSessionDescriptionError);
}

보너스 점수

  1. chrome://webrtc-internals로 이동합니다.

이 페이지에서는 WebRTC 통계 및 디버깅 데이터를 제공합니다. Chrome URL의 전체 목록은 chrome://about에서 확인할 수 있습니다.

  1. CSS로 페이지 스타일을 지정합니다.
  2. 동영상을 나란히 배치합니다.
  3. 버튼의 너비를 확대하여 텍스트를 확대합니다.
  4. 레이아웃이 모바일에서 작동하는지 확인하세요.
  5. Chrome 개발자 도구 콘솔에서 localStream, localPeerConnection, remotePeerConnection를 확인합니다.
  6. Console에서 localPeerConnectionpc1.localDescription를 확인합니다.

SDP 형식은 어떤 형식인가요?

  • 어댑터.js shim에 관한 자세한 내용은 adapter.js GitHub 저장소를 참조하세요.
  • WebRTC 호출을 위한 WebRTC 프로젝트인 AppRTC코드를 살펴보세요. 통화 설정 시간이 500밀리초 미만입니다.

권장사항

코드의 미래 경쟁력을 확보하려면 새로운 Promise 기반 API를 사용하고 adapter.js를 사용하여 이 API를 지원하지 않는 브라우저와의 호환성을 사용 설정하세요.

5. 데이터 채널을 사용하여 데이터 교환

이 단계의 전체 버전은 step-03 폴더에 있습니다.

HTML 업데이트

이 단계에서는 WebRTC 데이터 채널을 사용하여 동일한 페이지의 두 textarea 요소 간에 텍스트를 전송합니다. 이 기능은 그다지 유용하지 않지만 WebRTC를 사용하여 데이터를 공유하고 동영상을 스트림하는 방법을 보여줍니다.

index.html,에서 videobutton 요소를 삭제하고 다음 HTML로 바꿉니다.

<textarea id="dataChannelSend" disabled
    placeholder="Press Start, enter some text, then press Send."></textarea>
<textarea id="dataChannelReceive" disabled></textarea>

<div id="buttons">
  <button id="startButton">Start</button>
  <button id="sendButton">Send</button>
  <button id="closeButton">Stop</button>
</div>

하나는 textarea를 입력하고 다른 하나는 동종 앱 간에 스트리밍된 텍스트를 표시하는 데 사용됩니다.

이제 index.html 파일이 다음과 같이 표시됩니다.

<!DOCTYPE html>
<html>

<head>

  <title>Real-time communication with WebRTC</title>

  <link rel="stylesheet" href="css/main.css" />

</head>

<body>

  <h1>Real-time communication with WebRTC</h1>

  <textarea id="dataChannelSend" disabled
    placeholder="Press Start, enter some text, then press Send."></textarea>
  <textarea id="dataChannelReceive" disabled></textarea>

  <div id="buttons">
    <button id="startButton">Start</button>
    <button id="sendButton">Send</button>
    <button id="closeButton">Stop</button>
  </div>

  <script src="https://webrtc.github.io/adapter/adapter-latest.js"></script>
  <script src="js/main.js"></script>

</body>

</html>

자바스크립트 업데이트하기

  1. main.jsstep-03/js/main.js의 콘텐츠로 바꿉니다.
  1. 동종 앱 간에 데이터를 스트리밍해 보세요.
  2. index.html를 엽니다.
  3. 시작을 클릭하여 피어 연결을 설정합니다.
  4. 왼쪽에 있는 textarea에 텍스트를 입력합니다.
  5. Send를 클릭하여 WebRTC 데이터 채널을 사용하여 텍스트를 전송합니다.

사용 방법

이 코드는 RTCPeerConnectionRTCDataChannel를 사용하여 문자 메시지 교환을 사용 설정합니다.

이 단계의 코드 대부분이 RTCPeerConnection 예와 동일합니다. sendData()createConnection() 함수에는 대부분 새 코드가 있습니다.

function createConnection() {
  dataChannelSend.placeholder = '';
  var servers = null;
  pcConstraint = null;
  dataConstraint = null;
  trace('Using SCTP based data channels');
  // For SCTP, reliable and ordered delivery is true by default.
  // Add localConnection to global scope to make it visible
  // from the browser console.
  window.localConnection = localConnection =
      new RTCPeerConnection(servers, pcConstraint);
  trace('Created local peer connection object localConnection');

  sendChannel = localConnection.createDataChannel('sendDataChannel',
      dataConstraint);
  trace('Created send data channel');

  localConnection.onicecandidate = iceCallback1;
  sendChannel.onopen = onSendChannelStateChange;
  sendChannel.onclose = onSendChannelStateChange;

  // Add remoteConnection to global scope to make it visible
  // from the browser console.
  window.remoteConnection = remoteConnection =
      new RTCPeerConnection(servers, pcConstraint);
  trace('Created remote peer connection object remoteConnection');

  remoteConnection.onicecandidate = iceCallback2;
  remoteConnection.ondatachannel = receiveChannelCallback;

  localConnection.createOffer().then(
    gotDescription1,
    onCreateSessionDescriptionError
  );
  startButton.disabled = true;
  closeButton.disabled = false;
}

function sendData() {
  var data = dataChannelSend.value;
  sendChannel.send(data);
  trace('Sent Data: ' + data);
}

RTCDataChannel 구문은 send() 메서드와 message 이벤트가 있는 WebSocket과 의도적으로 유사합니다.

dataConstraint을 사용합니다. 성능보다는 안정적인 전송에 우선순위를 두는 등 다양한 유형의 데이터 공유를 가능하게 하도록 데이터 채널을 구성할 수 있습니다.

보너스 점수

  1. SCTP를 사용하면 WebRTC 데이터 채널에서 사용하는 프로토콜이 안정적이고 순서가 정확하며 기본적으로 사용 설정됩니다. RTCDataChannel에서 안정적인 데이터 제공을 제공해야 하는 경우 및 데이터 손실이 발생하더라도 성능이 더 중요할 수 있는 경우는 언제인가요?
  2. CSS를 사용하여 페이지 레이아웃을 개선하고 자리표시자 속성을 dataChannelReceive textarea에 추가합니다.
  3. 휴대기기에서 페이지를 테스트합니다.

자세히 알아보기

6. 메시지 교환을 위한 신호 서비스 설정하기

같은 페이지에서 동종 앱 간에 데이터를 교환하는 방법을 배웠습니다. 그런데 어떻게 다른 컴퓨터에서 데이터를 교환하나요? 먼저 메타데이터 메시지를 교환할 신호 채널을 설정해야 합니다.

이 단계의 전체 버전은 step-04 폴더에 있습니다.

앱 정보

WebRTC가 클라이언트 측 자바스크립트 API를 사용하지만 실제로 사용하려면 STUN 및 TURN 서버뿐만 아니라 신호 (메시지) 서버가 필요합니다. 자세한 내용은 여기를 참조하세요.

이 단계에서는 Socket.IO Node.js 모듈과 자바스크립트용 메시지 라이브러리를 사용해 간단한 Node.js 신호 서버를 빌드합니다.

이 예시에서 서버 (Node.js 앱)는 index.js에서 구현되고 서버에서 실행되는 클라이언트 (웹 앱)는 index.html에서 구현됩니다.

이 단계의 Node.js 앱에는 두 가지 작업이 있습니다.

먼저 메일이 릴레이 역할을 합니다.

socket.on('message', function (message) {
  log('Got message: ', message);
  socket.broadcast.emit('message', message);
});

둘째, WebRTC 영상 채팅방을 관리합니다.

if (numClients === 0) {
  socket.join(room);
  socket.emit('created', room, socket.id);
} else if (numClients === 1) {
  socket.join(room);
  socket.emit('joined', room, socket.id);
  io.sockets.in(room).emit('ready');
} else { // max two clients
  socket.emit('full', room);
}

간단한 WebRTC 앱을 사용하면 최대 2개의 동종 앱이 회의실을 공유할 수 있습니다.

HTML 및 자바스크립트

  1. index.html을 다음과 같이 업데이트합니다.
<!DOCTYPE html>
<html>

<head>

  <title>Real-time communication with WebRTC</title>

  <link rel="stylesheet" href="css/main.css" />

</head>

<body>

  <h1>Real-time communication with WebRTC</h1>

  <script src="/socket.io/socket.io.js"></script>
  <script src="js/main.js"></script>
  
</body>

</html>

이 단계에서는 페이지에 아무 것도 표시되지 않습니다. 모든 로깅은 브라우저 콘솔에서 이루어집니다. Chrome에서 콘솔을 보려면 Control+Shift+J(Mac은 Command+Option+J)를 누릅니다.

  1. js/main.js를 다음으로 바꿉니다.
'use strict';

var isInitiator;

window.room = prompt("Enter room name:");

var socket = io.connect();

if (room !== "") {
  console.log('Message from client: Asking to join room ' + room);
  socket.emit('create or join', room);
}

socket.on('created', function(room, clientId) {
  isInitiator = true;
});

socket.on('full', function(room) {
  console.log('Message from client: Room ' + room + ' is full :^(');
});

socket.on('ipaddr', function(ipaddr) {
  console.log('Message from client: Server IP address is ' + ipaddr);
});

socket.on('joined', function(room, clientId) {
  isInitiator = false;
});

socket.on('log', function(array) {
  console.log.apply(console, array);
});

Node.js에서 실행할 Socket.IO 파일 설정

HTML 파일에서 Socket.IO 파일을 사용하고 있을 수도 있습니다.

<script src="/socket.io/socket.io.js"></script>
  1. work 디렉터리의 최상위 수준에서 다음 콘텐츠가 포함된 package.json 파일을 만듭니다.
{
  "name": "webrtc-codelab",
  "version": "0.0.1",
  "description": "WebRTC codelab",
  "dependencies": {
    "node-static": "^0.7.10",
    "socket.io": "^1.2.0"
  }
}

노드 패키지 관리자 (npm)에 어떤 프로젝트를 알려 주는 앱 매니페스트입니다.

설치해야 합니다

  1. /socket.io/socket.io.js와 같은 종속 항목을 설치하려면 work 디렉터리의 명령줄 터미널에서 다음을 실행합니다.
npm install

다음과 같이 끝나는 설치 로그가 표시됩니다.

3ab06b7bcc7664b9.png

npmpackage.json에 정의된 종속 항목을 설치했음을 알 수 있습니다.

  1. js 디렉터리가 아닌 work 디렉터리의 최상위 수준에 새 파일 index.js를 만들고 다음 코드를 추가합니다.
'use strict';

var os = require('os');
var nodeStatic = require('node-static');
var http = require('http');
var socketIO = require('socket.io');

var fileServer = new(nodeStatic.Server)();
var app = http.createServer(function(req, res) {
  fileServer.serve(req, res);
}).listen(8080);

var io = socketIO.listen(app);
io.sockets.on('connection', function(socket) {

  // Convenience function to log server messages on the client
  function log() {
    var array = ['Message from server:'];
    array.push.apply(array, arguments);
    socket.emit('log', array);
  }

  socket.on('message', function(message) {
    log('Client said: ', message);
    // For a real app, would be room-only (not broadcast)
    socket.broadcast.emit('message', message);
  });

  socket.on('create or join', function(room) {
    log('Received request to create or join room ' + room);

    var clientsInRoom = io.sockets.adapter.rooms[room];
    var numClients = clientsInRoom ? Object.keys(clientsInRoom.sockets).length : 0;

    log('Room ' + room + ' now has ' + numClients + ' client(s)');

    if (numClients === 0) {
      socket.join(room);
      log('Client ID ' + socket.id + ' created room ' + room);
      socket.emit('created', room, socket.id);

    } else if (numClients === 1) {
      log('Client ID ' + socket.id + ' joined room ' + room);
      io.sockets.in(room).emit('join', room);
      socket.join(room);
      socket.emit('joined', room, socket.id);
      io.sockets.in(room).emit('ready');
    } else { // max two clients
      socket.emit('full', room);
    }
  });

  socket.on('ipaddr', function() {
    var ifaces = os.networkInterfaces();
    for (var dev in ifaces) {
      ifaces[dev].forEach(function(details) {
        if (details.family === 'IPv4' && details.address !== '127.0.0.1') {
          socket.emit('ipaddr', details.address);
        }
      });
    }
  });

});
  1. 명령줄 터미널의 work 디렉터리에서 다음 명령어를 실행합니다.
node index.js
  1. 브라우저에서 http://localhost:8080으로 이동합니다.

이 URL로 이동할 때마다 채팅방 이름을 입력하라는 메시지가 표시됩니다.

동일한 채팅방에 참여하려면 매번 같은 채팅방 이름을 입력합니다(예: foo).

  1. 새 탭을 열고 http://localhost:8080으로 다시 이동하여 동일한 채팅방 이름을 다시 입력합니다.
  2. 다른 새 탭을 열고 http://localhost:8080으로 다시 이동한 후 같은 방 이름을 다시 입력합니다.
  3. 각 탭에서 콘솔을 확인합니다.

자바스크립트의 로깅이 표시됩니다.

보너스 점수

  • 어떤 대체 메시지 메커니즘이 있을 수 있나요? 순수 WebSocket을 사용할 때 어떤 문제가 발생할 수 있나요?
  • 이 앱을 확장할 때 어떤 문제가 발생할 수 있나요? 수천 또는 수백만 개의 동시 채팅방 요청을 테스트하는 방법을 개발할 수 있나요?
  • 이 앱은 자바스크립트 메시지를 사용하여 채팅방 이름을 가져옵니다. URL에서 채팅방 이름을 가져오는 방법을 알아봅니다. 예를 들어 http://localhost:8080/foo에서는 회의실 이름을 foo로 지정합니다.

자세히 알아보기

7. 피어 연결 및 신호 결합

이 단계의 전체 버전은 step-05 폴더에 있습니다.

HTML 및 자바스크립트 교체

  1. index.html의 내용을 다음으로 바꿉니다.
<!DOCTYPE html>
<html>

<head>

  <title>Real-time communication with WebRTC</title>

  <link rel="stylesheet" href="/css/main.css" />

</head>

<body>

  <h1>Real-time communication with WebRTC</h1>

  <div id="videos">
    <video id="localVideo" autoplay muted></video>
    <video id="remoteVideo" autoplay></video>
  </div>

  <script src="/socket.io/socket.io.js"></script>
  <script src="https://webrtc.github.io/adapter/adapter-latest.js"></script>
  <script src="js/main.js"></script>
  
</body>

</html>
  1. js/main.jsstep-05/js/main.js의 콘텐츠로 바꿉니다.

Node.js 서버 실행

work 디렉터리에서 이 Codelab을 따르지 않으면 step-05 폴더나 현재 작업 중인 폴더의 종속 항목을 설치해야 할 수 있습니다.

  1. 작업 디렉터리에서 다음 명령어를 실행합니다.
npm install
  1. 설치 후 Node.js 서버가 실행되고 있지 않으면 work 디렉터리에서 다음 명령어를 실행하여 시작합니다.
node index.js

Socket.IO를 구현하는 이전 단계의 index.js 버전을 사용하고 있는지 확인하세요. 노드 및 소켓 IO에 대한 자세한 내용은 메시지 교환을 위한 신호 서비스 설정 섹션을 참조하세요.

  1. 브라우저에서 http://localhost:8080으로 이동합니다.
  2. 새 탭을 열고 http://localhost:8080으로 다시 이동합니다.

video 요소 중 하나는 getUserMedia()의 로컬 스트림을 표시하고 다른 요소는 RTCPeerconnection를 통해 스트리밍된 원격 동영상을 표시합니다.

  1. 브라우저 콘솔에서 로깅을 확인합니다.

점수****

  • 이 앱은 일대일 영상 채팅만 지원합니다. 2명 이상의 사용자가 동일한 영상 채팅방을 공유할 수 있도록 디자인을 변경하려면 어떻게 해야 할까요?
  • 이 예에는 foo라는 하드 코딩된 회의실 이름이 있습니다. 다른 방 이름을 사용 설정하는 가장 좋은 방법은 무엇인가요?
  • 사용자는 회의실 이름을 어떻게 공유하나요? 방 이름을 공유하는 대신 빌드해 보세요.
  • 앱을 어떻게 변경할 수 있을까요?

  • chrome://webrtc-internals에서 WebRTC 통계를 찾아 데이터를 디버그합니다.
  • WebRTC 문제 해결 도구를 사용하여 로컬 환경을 확인하고 카메라 및 마이크를 테스트합니다.
  • 캐싱에 문제가 있는 경우 다음을 시도해 보세요.
  1. Control 키를 누르고 이 페이지 새로고침을 클릭합니다.
  2. 브라우저를 다시 시작합니다.
  3. 명령줄에서 npm cache clean를 실행합니다.

8. 사진 촬영 및 데이터 채널 공유

이 단계의 전체 버전은 step-06 폴더에 있습니다.

사용 방법

이전 과정에서는 RTCDataChannel를 사용하여 문자 메시지를 교환하는 방법을 알아봤습니다. 이 단계를 통해 전체 파일을 공유할 수 있습니다. 이 예시에서는 getUserMedia()로 사진이 캡처됩니다.

이 단계의 핵심은 다음과 같습니다.

  1. 데이터 채널 설정

이 단계에서는 미디어 연결을 피어 연결에 추가하지 않습니다.

  1. getUserMedia()로 웹캠 동영상 스트림을 캡처하세요.
var video = document.getElementById('video');

function grabWebCamVideo() {
  console.log('Getting user media (video) ...');
  navigator.mediaDevices.getUserMedia({
    video: true
  })
  .then(gotStream)
  .catch(function(e) {
    alert('getUserMedia() error: ' + e.name);
  });
}
  1. Snap을 클릭하여 동영상 스트림에서 스냅샷 (동영상 프레임)을 가져와 canvas 요소에 표시합니다.
var photo = document.getElementById('photo');
var photoContext = photo.getContext('2d');

function snapPhoto() {
  photoContext.drawImage(video, 0, 0, photo.width, photo.height);
  show(photo, sendBtn);
}
  1. Send를 클릭하여 이미지를 바이트로 변환하고 데이터 채널을 통해 보냅니다.
function sendPhoto() {
  // Split the data-channel message in chunks of this byte length.
  var CHUNK_LEN = 64000;
  var img = photoContext.getImageData(0, 0, photoContextW, photoContextH),
    len = img.data.byteLength,
    n = len / CHUNK_LEN | 0;

  console.log('Sending a total of ' + len + ' byte(s)');
  dataChannel.send(len);

  // Split the photo and send in chunks of approximately 64KB.
  for (var i = 0; i < n; i++) {
    var start = i * CHUNK_LEN,
      end = (i + 1) * CHUNK_LEN;
    console.log(start + ' - ' + (end - 1));
    dataChannel.send(img.data.subarray(start, end));
  }

  // Send the reminder, if applicable.
  if (len % CHUNK_LEN) {
    console.log('last ' + len % CHUNK_LEN + ' byte(s)');
    dataChannel.send(img.data.subarray(n * CHUNK_LEN));
  }
}

수신 측은 데이터 채널 메시지 바이트를 이미지로 변환하여 이미지를 사용자에게 표시합니다.

function receiveDataChromeFactory() {
  var buf, count;

  return function onmessage(event) {
    if (typeof event.data === 'string') {
      buf = window.buf = new Uint8ClampedArray(parseInt(event.data));
      count = 0;
      console.log('Expecting a total of ' + buf.byteLength + ' bytes');
      return;
    }

    var data = new Uint8ClampedArray(event.data);
    buf.set(data, count);

    count += data.byteLength;
    console.log('count: ' + count);

    if (count === buf.byteLength) {
      // we're done: all data chunks have been received
      console.log('Done. Rendering photo.');
      renderPhoto(buf);
    }
  };
}

function renderPhoto(data) {
  var canvas = document.createElement('canvas');
  canvas.width = photoContextW;
  canvas.height = photoContextH;
  canvas.classList.add('incomingPhoto');
  // The trail is the element that holds the incoming images.
  trail.insertBefore(canvas, trail.firstChild);

  var context = canvas.getContext('2d');
  var img = context.createImageData(photoContextW, photoContextH);
  img.data.set(data);
  context.putImageData(img, 0, 0);
}

코드 가져오기

  1. work 폴더의 콘텐츠를 step-06의 콘텐츠로 바꿉니다.

이제 workindex.html 파일이 다음과 같이 표시됩니다.**:**

<!DOCTYPE html>
<html>

<head>

  <title>Real-time communication with WebRTC</title>

  <link rel="stylesheet" href="/css/main.css" />

</head>

<body>

  <h1>Real-time communication with WebRTC</h1>

  <h2>
    <span>Room URL: </span><span id="url">...</span>
  </h2>

  <div id="videoCanvas">
    <video id="camera" autoplay></video>
    <canvas id="photo"></canvas>
  </div>

  <div id="buttons">
    <button id="snap">Snap</button><span> then </span><button id="send">Send</button>
    <span> or </span>
    <button id="snapAndSend">Snap &amp; Send</button>
  </div>

  <div id="incoming">
    <h2>Incoming photos</h2>
    <div id="trail"></div>
  </div>

  <script src="/socket.io/socket.io.js"></script>
  <script src="https://webrtc.github.io/adapter/adapter-latest.js"></script>
  <script src="js/main.js"></script>

</body>

</html>
  1. work 디렉터리에서 이 Codelab을 따르지 않으면 step-06 폴더나 현재 작업 중인 폴더의 종속 항목을 설치해야 할 수 있습니다. 작업 디렉터리에서 다음 명령어를 실행하면 됩니다.
npm install
  1. 설치 후 Node.js 서버가 실행되고 있지 않으면 work 디렉터리에서 다음 명령어를 실행하여 시작합니다.
node index.js
    Make sure that you're using the version of `index.js` that implements Socket.IO and 

변경사항이 있는 경우 Node.js 서버를 다시 시작해야 합니다.

노드 및 Socket.IO에 대한 자세한 내용은 신호 설정 섹션을 참조하세요.

서비스를 사용하여 메시지를 교환할 수 있습니다.

  1. 필요한 경우 앱에서 웹캠을 사용할 수 있도록 허용을 클릭합니다.

앱에서 임의의 방 ID를 만들고 URL에 ID를 추가합니다.

  1. 새 브라우저 탭 또는 창의 주소 표시줄에서 URL을 엽니다.
  2. Snap & Send를 클릭한 다음 페이지 하단의 다른 탭에서 수신 사진을 확인합니다.

앱에서 탭 간에 사진을 전송합니다.

다음과 같이 표시됩니다.

911b40f36ba6ba8.png

보너스 점수

모든 파일 형식을 공유할 수 있도록 코드를 변경하려면 어떻게 해야 하나요?

자세히 알아보기

9. 축하합니다

실시간 동영상 스트리밍 및 데이터 교환을 위한 앱을 빌드했습니다.

자세히 알아보기