728x90
안녕하세요. 이번 시간에는 webrtc 1:1 영상통화를 구현해보겠습니다.
시그널링 서버 코드를 돌리기 위한 node.js 세팅이 완료되었다는 가정을 하고,
영상통화 구현을 위한 코드에만 초점을 맞춰서 설명하겠습니다.
+ 참고 socket.io 와 express 버전
"express": "4.17.1",
"socket.io": "2.3.0"
server.js
//express는 다음과 같은 것을 제공한다.
// 1. http 통신 요청 (Request; GET, POST, DELETE 등)에 대한 핸들러를 만든다.
// -> 핸들러는 특정 데이터에 특화되어 있고, 특정 작업에 중점을 둔 함수, 방식
// 2. 템플릿에 데이터를 넣어서 응답을 생성하기 위해 "view" 렌더링 엔진과 통합을 한다
// -> 템플릿 : 무언가의 기본 형식을 가진 컴퓨터 문서
// -> 렌더링 엔진 : 화면에 텍스트와 이미지를 그리는 스프트웨어
// 3. 접속을 위한 포트나 응답 렌더링을 위한 템플릿 위치같은 공통 웹 어플리케이션 세팅을 한다.
// 4. 핸들링 파이프 라인 중 필요한 곳에 추가적인 미들웨어 처리 요청을 추가한다.
// -> 미들웨어란? 클라이언트에게 요청이 오면 요청을 보내기 위해 중간에서 목적에 맞게 거쳐가는 함수들 미들웨어 패키지 : 쿠키, 세션,
// 사용자 로그인, url 매개변수 등의 라이브러리
const express = require('express');
const path = require('path');
// path 모듈은 파일 및 디렉터리 경로 작업을 위한 utility 를 제공한다.
const app = express();
// express 모듈과 , application을 생성한다. 여기서 app 객체는 일반적으로 express 응용 프로그램을 나타낸다. app
// 객체는
// 1. http 요청 라우팅, 2. 미들웨어 구성, 3. html 뷰 렌더링
// 4. 템플릿 엔진 등록 5. 애플리케이션 동작 방식-> 설정 수정을 위한 방법 설정
// 5->ex) 환경모드, 경로정의가 대소문자를 구분하는지의 여부 등 을 제어
const http = require('http').createServer(app);
// http 서버와 클라이언트를 사용하려면 require('http')를 사용함 http 모듈을 이용하여 express 서버를 생성한다. 위
// 방식을 통해 생성한 서버들을 활용할 수 있다는 점에서 더 유용하게 쓰인다고 한다 자세한 내용 ->
// https://stackoverflow.com/questions/17696801/express-js-app-listen-vs-server-listen
const io = require('socket.io')(http);
// 생성된 http 모듈로 socket.io 모듈을 생성할 수 있다. socket.io 는 node.js 기반 실시간 웹 애플리케이션 지원
// 라이브러리 io는 socket.io 패키지를 import 한 변수이다.
app.use(express.static(path.join(__dirname, '../public')));
// app.use 안에 있는 모든 함수들은 모두 미들웨어이며 요청이 올 때 마다 이 미들웨어를 거치며 클라이언트에게 응답하게 된다.
// __dirname은 바로 현재 위치를 가리키는 Node.js 의 전역 변수이다. express.static은 어떤 파일을 읽을 때
// 사용하는데, 원하는 값을 public이라는 폴더에서 불러온다는 뜻이다. 불러오는 모든 파일의 source(소스 코드) 는
// _dirname/public이 된다는 것이다. 예를 들어 index.css 파일을 요청하게 되면 자동으로 /public 폴더에 찾는다.
let connectedUsers = [];
// let으로 선언하면 값을 재정의 할 수 있다. 범위 {}
function msToTime() { // 타임스탬프 찍기 위한 함수
let date = new Date(
new Date().getTime() - new Date().getTimezoneOffset() * 60000
).toISOString();
let splits = date.split(':', 4);
// 분:초:msc
return splits[1] + ':' + splits[2];
}
// on 메소드 : 현재 접속 되어 있는 클라이언트로부터 메시지를 받을기 위해 사용
io.on('connection', socket => {
console.log('---------------------start-------------------------- ');
console.log(
socket.id + ' / ' + msToTime() + ': 사용자 웹사이트에 들어오면 소켓에 연결됨 '
);
// 'connection' : soket.io의 기본 이벤트, 사용자가 웹사이트에 들어왔을 때 연결된다. 두 번째 변수는 socket은 연결이
// 성공 했을 때 connection 에 대한 정보를 담고 있다.
connectedUsers.push(socket.id);
console.log(
socket.id + ' / ' + msToTime() + ' : 유저배열에 소켓 식별자를 값으로 넣어준다.'
);
// push 함수는 배열 끝에 새 요소를 추가하고 배열의 새 길이를 반환한다. 인자 값 1 : socket.id는 배열에 새로 추가할 값이다.
// 새 연결에는 20자로 구성된 임의의 식별자(socket.id)가 할당된다.
socket.on('disconnect', () => { //클라이언트와 연결이 끊어졌을 때 발생한다.
console.log(
socket.id + ' / ' + msToTime() + '클라이언트와 연결 끊어짐 -> 소켓 끊어짐'
);
connectedUsers = connectedUsers.filter(user => user !== socket.id)
console.log(
socket.id + ' / ' + msToTime() + ' : 유저 배열에서 끊어진 소켓 아이디를 제거'
);
// filter 함수는 : 콜백 함수에 지정된 조건을 충족하는 배열 요소를 반환한다. connectedUsers 배열에서 인자값이
// socket.id와 타입&값이 다른 배열 요소만 반환 socket.broadcast.emit('update-user-list', {
// userIds: connectedUsers }) console.log(socket.id +' / ' + msToTime() + ' :
// <--- update-user-list : \n' + connectedUsers);
console.log('f--------------finish----------------f \n\n');
// sender인 socket의 클라이언트는 제외하여 메시지가 전송된다.
})
socket.on('mediaOffer', data => {
console.log(
socket.id + ' / ' + msToTime() +
' --> mediaOffer : 웹브라우저에서 내가 만든 offer 메시지 받음'
);
// socket.on(namespace) 특정 노드끼리 연결해주는 것이 namespace 이다 즉 지정한 Namespace에 있는 소켓끼리만
// 통신 한다는 것이다. 소켓을 묶어주는 단위 io.on("new_namespace", (namespace) => { ...}); 의
// 형태는 새 namespace가 생셩될 때 실행됨.
socket
.to(data.to)
.emit('mediaOffer', {
// socket.to(room) : 이벤트가 지정된 룸(인자)에 가입한 클라이언트에게만 방송되는 이벤트 emit에 대한 한정자(다른 코드의
// 의미를 규정하는 키워드)를 설정한다
from: data.from,
offer: data.offer
});
console.log(
socket.id + ' / ' + msToTime() +
' : <-- mediaOffer 다른 유저한테 offer 메시지 웹브라우저로 보냄 '
);
});
socket.on('mediaAnswer', data => {
console.log(
socket.id + ' / ' + msToTime() +
' --> mediaAnswer : 웹브라우저에서 내가 만든 answer 메시지 받음'
);
socket
.to(data.to)
.emit('mediaAnswer', {
from: data.from,
answer: data.answer
});
console.log(
socket.id + ' / ' + msToTime() + ' : <-- mediaAnswer 다른 유저한테 answer 메시지 웹브라우저로 ' +
'보냄 '
);
});
socket.on('iceCandidate', data => {
console.log(
socket.id + ' / ' + msToTime() + ' --> iceCandidate : 웹 브라우저에서 내가 만든 SDP string' +
'으로 나타내지는 ice candidate(후보자) 받음'
);
socket
.to(data.to)
.emit('remotePeerIceCandidate', { from: data.from, candidate: data.candidate });
console.log(
socket.id + ' / ' + msToTime() + ' <-- remotePeerIceCandidate : 다른 유저한테 candida' +
'te후보자 보냄'
);
})
socket.on('requestUserList', () => {
console.log(
socket.id + ' / ' + msToTime() + ' --> requestUserList : 웹브라우저에서 보낸 메시지는 없음'
);
socket.emit('update-user-list', { userIds: connectedUsers });
console.log(
socket.id + ' / ' + msToTime() + ' : <-- update-user-list 나에게 업데이트된 유저 리스트 담아서 ' +
'보냄 '
);
socket.broadcast.emit('update-user-list', { userIds: connectedUsers });
// userIds가 키값 value 로는 유저 소켓 식별자가 들어간 배열값이다.
console.log(socket.id + ' / ' + msToTime() +
' : <-- update-user-list 나를 제외한 유저들에게 업데이트된 유저 리스트 담아서 보냄');
console.log(socket.id + ' // update-user-list : ' + connectedUsers);
// broadcast는 sender 을 제외한 모든 client에게 보낸다.
});
});
http.listen(3000, () => { // 지정된 포트 3000에서 서버를 시작하고
console.log(msToTime() + ': listening on *:3000'); //콘솔에 로그 주석을 인쇄한다.
});
그다음은 peer 의 js 코드 입니다.
index.js
// Creating the peer
// 로컬 컴퓨터와 원격 피어 간의 웹 RTC 연결하기 위해 필요한 객체
// 연결을 유지 및 모니터링함 또한 더이상 필요하지않으면 연결을 닫을 수 있는 방법 제공
// 이 객체는 어떻게 peer 연결 설정할지, 사용할 ice 서버의정보
const peerConnection = new RTCPeerConnection({
iceServers: [
{
urls: "stun:stun.stunprotocol.org"
}
]
});
// Connecting to socket
const socket = io('http://자신의 서버주소:(자신의 포트번호)');
const videoGrid = document.getElementById('video-grid')
function msToTime() { // 분:초:msc
let date =
new Date(new Date().getTime() - new Date().getTimezoneOffset() * 60000).toISOString();
let splits = date.split(':', 4);
return splits[1] + ':' + splits[2];
}
const onSocketConnected = async () => {
console.log(' / ' + msToTime() + ' -- onSocketConnected 나의 웹켐과 마이크에 접근하기 위한 과정---');
const constraints = { //제약 조건
// 해상도, 장치 id, 마이크 에코 등을 설정가능
audio: true,
video: true
};
const stream = await navigator.mediaDevices.getUserMedia(constraints);
console.log(' / ' + msToTime() + ` 연결된 장치들 중 constraints 제약 조건에 만족하는 장치 불러오고 성공시 MediaStream 반환됨`);
// 연결된 장치들 중 constraints 제약 조건에 만족하는 장치 불러오고
// 성공 시 MediaStream 반환됨
document.querySelector('#localVideo').srcObject = stream;
console.log(' / ' + msToTime() + ` 제약 조건에 만족하는 장치의 Mediastream : ` + stream);
stream.getTracks().forEach(track => peerConnection.addTrack(track, stream));
console.log(' / ' + msToTime() + `Mediastream 에서 트랙 목록을 얻고..?`);
// getTrack은 MediaStream 인터페이스(규격)의 함수
// 배열을 반환한다. 이 배열은 모든 mediaStreamtrack객체를 나타낸다.
// 트랙은 미디어를 기록하는 길이다.
}
let callButton = document.querySelector('#call');
// Handle call button
callButton.addEventListener('click', async () => {
console.log(' / ' + msToTime() + '----- call 버튼 클릭 리스너 함수 실행 ------');
const localPeerOffer = await peerConnection.createOffer();
console.log(' / ' + msToTime() + 'createoffer : SDP Offer 생성 -> 원격피어에 대한 새로운 webrtc 연결 시작 위해서');
await peerConnection.setLocalDescription(new RTCSessionDescription(localPeerOffer));
// 두 피어가 구성에 동의할 때까지 설명이 교환되기 때문에 setRemotDescription은 즉시 적용되지 않는다.
console.log(' / ' + msToTime() + 'setLocalDescription에 SDP Offer 인자로 넣어줌 : 연결과 관련된 로컬 설명을 변경');
console.log(' / ' + msToTime() + 'sendMediaOffer 함수 호출 : 시그널링 서버에 offer 보내주기 위해서');
// 미디어 형식과 연결의 로컬 끝 속성 지정
// 인자1: RTCSessionDescription:연결의 한쪽 끝(또는 잠재적 연결)과 구성 방법
sendMediaOffer(localPeerOffer);
});
// Create media offer
socket.on('mediaOffer', async (data) => {
console.log(' / ' + msToTime() + socket.id + ' --> mediaOffer : 웹브라우저에서 다른 유저의 offer 메시지 받음');
// remotePeerIceCandiate -> addEventListener 후에 다음 로그 실행
await peerConnection.setRemoteDescription(new RTCSessionDescription(data.offer));
console.log(' / ' + msToTime() + 'setRemoteDescription 원격 피어(상대방)의 offer 인자로 넣어줘서 환경설정 ');
//remotePeerIceCandiate 코드 실행
const peerAnswer = await peerConnection.createAnswer();
console.log(' / ' + msToTime() + 'createAnswer : 원격 피어로부터 받은 제안에 대한 sdp 응답 생성');
// 답변에는 세션에 이미 연결된 모든 미디어, 브라우저에서 지원하는 코덱 및 옵션,
// 이미 수집된 모든 ICE 후보에 대한 정보가 포함됨
await peerConnection.setLocalDescription(new RTCSessionDescription(peerAnswer));
console.log(' / ' + msToTime() + 'setLocalDescription에 위에서 만든 Answer을 사용해 연결과 관련된 로컬 설명을 변경');
console.log(' / ' + msToTime() + 'sendMediaAnswer 함수 호출');
sendMediaAnswer(peerAnswer, data); //sendMediaAnswer 함수 실행
});
// Create media answer
socket.on('mediaAnswer', async (data) => {
console.log(' / ' + msToTime() + '--> mediaAnswer / answer 메시지 받음');
console.log(' / ' + msToTime() + 'answer : ' + data);
await peerConnection.setRemoteDescription(new RTCSessionDescription(data.answer));
console.log(' / ' + msToTime() + 'setRemoteDescription 원격 피어(상대방)의 answer 인자로 넣어줘서 환경설정 ');
});
// ICE layer
peerConnection.onicecandidate = (event) => {
console.log(' / ' + msToTime() + '-------------> peer.onicecandidate event : ');
console.log(' / ' + msToTime() + ' event : ');
console.log(event);
console.log(' / ' + msToTime() + 'sendIceCandidate 함수 호출 / 인자로 event 넘겨줌');
sendIceCandidate(event);
}
socket.on('remotePeerIceCandidate', async (data) => {
console.log(' / ' + msToTime() + '--> remotePeerIceCandidate / candidate(후보자) 메시지 받음');
console.log(' / ' + msToTime() + 'remotePeerIceCandidate data : ' + data);
try {
console.log(' / ' + msToTime() + "try 다음 RTCICeCandidate 후보자 메시지를 인자를 넣어줌");
const candidate = new RTCIceCandidate(data.candidate);
console.log(' / ' + msToTime() + "candiate 후보를 자세히 설명하는 문자열 반환 ");
console.log(candidate);
await peerConnection.addIceCandidate(candidate);
console.log(' / ' + msToTime() + "peer.addIceCandidate 에 후보자를 인자로 넘겨줘서 새로운 원격 후보가 추가 됨");
} catch (error) {
console.log(' / ' + msToTime() + ' % % % % % ERROR_remotePeerIceCandidate % % % % % ');
console.log(error);
}
})
peerConnection.addEventListener('track', (event) => {
console.log(' / ' + msToTime() + '-------------------peer.addEventListener');
const [stream] = event.streams;
document.querySelector('#remoteVideo').srcObject = stream;
})
let selectedUser;
const sendMediaAnswer = (peerAnswer, data) => {
console.log(' / ' + msToTime() + '<-- sendMediaAnswer 함수 시작');
console.log(' / ' + msToTime() + '<-- mediaAnswer');
// console.log('<-- sendMediaAnswer / answer : ' + peerAnswer + '/' + '보내는 사람 : ' + socket.id + '/' + '받는 사람 ' + data.from);
socket.emit('mediaAnswer', {
answer: peerAnswer,
from: socket.id,
to: data.from
})
}
const sendMediaOffer = (localPeerOffer) => {
console.log(' / ' + msToTime() + 'sendMediaOffer 함수 시작');
console.log(' / ' + msToTime() + '<-- mediaOffer /msg : localPeerOffer');
socket.emit('mediaOffer', {
offer: localPeerOffer,
from: socket.id,
to: selectedUser
});
};
const sendIceCandidate = (event) => {
console.log(' / ' + msToTime() + 'sendIceCandidate 함수 실행');
console.log(' / ' + msToTime() + '<-- iceCandiate');
socket.emit('iceCandidate', {
to: selectedUser,
candidate: event.candidate
});
}
const onUpdateUserList = ({ userIds }) => {
console.log(' / ' + msToTime() + 'onUpdateUserList 함수 실행 : 매개변수 : 유저 리스트');
console.log(' / ' + msToTime() +
' : html 유저리스트 동적 추가 & selectUser 변수에 나를 제외한 참여한 유저 리스트를 넣어준다. ');
const usersList = document.querySelector('#usersList');
const usersToDisplay = userIds.filter(id => id !== socket.id);
usersList.innerHTML = '';
usersToDisplay.forEach(user => {
const userItem = document.createElement('div');
userItem.innerHTML = user;
userItem.className = 'user-item';
userItem.addEventListener('click', () => {
const userElements = document.querySelectorAll('.user-item');
userElements.forEach((element) => {
element.classList.remove('user-item--touched');
})
userItem.classList.add('user-item--touched');
selectedUser = user;
});
usersList.appendChild(userItem);
});
};
socket.on('update-user-list', onUpdateUserList);
const handleSocketConnected = async () => {
console.log(' / ' + msToTime() + '소켓 연결이 된 후에 handleSocketConnected 함수 실행 ');
console.log(' / ' + msToTime() + 'onSocketConnected 함수 호출함 : : 나의 mediaStream 얻기 위해서 ');
onSocketConnected();
socket.emit('requestUserList');
//나에게만 보냄
console.log(' / ' + msToTime() + '<-- requestUserList : 시그널링 서버에서 유저 리스트 업데이트 하는 과정');
};
socket.on('connect', handleSocketConnected);
// 소켓이 연결되면 handleSocketConneted 함수 실행됨
Webrtc 1:1 영상통화를 위한 화면 세팅하기 부분은 다음 글을 참고해주세요
https://stickode.tistory.com/716
1:1 영상통화 영상
'JavaScript' 카테고리의 다른 글
[JavaScript] 체크박스 커스텀하기 (0) | 2023.03.04 |
---|---|
[JavaScript]XmlHttpRequest 사용해서 http 통신하기. (0) | 2023.03.03 |
[javascript]정규식(regular expression) (0) | 2023.02.19 |
Formdata와 fetch를 활용하여 서버로 데이터 전송하기 (0) | 2023.02.15 |
[Javascript] Local storage를 사용하여 로그인 정보 저장하기. (0) | 2023.02.05 |