https://cornbro.tistory.com/3에서 이어지는 내용입니다.

 

먼저 RTCPeerConnection은 비디오 및 오디오 데이터 교환을 위한 API입니다.

해당 예제는 실제 외부접속은 아니며 PeerConnection API 작동 방식을 이해하기위한 단순 로컬 예제입니다.

 

work/index.html에 다음과 같이 내용을 추가합니다.

<!DOCTYPE html>
<html>

<head>

  <title>Realtime communication with WebRTC</title>

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

</head>

<body>

  <h1>Realtime 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>
		<button id="stopButton">Stop</button>
	</div>	

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

</body>

</html>

2개의 비디오 구성과 4개의 컨트롤 버튼을 추가하였습니다.

 "localVideo"는 getUserMedia()를 통해 비디오 스트림을 가져오고,  "remoteVideo"는 RTCPeerConnection통해 동일한 비디오 스트림을 가져옵니다.

 그리고 최신 버전의 adapter.js shim 링크를 연결해줍니다.

더보기

 adapter.js : 규격화를 통해 대부분의 프로토콜은 동일하게 구성되었지만 다른 브라우져간에 생기는 일부 다른 부분(ex. prefix)을 호환 시켜주기 위해 사용하게 됩니다.

step-2 폴더에 있는 main.js를 work 폴더로 옮겨서 열어봅니다.

개인적으로 로컬 스트림도 닫을수 있게 stop버튼을 추가했습니다.

 

변수 생성 및 상태 로그 메세지에 대한 내용은 넘어가도록 하겠습니다.

 

start 버튼에서 로컬 미디어 스트림을 가져옵니다. => gotLocalMediaStream()

// Handles start button action: creates local MediaStream.
function startAction() {
  startButton.disabled = true;
  navigator.mediaDevices.getUserMedia(mediaStreamConstraints)
    .then(gotLocalMediaStream).catch(handleLocalMediaStreamError);
  trace('Requesting local stream.');
}

//--------------------------------------------------------------------------------
      // Sets the MediaStream as the video element src.
      function gotLocalMediaStream(mediaStream) {
        localVideo.srcObject = mediaStream;
        localStream = mediaStream;
        trace('Received local stream.');
        callButton.disabled = false;  // Enable call button.
        stopButton.disabled = false;
      }
//--------------------------------------------------------------------------------

call 버튼에서 Peer Connection을 통해 end to end 연결을 하고 => handleConnection()

로컬에서 원격으로 비디오 스트림을 가져옵니다. => gotRemoteMediaStream()

실제에서는 메시징 서비스를 통해 외부로 연결되겠지만, 해당 스텝에서는 같은 페이지에서 직접 통신을 합니다.

WebRTC Peer는 해상도 및 코덱 기능과 같은 미디어 정보를 교환해야 합니다.

SDP 프로토콜의 Offer<->Answer 를 통해 메타 데이터의 Blob을 교환합니다. => createdOffer/createdAnswer

