1. ICE candidate 교환 과정
offer 와 answer 를 교환하는 과정에서 ICE candidate 생성되며 이후 시그널링 서버를 통해 교환 되어 실제 peer 간 연결이 완료됩니다.
이전 포스트와 이어집니다. RTCPeerConnection 간 연결 과정 (offer, answer)
참고할만한 내용
📕 기본적인 시그널링 서버 및 클라이언트 연결 관련 포스트: 시그널링 서버 연결 및 미디어 장치 연결
RTCPeerConnection 및 ICE candidate 관련 포스트: RTCPeerConnection 이해와 관련된 주요 인터페이스
2. ICE candidate 교환 과정
두 사용자 A, B 가 offer/answer 주고 받는 과정에서 ICE candidate 교환 시점과 흐름 예시
1️⃣ A 측
- 새로 입장한 B 와 연결을 위한
RTCPeerConnection
생성 createOffer()
→setLocalDescription(offer)
- 이 시점에 ICE gathering(ICE Candidate 수집) 시작
- ICE Candidate 생성 되는 시점에
RTCPeerConnection.onicecandidate
이벤트가 비동기적으로 호출되며 이 과정에서 시그널링 서버를 통해 ICE Candidate 를 B 에게 전송
- offer를 시그널링 서버 통해 B에게 전송
2️⃣ B 측
- 기존 사용자 A 와 연결을 위한
RTCPeerConnection
생성 - 시그널링 서버를 통해 offer 전달 받음 ->
setRemoteDescription(offer)
이 시점에 ICE gathering(ICE Candidate 수집) 시작- ✅ offer 수신 시 ICE candidate는 수집 중이거나 이미 수집된 상태일 수 있음 (이전 포스트 참고)
- 동시에 시그널링 서버를 통해 A의 ICE candidate 들도 비동기적으로 수신 후
addIceCandidate(candidate)
를 통해 자신의 PeerConnection에 즉시 추가
createAnswer()
→setLocalDescription(answer)
- 이 시점에 ICE gathering(ICE Candidate 수집) 시작
- A 측과 마찬가지로
RTCPeerConnection.onicecandidate
이벤트가 비동기적으로 호출되며 이 과정에서 시그널링 서버를 통해 ICE Candidate 를 A 에게 전송
- answer를 시그널링 서버 통해 A에게 전송
3️⃣ A 측
- 시그널링 서버를 통해 answer 전달 받음 ->
setRemoteDescription(answer)
- B 측과 마찬가지로 동시에 시그널링 서버를 통해 B의 ICE candidate 들도 비동기적으로 수신 후
addIceCandidate(candidate)
를 통해 자신의 PeerConnection에 즉시 추가
- B 측과 마찬가지로 동시에 시그널링 서버를 통해 B의 ICE candidate 들도 비동기적으로 수신 후
3. 구현 코드
※ 예시로 작성한 코드이며 일부 내용을 생략한 관계로 참고만 하시길 추천드립니다. ※
✅ 생략한 내용 및 추가할만한 부분
- 3인 이상의 peer 연결 및 connection 관리에 필요한 로직 (아래 예시는 간단한 1 대 1 상황을 예시로 구현)
- 클라이언트에서는 peer 의 고유한 id 를 부여, 시그널링 서버에서는 해당 id 를 기반으로 사용자 목록 사용자 및 device 상태 목록 관리
- 클라이언트에서 다른 사용자들의 Stream 및 PeerConnection 목록 관리 (시그널링 서버에서 사용자 목록도 가져와 활용 가능)
- 클라이언트에서 상위에 서술한 WebRTC의 ICE Candidate 처리 순서에 따른 에러 가능성에 따른 핸들링
클라이언트 코드
const VideoChat = () => {
const [myStream, setMyStream] = useState<MediaStream>(new MediaStream());
const [someoneStream, setSomeoneStream] = useState<MediaStream>(new MediaStream());
const peerConnection = useRef<RTCPeerConnection>(new RTCPeerConnection());
// 입장 함수
const enterRoom = async () => videoChatSocket.emit("enter_room");
// 새로운 PeerConnection 생성 함수
const createPeerConnection = useCallback(() => {
const pc = new RTCPeerConnection(); // 새로운 RTCPeerConnection 객체 생성
// 내 로컬 스트림의 모든 트랙(audio, video 등)을 PeerConnection에 추가
myStream.getTracks().forEach((track) => {
pc.addTrack(track, myStream);
});
// ICE 후보가 생성될 때마다 signaling 서버로 전송
pc.onicecandidate = (event: RTCPeerConnectionIceEvent) => {
videoChatSocket.emit("send_ice", event.candidate);
};
// 상대방으로부터 media 트랙(audio, video)이 들어올 때 실행
pc.ontrack = (event: RTCTrackEvent) => {
// media stream 에 track 등록
setSomeoneStream((prev) => {
prev.addTrack(event.track);
return prev;
});
};
// 연결 상태가 변경될 때 실행되는 핸들러
pc.onconnectionstatechange = () => {
const state = pc.connectionState;
// failed 된 경우 close 하여 메모리 내부 자원 정리, 메모리 누수 방지
if (state === "failed") {
peerConnection.current.close();
}
if (state === "closed") {
setSomeoneStream(new MediaStream()); // 연결이 끊긴 peer 의 stream 정리
}
};
return pc;
}, [myStream]);
useEffect(() => {
// 다른 사용자 입장시 실행되는 이벤트 핸들러 (offer 생성 및 전달 과정)
// 시그널링 서버에서 다른 사용자 입장시 그외 사용자들에게 "user_joined" 이벤트를 생성하는 구조
const onJoinUser = async () => {
// 새로 입장한 사용자와의 PeerConnection 생성
const newPC = createPeerConnection();
// 로컬 미디어 트랙 추가
myStream.getTracks().forEach((track) => {
newPC.addTrack(track, myStream);
});
const offer = await newPC.createOffer(); // Offer 생성
await newPC.setLocalDescription(offer); // offer 저장
peerConnection.current = newPC; // PeerConnection 저장
// 시그널링 서버에 offer 전달
videoChatSocket.emit("send_offer", offer);
};
// 서버에서 offer 를 전달 받을 때 실행되는 이벤트 핸들러
const onReceiveOffer = async (offer: RTCSessionDescriptionInit) => {
const newPC = createPeerConnection(); // 기존 사용자와의 PeerConnection 생성
await newPC.setRemoteDescription(offer); // 기존 사용자에게 전달받은 offer 저장
const answer = await newPC.createAnswer(); // 기존 사용자에게 전달할 answer 생성
await newPC.setLocalDescription(answer); // answer 저장
peerConnection.current = newPC; // PeerConnection 저장
// 시그널링 서버에 answer 전달
videoChatSocket.emit("send_answer", answer);
};
// 기존 사용자가 새로 입장한 사용자에 answer 전달받는 경우에 이벤트 핸들러
const onReceiveAnswer = async (answer: RTCSessionDescriptionInit) => {
await peerConnection.current.setRemoteDescription(answer); // 새로 입장한 사용자에게 전달받은 answer 저장
};
// 기존 사용자가 새로 입장한 사용자에 ICE 후보를 전달받는 경우 처리하는 이벤트 핸들러
const onReceiveICE = async (ice: RTCIceCandidate) => {
await peerConnection.current.addIceCandidate(ice); // PeerConnection 에 ICE candidate 저장
};
// 이벤트 핸들러 등록
videoChatSocket.on("user_joined", onJoinUser);
videoChatSocket.on("receive_offer", onReceiveOffer);
videoChatSocket.on("receive_answer", onReceiveAnswer);
videoChatSocket.on("receive_ice", onReceiveICE);
// 프로세스 종료 후 이벤트 정리
return () => {
videoChatSocket.off("user_joined", onJoinUser);
videoChatSocket.off("receive_offer", onReceiveOffer);
videoChatSocket.off("receive_answer", onReceiveAnswer);
videoChatSocket.off("receive_ice", onReceiveICE);
};
}, [createPeerConnection, myStream]);
return (
<div>
<button onClick={enterRoom}>입장</button>
</div>
);
};
서버 코드
socketServer.on("connection", (socket) => {
console.log(`🔵Client connected video chat ws server: ${socket.id}`);
// 입장
socket.on("enter_room", () => {
socket.join("videoChatRoom"); // "videoChatRoom" 라는 이름의 room 에 입장
// 다른 사용자에게 "user_joined" 이벤트 발송
socket.to("videoChatRoom").emit("user_joined");
});
// offer 전달
socket.on("send_offer", (offer: RTCSessionDescriptionInit) => {
// 다른 사용자에게 offer 와 함께 "receive_offer" 이벤트 발송
socket.to("videoChatRoom").emit("receive_offer", offer);
});
// answer 전달
socket.on("send_answer", (answer: RTCSessionDescriptionInit) => {
// 다른 사용자에게 answer 와 함께 "receive_answer" 이벤트 발송
socket.to("videoChatRoom").emit("receive_answer", answer);
});
// ice 전달
socket.on("send_ice", (ice: RTCIceCandidate) => {
console.log("send_ice");
socket.to("roomName").emit("receive_ice", ice);
});
// 연결 종료시 room 에서 퇴장
socket.on("disconnecting", () => {
socket.leave("videoChatRoom");
});
});
'websocket & webRTC' 카테고리의 다른 글
4. RTCPeerConnection 간 연결 과정 (offer, answer) (0) | 2025.04.28 |
---|---|
3. RTCPeerConnection 이해와 관련된 주요 인터페이스 (0) | 2025.04.22 |
2. 시그널링 서버 연결 및 미디어 장치 연결 (0) | 2025.04.18 |
1. 시그널링 서버 (0) | 2025.04.17 |
0. Web RTC 이해 (0) | 2025.04.16 |