728x90
728x90

WebRTC


웹, 네이티브 앱에서 오디오, 비디오, 데이터의 실시간 통신을 가능하게 하는 오픈 소스 프로젝트. 즉 웹 브라우저에서 화상채팅, 음성채팅 등을 제작 가능한데, 스트리밍 데이터를 얻고 통신하기 위한 API가 구현되어 있다. (출처:구글코드랩)

몇 가지 JS API를 알아보면...

 

getUserMedia() : 오디오, 비디오를 캡쳐


MediaRecorder : 오디오, 비디오를 녹화


RTCPeerConnection : 사용자간 오디오, 비디오를 스트리밍

WebRTC는 RTCPeerConnection을 사용하여 브라우저간에 스트리밍 데이터를 전달하는데, 통신을 조정하고 신호로 알려진 프로세스 인 제어 메시지를 보내는 메커니즘이 필요하다. 이 시그널링 방법과 프로토콜은 WebRTC에서 지정하지 않고 보통 메시징에 Socket.IO를 사용한다.

RTCDataChannel : 사용자간 데이터를 스트리밍 

 

 

 

사용처


파폭, 오페라, 등 브라우저와 데스크톱, 안드로이드 크롬, ios, 안드로이드 기본 앱에서 사용 가능

 

 

실습 예제 목차


웹캠으로 비디오를 가져오고, 스냅샷을 찍고, P2P를 공유하는 앱을 빌드하는 예제이다. WebRTC API 사용 방법을 배우며 Node.js를 사용하여 메시징 서버를 설정할 수 있다. 웹캠에서 비디오 가져오기, RTCPeerConnection으로 비디오 스트리밍, RTCDataChannel를 사용한 데이터 스트리밍, 메시지 교환을 위한 신호 서비스 설정, 피어 연결 및 신호 결합, 스냅샷을 찍어 데이터 채널을 통해 공유하기 등을 해 본다.

요구사항은 Chrome 47 이상, Chrome용 웹서버나 다른 웹 서버, 샘플 코드, 텍스트 편집기, 아래 선행 지식이다.

웹 서버로 파이어베이스(구글 Firebase)를 사용했다.

 

선행 지식 : HTML, CSS, JS, NodeJS, SSL, Soket, IO

 

 

1~3. 네트워크 없이 실행

1. 카메라에서 비디오 추출

2. RTCPeerConnection을 이용해서 상대에게 비디오 제공 (네트워크 x)

3. RTCDataChannel을 이용해서 데이터 주고받기

 

4~6. 네트워크 이용, NodeJS 설치 후 웹으로 실행

4. 서버를 이용해서 메세지 주고받기 (signaling) 

5. 네트워크 상에서 비디오 주고받기 : getUserMedia, signaling, RTCPeerConnection의 종합 예제

6. 데이터 공유(제공)

 

 

예제 관련 주의사항

※ 구글 예제는 크롬에서만 작동하여 다른 브라우저에서 작동을 원할 시 추가 작업이 필요하다.

※ 네트워크 연결 시 SSL이 기본적으로 필요

※ 대화 상대를 찾기 위해 사용하는 서버가 한번만 연결되도록 만들어져 서버 재가동이 필요

※ 해당 예제는 오디오는 제공되지 않음, 위 사항들은 코드 일부 수정으로 보완 가능

 

 

 

구글 예제 코드 (다운)


 

CMD에서 step-05 디렉토리로 이동하여 npm i (설치) 를 명령하여 필요한 라이브러리를 설치한다.

 

C:\경로\step-05> dir
npm i

 

 

 

node index.js를 실행하여 웹서버를 실행
성공하면 아무런 메세지가 리턴되지 않는다

 

node index.js

 

크롬 브라우저 주소창에 http://localhost:8080 을 입력한다

카메라 사용 권한 요청화면 > 허용 클릭

 

 

