들어가며...
최근 WebRTC 에 대한 관심이 생겨서 클론 코딩을 해보았다. 이를 정리하고자 포스팅을 시작했습니다.
WebRTC : 출처 (MDN)
(Web Real-Time Communication)은 웹 애플리케이션과 사이트가 중간자 없이 브라우저 간에 오디오나 영상 미디어를 포착하고 마음대로 스트림할 뿐 아니라, 임의의 데이터도 교환할 수 있도록 하는 기술입니다
흔히 ZEP, gather 와 같은 메타버스 플랫폼 경우에 WebRTC를 활용하여 실시간 스트리밍을 가능하게 해줍니다.
1단계 장치 정보 받기
WebRTC로 P2P 통신을 하기 이전에 우선 장치 정보를 받아올 필요가 있습니다. 이때 사용하는 함수가
navigator.mediaDevices.getUserMedia 입니다. 이때 MediaDevices.enumerateDevices() 을 이용하면 그전 단계에서
사용 가능한 디바이스 정보를 가져올 수 있습니다.
const getMediaStream = useCallback(
async (faceMode?: string) => {
if (localStream) {
return localStream;
}
try {
const devices = await navigator.mediaDevices.enumerateDevices();
const videoDevices = devices.filter(
(divice) => divice.kind === 'videoinput'
);
const stream = await navigator.mediaDevices.getUserMedia({
audio: true,
video: {
width: { min: 640, ideal: 1280, max: 1920 },
height: { min: 360, ideal: 720, max: 1080 },
frameRate: { min: 16, ideal: 30, max: 30 },
facingMode: videoDevices.length > 0 ? faceMode : undefined,
},
});
setLocalStream(stream);
return stream;
} catch (error) {
console.log('failed to get stream', error);
setLocalStream(null);
return null;
}
},
[localStream]
);
2단계 PEER 연결하기
복잡한 P2P 연결에 대해서 알기 위해선 우선 네트워크에 관한 지식이 필요합니다. 비디오 스트리밍의 경우 데이터를 서버를 통해 직접적으로 전달하면 많은 부하가 생깁니다. 따라서 P2P 연결을 하는데, 이를 위해선 상대방의 IP 주소가 필요합니다. 이때 사용할 기술들은 보통 NAT, STUN 서버 ,ICE 입니다.
NAT (Network Address Translation)와 STUN
- NAT는 사설 네트워크(private network) 내에서 사용하는 private IP 주소를 공인 IP(public IP 주소)로 변경하는 기술입니다. 이는 보안상 이유로 외부와의 직접적인 연결을 차단하고, 여러 장치가 하나의 공인 IP를 공유할 수 있게 해줍니다.
- STUN (Session Traversal Utilities for NAT) 서버는 NAT 방화벽을 통과할 수 있도록 도와주는 역할을 합니다. 이 서버는 클라이언트에게 자신의 공인 IP 주소와 포트 번호를 알려주어, 외부에서 접근할 수 있도록 합니다. 하지만 STUN만으로는 NAT 뒤에 있는 장치 간의 연결을 완전히 해결할 수 없을 때도 있습니다.
ICE (Interactive Connectivity Establishment)
- ICE는 WebRTC에서 두 피어 간의 연결을 설정할 때 최적의 경로를 찾는 프레임워크입니다. ICE는 STUN과 TURN 서버를 활용하여, NAT 뒤에 있는 두 장치가 서로 연결할 수 있도록 도와줍니다.
- STUN은 NAT 방화벽을 통과할 수 있도록 공인 IP와 포트를 알려주는 데 사용됩니다.
- TURN은 직접적인 연결이 불가능한 경우 중계 서버를 통해 연결을 지원합니다.
따라서 우리는 장치정보를 바탕으로 peer를 생성할 것 입니다. 이때 구글의 무료 스턴 서버를 사용하였고, 연결이 되면 peer의 stream을 통해서 p2p 연결 후 데이터를 주고 받습니다.
const createPeer = useCallback(
(stream: MediaStream, initiator: boolean) => {
const iceServers: RTCIceServer[] = [
{
urls: [
'stun:stun.1.google.com:19302',
'stun:stun1.1.google.com:19302',
'stun:stun2.1.google.com:19302',
'stun:stun3.1.google.com:19302',
],
},
];
const peer = new Peer({
stream,
initiator,
trickle: true,
config: { iceServers },
});
peer.on('stream', (stream) => {
setPeer((prevPeer) => {
if (prevPeer) {
return { ...prevPeer, stream };
}
return prevPeer;
});
});
peer.on('error', console.error);
peer.on('close', () => handleHangup({}));
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const rtcPeerConnection: RTCPeerConnection = (peer as any)._pc;
rtcPeerConnection.onconnectionstatechange = async () => {
if (
rtcPeerConnection.iceConnectionState === 'disconnected' ||
rtcPeerConnection.iceConnectionState === 'failed'
) {
handleHangup({});
}
};
return peer;
},
[handleHangup]
);
이때 보편적으로 RTCPeerConnection 를 통해서 연결을 합니다. 하지만 해당 프로젝트에서는 simple-peer 라이브러리를 활용하여 해당 단계를 쉽게 구현할 수 있습니다.
이를 통해서 스턴 서버를 통해서 ice 정보를 받고 스트리밍이 된다면 상대방의 stream 정보를 받아서 peer에 할당합니다.
이후 해당 함수를 각각 전화거는 사람과 받는 사람이 사용하여 연결을 완료할 수 있습니다.
const completePeerConnection = useCallback(
async (connectionData: {
sdp: SignalData;
ongoingCall: OngoingCall;
isCaller: boolean;
}) => {
if (!localStream) {
console.log('Missing the localStream');
return;
}
if (peer) {
peer.peerConnection?.signal(connectionData.sdp);
return;
}
const newPeer = createPeer(localStream, true);
setPeer({
peerConnection: newPeer,
partipantUser: connectionData.ongoingCall.participants.receiver,
stream: undefined,
});
newPeer.on('signal', async (data: SignalData) => {
if (socket) {
// emit offer
socket.emit('webrtcSignal', {
sdp: data,
ongoingCall,
isCaller: true,
});
}
});
},
[createPeer, localStream, ongoingCall, peer, socket]
);
const handleJoinCall = useCallback(
async (ongoingCall: OngoingCall) => {
setIsCallEnded(false);
setOngoingCall((prev) => {
if (prev) {
return { ...prev, isRinging: false };
}
return prev;
});
const stream = await getMediaStream();
if (!stream) {
console.log('Could not get stream in handleJoinCall');
return;
}
const newPeer = createPeer(stream, true);
setPeer({
peerConnection: newPeer,
partipantUser: ongoingCall.participants.caller,
stream: undefined,
});
newPeer.on('signal', async (data: SignalData) => {
if (socket) {
// emit offer
socket.emit('webrtcSignal', {
sdp: data,
ongoingCall,
isCaller: false,
});
}
});
},
[createPeer, getMediaStream, socket]
);
이때 ICE 정보의 경우 SDP(Session Description Protocol)에 담겨서 전송되고, 이는 Socket의 webritcSignal을 통해서 유저간 실시간으로 주고 받습니다. 그리고 해당 SDP 정보를 기반으로 p2p 연결을 하면 데이터가 stream 됩니다.
자세한 통화 프로젝트의 코드는 https://www.youtube.com/watch?v=dpWAqVSjgTM&list=PL63c_Ws9ecIS8ReV9MISpUGU71CK5cy0V 통해서 확인할 수 있습니다.
참고문헌 및 출처
WebRTC
An open framework for the web that enables Real-Time Communications (RTC) capabilities in the browser.
webrtc.org
https://developer.mozilla.org/ko/docs/Web/API/WebRTC_API
WebRTC API - Web API | MDN
WebRTC(Web Real-Time Communication)은 웹 애플리케이션과 사이트가 중간자 없이 브라우저 간에 오디오나 영상 미디어를 포착하고 마음대로 스트림할 뿐 아니라, 임의의 데이터도 교환할 수 있도록 하는
developer.mozilla.org
https://www.npmjs.com/package/simple-peer
simple-peer
Simple one-to-one WebRTC video/voice and data channels. Latest version: 9.11.1, last published: 3 years ago. Start using simple-peer in your project by running `npm i simple-peer`. There are 296 other projects in the npm registry using simple-peer.
www.npmjs.com
https://www.youtube.com/watch?v=dpWAqVSjgTM&list=PL63c_Ws9ecIS8ReV9MISpUGU71CK5cy0V
[WebRTC] WebRTC란 무엇일까?
🎞 WebRTC란 무엇인가? Web Real-Time Communication의 약자로 웹/앱에서 별다른 소프트웨어 없이 카메라, 마이크 등을 사용하여 실시간 커뮤니케이션을 제공해주는 기술이다. 우리가 잘 알고있는 화상통
gh402.tistory.com
[WebRTC] NAT, ICE, STUN, TURN 이란? ( WebRTC를 이해하기 위해 필요한 지식들)
WebRTC를 사용하기 전, 기본적으로 익혀야 할 지식들!! 🌎 NAT(Network Address Translation) '나'는 누구인지 '이름'으로 구별할 수 있듯, 각 기기에도 자신만의 이름이 있다. 그것이 바로 IP이고 이 IP는 고
gh402.tistory.com
'프론트엔드 > React' 카테고리의 다른 글
간소화된 SPA 구현하기 (0) | 2024.11.17 |
---|---|
URL 기반 검색 기능 만들기 (URLSearchParams, Router) (1) | 2024.10.26 |
Intersection Observer API를 활용하여 무한 스크롤 구현하기 (0) | 2024.10.07 |
input 요소 uncontrolled와 controlled 관련 Warning (0) | 2024.05.01 |
React에서 불변성을 지켜야 하는 이유 (0) | 2024.04.11 |