728x90
안녕하세요 이번 시간에는 Webrtc N:M 실시간으로 영상 전송을 구현해보겠습니다.
이 글을 보기 전에 해당 링크를 참고해주세요. ( https://stickode.tistory.com/715)
구현을 위한 코드에만 초점을 맞춰서 설명하겠습니다.
server.js
const express = require('express');
const path = require('path');
const app = express();
const http = require('http').createServer(app);
const io = require('socket.io')(http);
app.use(express.static(path.join(__dirname, '../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 메시지 웹브라우저로 보냄 '
);
// console.log(socket.id + ' mediaOffer data.to : ' + data.to + ' data.from :
// ' + data.from + ' data.offer : ' + data.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 메시지 웹브라우저로 ' +
'보냄 '
);
// console.log(socket.id + ' mediaAnswer data to :' + data.to + ' data.from
// : ' + data.from + ' data.offer :' + data.offer);
});
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'); //콘솔에 로그 주석을 인쇄한다.
});
index.js
// Creating the peer 로컬 컴퓨터와 원격 피어 간의 웹 RTC 연결하기 위해 필요한 객체 연결을 유지 및 모니터링함 또한 더이상
// 필요하지않으면 연결을 닫을 수 있는 방법 제공 이 객체는 어떻게 peer 연결 설정할지, 사용할 ice 서버의정보 Connecting to
// socket
// "use strict";
const socket = io('http://서버주소:포트번호');
let stream;
// let pcs = {};
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, 마이크 에코 등을 설정가능
video: true
};
console.log(' / ' + msToTime() +
' constraints 변수 생성 및 초기화 : 제약 조건(해상도, 마이크 에코 등을 설정)');
stream = await navigator.mediaDevices.getUserMedia(constraints);
console.log(' / ' + msToTime() + ` stream 변수 생성 및 초기화 : 연결된 장치들 중
constraints 제약 조건에 만족하는 장치 불러오고 성공시 MediaStream 반환됨 `);
document.querySelector('#localVideo').srcObject = stream;
console.log(' / ' + msToTime() + ' stream 변수를 html 비디오 부분에 넣어줌. ');
console.log(' / ' + msToTime() + ` MediaStreamTrack : 스트림 종류, peerConnection 등 여러가지 설정이 담겨 있다.`);
socket.emit('requestUserList');
// getTrack은 MediaStream 인터페이스(규격)의 함수 배열을 반환한다. 이 배열은 모든 mediaStreamtrack객체를
// 나타낸다. 트랙은 미디어를 기록하는 길이다.
}
// Create media offer
socket.on('mediaOffer', async (data) => {
console.log(
' / ' + msToTime() + socket.id + ' --> mediaOffer : 웹브라우저에서 다른 유저의 offer 메시지 받고' +
' peerConnection 생성');
let peerConnection = new RTCPeerConnection({
iceServers: [{ urls: "stun:stun.stunprotocol.org" }]
});
stream.getTracks().forEach(track => peerConnection.addTrack(track, stream));
console.log(' / ' + msToTime() + ` stream.getTrack() : MediaStreamTrack 반환함`);
let pc = data.from;
selectedUser_id.push(pc);
pcs.push(peerConnection);
// console.log(' / ' + msToTime() + '추가전의 pcs 배열 값 : ' + pcs);
// console.log(' / ' + msToTime() + ` 변수 pc에 offer를 보낸 caller의 소켓 아이디 넣어줌 / pc : ` + pc);
// pcs.push({ pc: peerConnection });
// console.log(' / ' + msToTime() + '추가후의 pcs 배열 값 : ' + pcs);
peerConnection.addEventListener("icegatheringstatechange", (ev) => {
switch (peerConnection.iceGatheringState) {
case "new":
console.log(' / ' + msToTime() + 'new');
break;
case "gathering":
console.log(' / ' + msToTime() + 'gathering');
break;
case "complete":
console.log(' / ' + msToTime() + 'complete');
break;
}
});
// 인자 1 : track : addTrack은 다른 피어로 전송될 트랙 집합에 새로운 미디어 트랙을 추가한다. 새로운 미디어 트랙을
// 추가한다는 것은 무슨 의미지? 상태에 대한 걱정 없이 미디어를 추가하거나 제거 할 수 있는 방식이라는데 -> 검증안됨 연결에 트랙을
// 추가하면 협상에 필요한 이벤트가 발생하여 재협상이 트리거 된다. ICE layer
peerConnection.onicecandidate = (event) => {
console.log(' / ' + msToTime() + ' --- onicecandidate 이벤트 핸들러 icecandidate에 관한 이벤트핸들러 대부분 이벤트가 후보자가 더해졌을 때임');
console.log(' / ' + msToTime() +
' setLocalDescrition()을 통해 후보자가 구분되고 로컬피어에 추가되면 이벤트 핸들러 실행됨');
console.log(' / ' + msToTime() +
' 이벤트 핸들러은 콜백 루틴이기 때문에 이 코드 상에서 원격 피어에 candidate들을 추가할 수 있도록 해야함');
// 이벤트 핸들러란? 어떠한 이벤트가 발생되었을 때 비동기적(기다리지 않음)으로 작동하는 콜백 루틴
// 이벤트에 뒤따르는 행동을 지시함
console.log(' / ' + msToTime() + 'event : ');
console.log(event);
if (event.candidate) {
console.log(' / ' + msToTime() + '새로 더해진 후보자가 있을 때만 if 문 실행 후 sendIceCandidate 호출 ');
console.log(' / ' + msToTime() + 'sendIceCandidate 함수 호출 / 인자 1 : event , 인자 2 :callee 소켓아이디: ' + pc);
sendIceCandidate(event, pc);
}
else {
console.log(' / ' + msToTime() + '추가된 후보자가 없을 떄 else 문 ');
/* there are no more candidates coming during this negotiation */
}
}
peerConnection.addEventListener('track', (event) => {
console.log(
' / ' + msToTime() + '-------------------peer.addEventListener'
);
// let [remoteStream] = event.streams;
// document.querySelector('#remoteVideo').srcObject = remoteStream;
const [stream] = event.streams;
const tagArea = document.getElementById("video-grid");
//집어넣을 영역
const newVideo = document.createElement('video');
newVideo.autoplay = true;
newVideo.srcObject = stream;
// 새로운 비디오 태그 만들기
tagArea?.appendChild(newVideo);
})
// remotePeerIceCandiate -> addEventListener 후에 다음 로그 실행
await peerConnection.setRemoteDescription(new RTCSessionDescription(data.offer));
console.log(' / ' + msToTime() + 'setRemoteDescription 원격 피어(상대방)의 offer 인자로 넣어줘서 환경설정');
const peerAnswer = await peerConnection.createAnswer();
console.log(' / ' + msToTime() + 'createAnswer : 원격 피어로부터 받은 제안에 대한 sdp 응답 생성');
// 답변에는 세션에 이미 연결된 모든 미디어, 브라우저에서 지원하는 코덱 및 옵션, 이미 수집된 모든 ICE 후보에 대한 정보가 포함됨
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);
const pc = data.from;
console.log(' / ' + msToTime() + 'answer pcs 배열 값 : ' + pcs);
for (let i = 0; i < selectedUser.length; i++) {
console.log(` / ${msToTime()}answer 보낸 사람 : ${pc} for 문 ${i}번째`);
console.log(' / ' + msToTime() + 'pcs[i] 값 확인 해볼까 ' + selectedUser_id[i]);
if (selectedUser_id[i] == pc) {
console.log(' / ' + msToTime()
+ 'mediaAnswer를 보낸 유저 소켓 아이디와 pcs 배열 중에서 키값이 일치할 때 : ' + pcs[i]);
let peerConnection = pcs[i];
await peerConnection.setRemoteDescription(new RTCSessionDescription(data.answer));
console.log(' / ' + msToTime()
+ 'setRemoteDescription 원격 피어(상대방)의 answer 인자로 넣어줘서 환경설정 ');
break;
}
}
});
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);
for (let i = 0; i < selectedUser.length; i++) {
console.log(' / ' + msToTime() + " -------remotePeerIcecandidate for문 " + i + '번째');
let pc = data.from;
console.log(' / ' + msToTime() + " 변수 pc 에 candidate 후보자를 보내준 유저의 socket id 값 넣어줌");
console.log(' / ' + msToTime() + " 후보자 보낸 사람 : " + pc);
console.log(' / ' + msToTime() + 'pcs[i] 값 확인 해볼까 ' + selectedUser_id[i]);
if (selectedUser_id[i] == pc) {
console.log(' / ' + msToTime() + " pcs[{socket.id : peerConnection}] 에서 보내준 유저의 socket id와 일치하는 키 값이 있다면 if 문 실행됨");
console.log(' / ' + msToTime() + ' candidate 를 보낸 유저 소켓 아이디와 pcs 배열 중에서 키값이 일치할 때' + pcs[i]);
const peerConnection = pcs[i];
await peerConnection.addIceCandidate(candidate);
console.log(' / ' + msToTime() + "addIceCandidate 는 새로 받은 후보자를 브라우저 ice 에이전트에 전달함 *-ice agent 는 연결관리를 한다고 한다.");
}
}
} catch (error) {
console.log(
' / ' + msToTime() + ' % % % % % ERROR_remotePeerIceCandidate % % % % % ');
console.log(error);
}
})
const sendMediaAnswer = (peerAnswer, data) => {
console.log(' / ' + msToTime() + '<-- sendMediaAnswer 함수 시작/ 서버에 answer 보냄');
console.log(' / ' + msToTime() + '<-- mediaAnswer');
socket.emit('mediaAnswer', {
answer: peerAnswer,
from: socket.id,
to: data.from
})
}
const sendMediaOffer = (localPeerOffer, toUser) => {
console.log(' / ' + msToTime() + 'sendMediaOffer 함수 시작 / 서버에 offer 보냄');
console.log(' / ' + msToTime() + '<-- mediaOffer');
socket.emit('mediaOffer', {
offer: localPeerOffer,
from: socket.id,
to: toUser
});
};
const sendIceCandidate = (event, toUser) => {
console.log(' / ' + msToTime() + 'sendIceCandidate 함수 실행 / 서버에 후보자 보냄');
console.log(' / ' + msToTime() + '<-- iceCandiate');
socket.emit('iceCandidate', {
from: socket.id,
to: toUser,
candidate: event.candidate
});
}
let allUserList; //서버로부터 받는 현재 참가한 유저 리스트
let selectedUser = []; // 나를 제외한 유저 리스트
let selectedUser_id = [];
let pcs = []; //[{socket.id : peerConnection}]
let localPeerOffer; // offer 생성후 담는 변수
const onUpdateUserList = async ({ userIds }) => { // 업데이트 된 유저 리스트 받는다.
console.log(' / ' + msToTime() + 'onUpdateUserList 함수 실행 : 매개변수 : 유저 리스트');
allUserList = userIds; // 현재 참여한 유저리스트 allUserList 에 넣어줌
selectedUser = userIds.filter(id => id !== socket.id);
console.log(' / ' + msToTime() + ' selectUser 초기화 : 나를 제외한 참여한 유저 리스트를 넣어준다. ');
console.log(' / ' + msToTime() + ' selectUser 값 : ' + selectedUser);
let userIdsCount = userIds.length; // 현재 유저 수
if (userIdsCount > 1 && socket.id == userIds[userIdsCount - 1]) { //Caller 만 if 문 통과
console.log(' / ' + msToTime() +
'나를 제외한 유저 수가 1이상 그리고 가장 마지막으로 들어온 유저 socket.id 가 내 socket.id 와 같을 때 if 문실행');
for (let i = 0; i < userIdsCount - 1; i++) {
console.log(' / ' + msToTime() + '----- updateUserList 의 for문 나를 제외한 유저 수 만큼 반복 ------- ');
const peerConnection = new RTCPeerConnection({ iceServers: [{ urls: "stun:stun.stunprotocol.org" }] });
console.log(' / ' + msToTime() + 'peerConnection 생성');
selectedUser_id.push(selectedUser[i]);
pcs.push(peerConnection);
// console.log(' / ' + msToTime() + 'pc 변수에 유저 소켓 아이디 값을 초기화 해줌 / pc 값 : ' + pc);
// console.log(' / ' + msToTime() + '추가전의 pcs 배열 값 : ' + pcs);
// pcs.push({ pc: peerConnection });
// console.log(' / ' + msToTime() + '배열에 {socket.id : peerConnection}을 값으로 넣어줌 / pcs 배열 값 : ' + pcs);
stream
.getTracks()
.forEach(track => peerConnection.addTrack(track, stream));
console.log(' / ' + msToTime() + ` stream.getTrack() : MediaStreamTrack 반환된 것을 addTrack 해줌`);
peerConnection.addEventListener("icegatheringstatechange", (ev) => {
switch (peerConnection.iceGatheringState) {
case "new":
console.log(' / ' + msToTime() + 'new');
break;
case "gathering":
console.log(' / ' + msToTime() + 'gathering');
break;
case "complete":
console.log(' / ' + msToTime() + 'complete');
break;
}
});
// ICE layer
peerConnection.onicecandidate = (event) => {
console.log(' / ' + msToTime() + ' --- onicecandidate 이벤트 핸들러 icecandidate에 관한 이벤트핸들러 대부분 이벤트가 후보자가 더해졌을 때임');
console.log(' / ' + msToTime() +
' setLocalDescrition()을 통해 후보자가 구분되고 로컬피어에 추가되면 이벤트 핸들러 실행됨');
console.log(' / ' + msToTime() +
' 이벤트 핸들러은 콜백 루틴이기 때문에 이 코드 상에서 원격 피어에 candidate들을 추가할 수 있도록 해야함');
// 이벤트 핸들러란? 어떠한 이벤트가 발생되었을 때 비동기적(기다리지 않음)으로 작동하는 콜백 루틴
// 이벤트에 뒤따르는 행동을 지시함
console.log(' / ' + msToTime() + 'event : ');
console.log(event);
if (event.candidate) {
console.log(' / ' + msToTime() + '새로 더해진 후보자가 있을 때만 if 문 실행 후 sendIceCandidate 호출 ');
console.log(' / ' + msToTime() + 'sendIceCandidate 함수 호출 / 인자 1 : event , 인자 2 :' + i + '번째 callee 소켓아이디');
sendIceCandidate(event, selectedUser[i]);
}
else {
console.log(' / ' + msToTime() + '추가된 후보자가 없을 떄 else 문 ');
/* there are no more candidates coming during this negotiation */
}
}
peerConnection.addEventListener('track', (event) => {
console.log(
' / ' + msToTime() + '-------------------peer.addEventListener');
// const [remoteStream] = event.streams;
// document.querySelector('#remoteVideo').srcObject = remoteStream;
const [stream] = event.streams;
const tagArea = document.getElementById("video-grid");
//집어넣을 영역
const newVideo = document.createElement('video');
newVideo.autoplay = true;
newVideo.srcObject = stream;
// 새로운 비디오 태그 만들기
tagArea.appendChild(newVideo);
console.log(' / ' + msToTime() + 'remotePeer stream 세팅 ');
});
console.log(
' / ' + msToTime() + '----- Offer 생성 시작 ------');
localPeerOffer = await peerConnection.createOffer();
console.log(' / ' + msToTime() + ' createoffer 변수 : SDP Offer 생성 -> 원격피어에 대한 새로운 webrtc 연결 시작 위해서');
peerConnection.setLocalDescription(new RTCSessionDescription(localPeerOffer));
// 두 피어가 구성에 동의할 때까지 설명이 교환되기 때문에 setRemotDescription은 즉시 적용되지 않는다.
console.log(
' / ' + msToTime() + ' setLocalDescription에 SDP Offer 인자로 넣어줌 : 연결과 관련된 로컬 설명을' +
' 변경'
);
console.log(
' / ' + msToTime() + ' sendMediaOffer 함수 호출 : 시그널링 서버에 offer 보내주기 위해서'
);
sendMediaOffer(localPeerOffer, selectedUser[i]);
// 미디어 형식과 연결의 로컬 끝 속성 지정 인자1: RTCSessionDescription:연결의 한쪽 끝(또는 잠재적 연결)과 구성 방법
}
console.log(' / ' + msToTime() + '----- updateUserList 의 if문 끝 ------- ');
}
};
socket.on('update-user-list', onUpdateUserList);
const handleSocketConnected = async () => {
console.log(' / ' + msToTime() + 'socket : ' + socket.id);
console.log(
' / ' + msToTime() + '소켓이 연결이 되면 handleSocketConnected 함수 호출됨'
);
console.log(
' / ' + msToTime() + '---handleSocketConnected(카메라 접근 및 서버에 유저 리스트 요청)--'
);
console.log(
' / ' + msToTime() + 'onSocketConnected 함수 호출할 예정 : 내 카메라와 마이크에 접근하기 위해서 '
);
onSocketConnected();
//나에게만 보냄
};
socket.on('connect', handleSocketConnected);
// 소켓이 연결되면 handleSocketConneted 함수 실행됨
'HTML/CSS' 카테고리의 다른 글
[HTML/CSS] ] css만으로 움직이는 배너 만들기 (0) | 2023.05.30 |
---|---|
[HTML/CSS] CSS만으로 마우스 hover시 효과 내기 (0) | 2023.05.14 |
[HTML/CSS] Webrtc 1:1 영상통화를 위한 화면 세팅하기 (0) | 2023.02.24 |
[ HTML/CSS ] 회원가입 화면 만들기 (0) | 2022.12.26 |
[HTML/CSS] 웹 사이트에서 모바일 화면 레이아웃 보여주기 (0) | 2022.12.24 |