서버 IP는 NodeJS를 실행한 컴퓨터 콘솔창에서 ipconfig나 ifconfig를 입력하면 확인 가능

 

  • index.js - NodeJS로 실행한 웹 서버 및 Signaling
  • index.html - 보여주는 첫 웹페이지. 화상채팅 화면.
    • js/main.js - RTCPeerConnection과 같은 실제 화상채팅을 하는 클라이언트 코드
    • css/main.css - index.html 에서 사용할 디자인

 

 

오류 정리


 

npm notice created a lockfile as package-lock.json. You should commit this file.

 

원인 : package.json에 해당 정보가 없어서

개인용 프로젝트거나 git이나 desc를 추가하지 않는다는 가정하에 description 쉼표 다음에 "private": true 추가 (출처)

 

 

getusermedia() error notfounderror

 

MediaDevices.getUserMedia()

반환값은 객체로 이행하는 Promise로 사용자가 권한 요청을 거부했거나 일치하는 유형의 미디어를 사용할 수 없는 경우 NonAllowedError와 NotFoundError 을 반환한다. 즉 지금 연결된 카메라가 없어서 뜬 메세지

 

 

 

 

주요 파일의 소스 구조


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>

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

  <!-- This file is automatically added/served when running "node index.js". -->
  <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>

 

main.css

 

body {
  font-family: sans-serif;
}

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

 

main.js

'use strict';

var isChannelReady = false;
var isInitiator = false;
var isStarted = false;
var localStream;
var pc;
var remoteStream;
var turnReady;

var pcConfig = {
  'iceServers': [{
    'urls': 'stun:stun.l.google.com:19302'
  }]
};

// Set up audio and video regardless of what devices are present.
var sdpConstraints = {
  offerToReceiveAudio: true,
  offerToReceiveVideo: true
};

/////////////////////////////////////////////

var room = 'foo';
// Could prompt for room name:
// room = prompt('Enter room name:');

var socket = io.connect();

if (room !== '') {
  socket.emit('create or join', room);
  console.log('Attempted to create or  join room', room);
}

socket.on('created', function(room) {
  console.log('Created room ' + room);
  isInitiator = true;
});

socket.on('full', function(room) {
  console.log('Room ' + room + ' is full');
});

socket.on('join', function (room){
  console.log('Another peer made a request to join room ' + room);
  console.log('This peer is the initiator of room ' + room + '!');
  isChannelReady = true;
});

socket.on('joined', function(room) {
  console.log('joined: ' + room);
  isChannelReady = true;
});

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

////////////////////////////////////////////////

function sendMessage(message) {
  console.log('Client sending message: ', message);
  socket.emit('message', message);
}

// This client receives a message
socket.on('message', function(message) {
  console.log('Client received message:', message);
  if (message === 'got user media') {
    maybeStart();
  } else if (message.type === 'offer') {
    if (!isInitiator && !isStarted) {
      maybeStart();
    }
    pc.setRemoteDescription(new RTCSessionDescription(message));
    doAnswer();
  } else if (message.type === 'answer' && isStarted) {
    pc.setRemoteDescription(new RTCSessionDescription(message));
  } else if (message.type === 'candidate' && isStarted) {
    var candidate = new RTCIceCandidate({
      sdpMLineIndex: message.label,
      candidate: message.candidate
    });
    pc.addIceCandidate(candidate);
  } else if (message === 'bye' && isStarted) {
    handleRemoteHangup();
  }
});

////////////////////////////////////////////////////

var localVideo = document.querySelector('#localVideo');
var remoteVideo = document.querySelector('#remoteVideo');

navigator.mediaDevices.getUserMedia({
  audio: false,
  video: true
})
.then(gotStream)
.catch(function(e) {
  alert('getUserMedia() error: ' + e.name);
});

function gotStream(stream) {
  console.log('Adding local stream.');
  localStream = stream;
  localVideo.srcObject = stream;
  sendMessage('got user media');
  if (isInitiator) {
    maybeStart();
  }
}

var constraints = {
  video: true
};

console.log('Getting user media with constraints', constraints);