// Handles call button action: creates peer connection.
function callAction() {
  callButton.disabled = true;
  hangupButton.disabled = false;

  trace('Starting call.');
  startTime = window.performance.now();

  // Get local media stream tracks.
  const videoTracks = localStream.getVideoTracks();
  const audioTracks = localStream.getAudioTracks();
  if (videoTracks.length > 0) {
    trace(`Using video device: ${videoTracks[0].label}.`);
  }
  if (audioTracks.length > 0) {
    trace(`Using audio device: ${audioTracks[0].label}.`);
  }

  const servers = null;  // Allows for RTC server configuration.

  // Create peer connections and add behavior.
  localPeerConnection = new RTCPeerConnection(servers);
  trace('Created local peer connection object localPeerConnection.');

  localPeerConnection.addEventListener('icecandidate', handleConnection);
  localPeerConnection.addEventListener(
    'iceconnectionstatechange', handleConnectionChange);
  //---------------------------------------------------------------------    
        // Connects with new peer candidate.
        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}.`);
            }
        }
        
        // Gets the "other" peer connection.
        function getOtherPeer(peerConnection) {
          return (peerConnection === localPeerConnection) ?
              remotePeerConnection : localPeerConnection;
        }
        
        // Logs changes to the connection state.
        function handleConnectionChange(event) {
            const peerConnection = event.target;
            console.log('ICE state change event: ', event);
            trace(`${getPeerName(peerConnection)} ICE state: ` +
                  `${peerConnection.iceConnectionState}.`);
        }
  //---------------------------------------------------------------------    

  remotePeerConnection = new RTCPeerConnection(servers);
  trace('Created remote peer connection object remotePeerConnection.');

  remotePeerConnection.addEventListener('icecandidate', handleConnection);
  remotePeerConnection.addEventListener(
    'iceconnectionstatechange', handleConnectionChange);
  remotePeerConnection.addEventListener('addstream', gotRemoteMediaStream);
  
  //--------------------------------------------------------------------- 
  		// Handles remote MediaStream success by adding it as the remoteVideo src.
        function gotRemoteMediaStream(event) {
          const mediaStream = event.stream;
          remoteVideo.srcObject = mediaStream;
          remoteStream = mediaStream;
          trace('Remote peer connection received remote stream.');
        }
  //--------------------------------------------------------------------- 
  
  // Add local stream to connection and create offer to connect.
  localPeerConnection.addStream(localStream);
  trace('Added local stream to localPeerConnection.');

  trace('localPeerConnection createOffer start.');
  localPeerConnection.createOffer(offerOptions)
    .then(createdOffer).catch(setSessionDescriptionError);
    
  //--------------------------------------------------------------------- 
        // 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);
        }
  //--------------------------------------------------------------------- 
}

hangup 버튼에서 Peer Connection 연결을 닫아줍니다.

// Handles hangup action: ends up call, closes connections and resets peers.
function hangupAction() {
  localPeerConnection.close();
  remotePeerConnection.close();
  localPeerConnection = null;
  remotePeerConnection = null;
  hangupButton.disabled = true;
  callButton.disabled = false;
  trace('Ending call.');
}

stop 버튼에서 로컬 스트림을 닫아줍니다.

function stopAction() {
	if (hangupButton.disabled == false){
		hangupAction();
	}
	startButton.disabled = false;
	callButton.disabled = true;
	stopButton.disabled = true;
	
	localStream.getTracks().forEach((track) => {
		track.stop();
	});
}

 


파일 다운받아서 실행하면 화면처럼 똑같이 실행됩니다.

step_2.zip
0.02MB

 

 

'WebRTC' 카테고리의 다른 글

[WebRTC] 웹캠 영상 단순출력 예제  (0) 2020.01.12
[WebRTC] 개념정리  (1) 2020.01.07

구글코드랩스에서 제공하는 WebRTC 예제입니다.

https://codelabs.developers.google.com/codelabs/webrtc-web/#0

 

 

다음 링크에서 소스코드를 다운받을 수 있습니다.

https://github.com/googlecodelabs/webrtc-web

그리고 크롬 브라우저에서 Web Server 앱을 설치합니다.

https://chrome.google.com/webstore/detail/web-server-for-chrome/ofhbbkphhbklhfoeikjpcbhemlocgigb?hl=en

설치한 후, 주소창에서 chrome://apps로 이동해 Web Server 앱을 선택해서 실행해 줍니다.

 

chrome://apps

 

실행된 창에서 다음 같이 세팅해 줍니다.

폴더는 다운로드한 소스코드에서 work이라는 폴더로 지정해주면 됩니다.

Web Server App

Web Server URL(s) : http://127.0.0.1:8887 클릭해서 창을 띄우시면 다음과 같은 화면을 보실 수 있습니다.

현재는 기능이 구현되지 않아 보시는 바와 같이 텍스트만 출력하고 있습니다.

초기실행

work폴더에 보시면 아시겠지만 index.html, main.css, main.js, adapter.js 이상 4개의 파일로 구성되어져 있습니다.

먼저, 다음 파일들을 열어서 코드를 추가로 입력해 줍니다.

'F12' 누르고 브라우저 상에서 작업하셔도 되고, 저 같은 경우 노트패드에서 작업했습니다.

 

 

 

 * index.html

<!DOCTYPE html>
<html>

<head>

  <title>Realtime communication with WebRTC</title>

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

</head>

<body>

  <h1>Realtime communication with WebRTC</h1>

  <video autoplay playsinline></video>

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

</body>

</html>

 

 * main.js

'use strict';

// On this codelab, you will be streaming only video (video: true).
const mediaStreamConstraints = {
  video: true,
};

// Video element where stream will be placed.
const localVideo = document.querySelector('video');

// Local stream that will be reproduced on the video.
let localStream;

// Handles success by adding the MediaStream to the video element.
function gotLocalMediaStream(mediaStream) {
  localStream = mediaStream;
  localVideo.srcObject = mediaStream;
}

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

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

다 입력하고 저장한 후에 'F5'번 키를 눌러 리로드 합니다.

처음 한번은 브라우져에서 카메라 접근 퍼미션 메세지가 뜰겁니다. 

확인하면 MediaStream이 반환되며, 웹캠을 통해 영상이 출력되는걸 확인할 수 있습니다.

웹캠 출력화면

 

그리고 Ctrl+Shift+J 를 눌러 콘솔창에서 커맨드를 입력해서 결과를 확인해 봅니다.

localStream.getVideoTracks()
localStream.getVideoTracks()[0].stop()

 

그리고 main.css에서 다음과 같이 다양한 이미지 옵션을 추가하거나 수정할 수 있습니다.

body {
  font-family: sans-serif;
}

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

video {
  filter: blur(4px) invert(1) opacity(0.5);
}

video {
   filter: hue-rotate(180deg) saturate(200%);
}

 

'WebRTC' 카테고리의 다른 글

[WebRTC] 로컬 PeerConnecton 비디오 스트림 출력  (0) 2020.01.20
[WebRTC] 개념정리  (1) 2020.01.07

 WebRTC에 흥미가 생겨서 공부해보려고 정리한 내용입니다.

 

 WebRTC는 심플한 API를 통해 웹 브라우저와 모바일 어플리케이션에 RTC(Real-Time Communication) 지원하는 오픈소스 프로젝트로 별도의 플러그인이나 프로그램 설치 P2P통신을 통해 웹 페이지 내에서 오디오와 비디오 통신이 가능하게 해준다. 웹 브라우저 기반의 통신 방식인 WebRTC는 구글이 오픈소스화한 프로젝트에서 시작되었고 국제 인터넷 표준화 기구 프로토콜 표준화 작업을, W3C가 API정의를 진행하였다. 라이선스는 BSD


* 지원 플랫폼

   - PC : MS Edge 12+ / Chrome 28+ / Firefox 22+ / Safari 11+ / Opera 18+ / Vivaldi 1.9+

   - Android : Chrome 28+ / Firefox 24+ / Opera Mobile 12+

   - Chrome OS

   - Firefox OS

   - BlackBerry 10

   - iOS : MobileSafari/WebKit(iOS 11+)

   - Tizen 3.0

 

 * 지원 코덱

   - 오디오 : PCMU(G.711u) / PCMA(G.711a) / Opus

   - 비디오 : VP8 / VP9 /H.264(AVC)

 

 * 주요기능

   - getUserMedia : 로컬 비디오와 오디오에 접근하여 미디어 데이터를 가져온다.

   - RTCPeerConnection : 피어간 오디오, 비디오 통신을 활성화한다. 신호처리, 코덱관리, P2P통신, 보안, 대역폭 관리 등을 수행한다.

   - RTCDataChannel : 피어간 양방향 임의 데이터 통신을 허용한다. 웹 소켓과 동일한 API를 사용하며 낮은 레이턴시를 보여준다.

   - getStats : 관리를 위한 통계 API이다.

 

* 아키텍쳐

WebRTC Architecture

 

 * 시그널링

Simple Signaling

신호는 2개의 엔드 포인트가 메타 데이터를 교환하여 통신을 조정할 수 있도록 한다. SDP(Session Description Protocol)의 Offer <-> Answer 기반으로 구성되어 있다.

 

Simple Signaling Sequence

 * NAT/Firewall Traversal

 보통 클라이언트는 사설 IP주소를 사용하는 내부 호스트인데 P2P로 연결요청하면 NAT/Firewall에서 막혀서 응답을 받을 수 없다.

 

   - ICE(Interactive Connectivity Establishment), RFC5245

  ICE는 연결에 사용할 수 있는 모든 가능한 IP 후보군(사설 IP, STUN이 돌려주는 공인 IP)들을 조사하고, Peer간 직접 연결을 맺을 수 있는지를 확인하는 기술이다. 연결 테스트를 위해 SDP(Session Description Protocol)을 사용하여 미디어 패킷을 흘려 보내며 연결 가능 여부를 테스트 한다.

 

   - TURN(Traversal Using Relays around NAT), RFC5766

 공인 IP간 연결을 테스트해보고 연결이 가능하면 WebRTC 클라이언트들은 P2P연결이 된 것이지만, 만약 연결이 실패한다면 인터넷 상의 중계 서버(Relay Server)를 사용해야 하는데, 이 서버가 TURN 서버이다.

릴레이 주소를 할당하고 패킷 릴레이를 통해 상호연결한다.

   - STUN(Session Traversal Utilities for NAT), RFC3489

 WebRTC의 P2P 연결을 위해 NAT 뒷 단의 클라이언트들은 사설 IP를 내부에서 보유하고 있다. 외부 통신을 위해 자신의 공인 IP정보를 스스로 파악하여 서로에게 알려주어야 한다. 이때 사설 IP를 보유한 장비들의 공인 IP정보를 알려주는 서버가 STUN이다.

자신의 공인 IP와 포트를 파악하여 서로에게 알려준다.

 

 * Web Topologies

   - Peer-to-peer (Mesh)

Peer To Peer (Mesh)

 세션의 각 참가자는 서버를 사용하지 않고 다른 모든 참가자와 직접 연결한다. 이런 종류의 연결은 비용이 가장 적고 설치하기 쉽기 때문에 작은 화상 회의에서 적절하다. 그러나 컨퍼런스가 커질 경우 CPU 사용량이 많아질 수 있기 때문에 모든 참가자들 간의 직접 연결 유지에 어려움을 겪게 된다. 모든 피어 사이간 개별 직접 연결 이기 때문에 메시 토폴로지는 녹화가 어렵다.

 낮은 대기 시간이 중요하고 녹화가 필요하지 않은 2~3명의 참가자를 연결하는 단순한 어플리케이션에 적절함다.

 

   - Selective Forwarding (SFU)

Selective Forwarding (SFU)

 세션의 각 참가자는 선택적 전달 장치(SFU) 역할을 하는 서버에 연결된다. 각 참가자는 암호화된 비디오 스트림을 서버에 한번 업로드한다. 서버는 그 스트림을 다른 참가자들에게 전달한다. 이것은 대기 시간을 줄이고 또한 피어 투 피어 연결에서 훨씬 더 어려운 SIP와 같은 다른 서버 측 통합과 같은 것들을 가능하게 한다.

 클라이언트 측에선 하나의 업스트림 연결만이 존재하므로 메시 토폴로지보다 업로딩 효율을 높이지만, 다운스트림은 참가자 수 만큼 존재한다. 업스트림에서의 여유로 참가자 수가 늘지만 클라이언트 자원 고갈에 의한 위상의 한계는 여전히 존재한다.

 여전히 낮은 대기 시간을 보증하며 기록이 필요하고 무결성이 중요한 4~10명의 참가자를 연결하는 어플리케이션에 적합하다. 균형잡힌 토폴로지로 간주된다.

 

   - Multipoint Control (Mixing)

Multipoint Control (Mixing)

 세션의 각 참가자는 다중 연결 제어장치(MCU) 역할을 하는 서버에 연결한다. 각 참가자로 부터 받은 미디어를 단일 스트림으로 합친 다음 각 클라이언트에게 제공한다. 서버 측면에서 대역폭 사용량과 다운스트림 연결에서 CPU 점유율 여유를 가져오지만 대신 오디오/비디오 미디어를 단일 스트림으로 합치기 위한 CPU 할당이 필요로 하다.

 가장 낮은 대역폭이기 때문에 네트워크 조건이 좋지 않을때 이점을 가지며, 위상의 제한이 없어 다수의 참가자 연결에 적절하다. 미디어를 서버에서 다루는 과정에서 낮은 레이턴시 보상을 받지 못한다.

 

 * WebRTC 보안

   - 브라우저 보호 : 기본적으로 브라우저 벤더에서 잠재적인 보안 위협이나 취약성을 보완해주는 부분이 있다.

   - 미디어 장치 접근 : 동의 없이 미디어 장치 자원 접근이 불가능하다.

   - 암호화 : 암호화는 WebRTC의 필수 사항이며 연결 설정 및 유지의 모든 부분에 적용된다. 이를 위해 선호하는 방법은 DTLS(Datagram Transport Layer Security) 핸드셰이크에 PFS(Perfect Forward Secrety)암호를 사용하여 키 데이터를 안전하게 교환하는 것이다. 오디오와 비디오의 경우, 키 데이터를 사용하여 AES(Advanced Encryption Standard) 키를 생성할 수 있으며, 이 키는 다시 SRTP(Secure Real-time Tranport Protocol)에서 미디어를 암호화 및 해독하는데 사용된다. WebRTC와 ORTC 모두 VoIP 시스템과 역 호환되고 상호운용 가능한 이 특정 스택을 의무화한다.

 

 


Reference

en.wikipedia.org/wiki/WebRTC

www.frozenmountain.com/ultimate-guide-to-webrtc

github.com/satanas/simple-signaling-server

 

+ Recent posts