if (location.hostname !== 'localhost') {
  requestTurn(
    'https://computeengineondemand.appspot.com/turn?username=41784574&key=4080218913'
  );
}

function maybeStart() {
  console.log('>>>>>>> maybeStart() ', isStarted, localStream, isChannelReady);
  if (!isStarted && typeof localStream !== 'undefined' && isChannelReady) {
    console.log('>>>>>> creating peer connection');
    createPeerConnection();
    pc.addStream(localStream);
    isStarted = true;
    console.log('isInitiator', isInitiator);
    if (isInitiator) {
      doCall();
    }
  }
}

window.onbeforeunload = function() {
  sendMessage('bye');
};

/////////////////////////////////////////////////////////

function createPeerConnection() {
  try {
    pc = new RTCPeerConnection(null);
    pc.onicecandidate = handleIceCandidate;
    pc.onaddstream = handleRemoteStreamAdded;
    pc.onremovestream = handleRemoteStreamRemoved;
    console.log('Created RTCPeerConnnection');
  } catch (e) {
    console.log('Failed to create PeerConnection, exception: ' + e.message);
    alert('Cannot create RTCPeerConnection object.');
    return;
  }
}

function handleIceCandidate(event) {
  console.log('icecandidate event: ', 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.');
  }
}

function handleCreateOfferError(event) {
  console.log('createOffer() error: ', event);
}

function doCall() {
  console.log('Sending offer to peer');
  pc.createOffer(setLocalAndSendMessage, handleCreateOfferError);
}

function doAnswer() {
  console.log('Sending answer to peer.');
  pc.createAnswer().then(
    setLocalAndSendMessage,
    onCreateSessionDescriptionError
  );
}

function setLocalAndSendMessage(sessionDescription) {
  pc.setLocalDescription(sessionDescription);
  console.log('setLocalAndSendMessage sending message', sessionDescription);
  sendMessage(sessionDescription);
}

function onCreateSessionDescriptionError(error) {
  trace('Failed to create session description: ' + error.toString());
}

function requestTurn(turnURL) {
  var turnExists = false;
  for (var i in pcConfig.iceServers) {
    if (pcConfig.iceServers[i].urls.substr(0, 5) === 'turn:') {
      turnExists = true;
      turnReady = true;
      break;
    }
  }
  if (!turnExists) {
    console.log('Getting TURN server from ', turnURL);
    // No TURN server. Get one from computeengineondemand.appspot.com:
    var xhr = new XMLHttpRequest();
    xhr.onreadystatechange = function() {
      if (xhr.readyState === 4 && xhr.status === 200) {
        var turnServer = JSON.parse(xhr.responseText);
        console.log('Got TURN server: ', turnServer);
        pcConfig.iceServers.push({
          'urls': 'turn:' + turnServer.username + '@' + turnServer.turn,
          'credential': turnServer.password
        });
        turnReady = true;
      }
    };
    xhr.open('GET', turnURL, true);
    xhr.send();
  }
}

function handleRemoteStreamAdded(event) {
  console.log('Remote stream added.');
  remoteStream = event.stream;
  remoteVideo.srcObject = remoteStream;
}

function handleRemoteStreamRemoved(event) {
  console.log('Remote stream removed. Event: ', event);
}

function hangup() {
  console.log('Hanging up.');
  stop();
  sendMessage('bye');
}

function handleRemoteHangup() {
  console.log('Session terminated.');
  stop();
  isInitiator = false;
}

function stop() {
  isStarted = false;
  pc.close();
  pc = null;
}

 

 

참고 : forest71.tistory.com/211?category=788767

 

 

728x90
728x90

'Project' 카테고리의 다른 글

[스크랩] Dark Glassmorphism UI & Black Web Design  (0) 2021.05.24
화상채팅 줌 (ZOOM) 프로그램 분석  (0) 2021.03.11
블로그 이미지

coding-restaurant

코딩 맛집에 방문해주셔서 감사합니다.

,

v