Notice
Recent Posts
Recent Comments
Link
투케이2K
171. (TWOK/LOGIC) [Aws] AWS WebRTC 뷰어 SDP Offer 전송 후 Answer 응답을 받지 못하는 경우 peerConnection 연결 재시도 로직 본문
투케이2K 로직정리
171. (TWOK/LOGIC) [Aws] AWS WebRTC 뷰어 SDP Offer 전송 후 Answer 응답을 받지 못하는 경우 peerConnection 연결 재시도 로직
투케이2K 2026. 1. 27. 18:45728x90
[로직 정리]
정리 로직 : AWS / WebRTC / Viewer
상태 : [Aws] AWS WebRTC 뷰어 SDP Offer 전송 후 Answer 응답을 받지 못하는 경우 peerConnection 연결 재시도 로직

[설 명]
// --------------------------------------------------------------------------------------
[사전) 설정 및 정보 확인 사항]
// --------------------------------------------------------------------------------------
1. 제 목 : [Aws] AWS WebRTC 뷰어 SDP Offer 전송 후 Answer 응답을 받지 못하는 경우 peerConnection 연결 재시도 로직
2. 테스트 환경 : Aws / Kvs / WebRTC / Viewer
3. 사전) WebRTC 설명 :
>> WebRTC 란 웹, 애플리케이션, 디바이스 간 중간자 없이 오디오나 영상 미디어를 포착하고 실시간 스트림할 뿐 아니라, 임의의 데이터도 교환할 수 있도록 하는 기술입니다
>> WebRTC 는 간단한 API 를 통해 웹 브라우저, 모바일 애플리케이션 및 커넥티드 디바이스 간에 실시간 통신을 활성화할 수 있습니다
>> WebRTC 주요 용어 :
- SDP (Session Description Protocol) : 오디오/비디오 코덱, 해상도, 포트 등 스트리밍 정보를 담은 텍스트 포맷
- Offer / Answer : 통신 연결을 협상하기 위한 SDP 메시지 (초기 연결 설정)
- ICE (Interactive Connectivity Establishment) : NAT/P2P 환경에서도 연결 가능한 경로(IP, 포트 등)를 찾기 위한 기술
- Candidate : 가능한 연결 경로 (IP + Port 조합)
>> WebRTC SDP 오퍼 생성 (뷰어) 및 응답 (마스터) 스트리밍 플로우 :
[Viewer → Signaling Server] -- SDP Offer --> [Master] : 뷰어는 마스터로 스트리밍 오퍼 신호 보낸다
[Master] -- SDP Answer --> [Viewer] : 마스터는 특정 뷰어의 오퍼 신호 확인 후 응답을 보낸다
[Viewer] -- ICE Candidate --> [Master] : 스트리밍을 할 수 있는 경로 확인
[Master] -- ICE Candidate --> [Viewer] : 스트리밍을 할 수 있는 경로 확인
P2P 연결 성립 → 스트리밍 시작
4. ✅ 사전) WebRTC 중복 SDP Offer 호출 시 Answer 응답 누락 상태 정리
>> Stable 상태가 아닌데 Offer 가 또 전송된 경우
>> Glare (동시 Offer 충돌) : 두 Peer 가 동시에 Offer를 보내는 경우
>> setLocalDescription / setRemoteDescription 순서 오류
>> Answer 처리 타이밍이 Offer 전송보다 늦는 경우
>> ICE Restart 시 Offer를 연속해서 보내는 경우 (상대가 Answer를 만들지 못함)
5. ✅ 사전) WebRTC 디바이스 기기 환경에서 Answer 를 응답하지 못하는 흔한 케이스 정리
>> 네트워크 지연으로 인해 Offer/Answer 타이밍이 뒤틀림
>> 시그널링 서버에서 메시지가 순서 뒤집힘
>> 로컬 로직에서 Offer 재발행이 너무 빠르게 일어남
>> PeerConnection 재생성 없이 Offer만 다시 보내는 구조
>> 설계 상 “중복 Offer 차단 로직”이 없음
// --------------------------------------------------------------------------------------
// --------------------------------------------------------------------------------------
[로직 설명]
// --------------------------------------------------------------------------------------
1. AWS IAM 계정 정보를 사용해 AWS.KinesisVideo 객체 초기화 수행
kinesisVideoClient = new AWS.KinesisVideo({
region, // 리전
accessKeyId, // IAM 액세스 키
secretAccessKey, // IAM 시크릿 키
correctClockSkew: true,
});
2. getSignalingChannelEndpoint 메소드를 호출해 신호 채널에 연결할 HTTPS 및 WSS 엔드포인트가 할당 수행
const endpoints = await kinesisVideoClient.getSignalingChannelEndpoint({
ChannelARN: channelARN,
SingleMasterChannelEndpointConfiguration: {
Protocols: ['WSS', 'HTTPS'],
Role: KVSWebRTC.Role.VIEWER,
},
})
.promise();
const endpointsByProtocol = endpoints.ResourceEndpointList.reduce((acc, endpoint) => {
acc[endpoint.Protocol] = endpoint.ResourceEndpoint;
return acc;
}, {});
3. new AWS.KinesisVideoSignalingChannels 객체 생성 및 STUN 및 TURN ICE 서버 구성 가져오기 수행
const kinesisVideoSignalingChannelsClient = new AWS.KinesisVideoSignalingChannels({
region: region,
accessKeyId,
secretAccessKey,
endpoint: endpointsByProtocol.HTTPS,
correctClockSkew: true,
});
const iceServers = [];
// [TUN]
const iceRes = await kinesisVideoSignalingChannelsClient
.getIceServerConfig({
ChannelARN: channelARN,
})
.promise();
const tunServerList = iceRes.IceServerList;
tunServerList?.forEach((iceServer) => {
iceServers.push({
urls: iceServer.Uris,
username: iceServer.Username,
credential: iceServer.Password,
});
});
// [STUN]
const stunIceServers = { urls: `stun:stun.kinesisvideo.${region}.amazonaws.com:443` };
iceServers.push(stunIceServers); // [추가]
const config = {
iceServers: iceServers,
iceTransportPolicy: "all", // all | relay
};
4. new KVSWebRTC.SignalingClient 신호 채널을 통해 메시지 송수신 수행 객체 생성
signalingClient = new KVSWebRTC.SignalingClient({
channelARN,
channelEndpoint: endpointsByProtocol.WSS,
clientId,
role: KVSWebRTC.Role.VIEWER,
region,
credentials: {
accessKeyId : accessKeyId,
secretAccessKey : secretAccessKey,
},
systemClockOffset: kinesisVideoClient.config.systemClockOffset,
});
5. SignalingClient 이벤트 감지 리스너 등록 및 signalingClient.open(); 수행
>> signalingClient.on('open', async () => { }); - signalingClient 정상 open 상태 감지 리스너 : 해당 부분에서 뷰어의 오퍼 신호를 마스터에게 보냄
>> signalingClient.on('sdpAnswer', async answer => { }); - Master 의 SDP Answer 수신 대기
>> signalingClient.on('iceCandidate', (candidate, remoteClientId) => { }); - ICE Candidate 수신 대기
>> signalingClient.on('close', () => { }); - ICE 종료 핸들러
>> signalingClient.on('error', error => { }); - ICE 에러 핸들러
6. signalingClient.open(); 이 정상적으로 완료 된 경우 sdp Offer 메시지 전송 수행 및 SDP Answer 응답 대기 타이머 등록 실시
>> signalingClient.on('open', async () => {
// -----------------------------------------
// [카메라 및 오디오 사용 권한 요청 >> 트랙 스트림 추가]
// -----------------------------------------
const localStream = await navigator.mediaDevices.getUserMedia({ // [휴대폰 권한 요청]
video: true,
audio: true,
});
// [해당 트랙 추가로 > 마스터에게 비디오, 음성 데이터 전달]
localStream.getTracks().forEach(track => peerConnection.addTrack(track, localStream)); // [스트림 트랙에 추가]
// -----------------------------------------
// ✅ [SDP 오퍼 생성 및 마스터에게 전송]
// -----------------------------------------
const offer = await peerConnection.createOffer({
offerToReceiveAudio: true,
offerToReceiveVideo: true,
});
await peerConnection.setLocalDescription(offer);
signalingClient.sendSdpOffer(peerConnection.localDescription);
// -----------------------------------------
// ✅ Answer : TimeOut Handler Reg
// -----------------------------------------
answerTimer = setTimeout(() => {
try {
// ..... 코드 계속 .....
}
catch (exception) {
console.error("[WebRTC] : Answer : TimeOut : Exception : ", exception.message);
}
}, (30 * 1000) ); // 30 초간 Answer 응답 대기
});
7. 정상적으로 지정 된 시간 내에 sdpAnswer 응답을 받은 경우 등록 된 타이머 해제 및 peerConnection 에 answer description 추가 수행
signalingClient.on('sdpAnswer', async answer => {
console.warn("[setSystem] : [로컬 <-> 원격 연결 신호 확인] : [signalingClient] : [sdpAnswer] : [Receive]");
try {
// --------------------------------------
// ✅ Clear : Answer TimeOut Timer
// --------------------------------------
if (answerTimer != null){
clearTimeout(answerTimer);
answerTimer = null;
}
}
catch (exception) {
console.error("[WebRTC] : Answer : TimeOut : Close : Exception : ", exception.message);
}
await peerConnection.setRemoteDescription(answer);
});
8. 지정 된 시간 내에 SDP Answer 응답을 받지 못한 경우 answerTimer 핸들러 내에서 객체 초기화 및 일정 시간 후 다시 재접속 시도 코드 추가
answerTimer = setTimeout(() => {
console.error("[WebRTC] : Answer : TimeOut : Run");
try {
if (GLOBAL_RETRY > 0){ // ✅ GLOBAL_RETRY 변수 체크 : 재시도 이력 있음 >> 에러 팝업창 표시
// -----------------------------------------
// [에러 팝업창 알림]
// -----------------------------------------
C_SweetAlert_Error_OK("알 림", "[1] SDP Answer 응답을 받는데, 너무 오랜 시간이 소요되고 있습니다. WebRTC 접속 정보 재확인 및 기기 구동 상태를 다시 확인 후 접속해주세요.", "확인");
}
else {
closeConnection(); // ✅ WebRTC 연결 객체 종료 처리 수행
GLOBAL_RETRY ++; // ✅ 전역 변수 시도 횟수 카운트 증가
const connectTimer = setTimeout(() => {
console.error("[WebRTC] : Reconnect : Start");
// -----------------------------------------
// [로딩 프로그레스 종료]
// -----------------------------------------
startWebRTCReconnnect();
}, 6000 ); // ✅ 일정 시간 대기 후 재연결 시도
}
}
catch (exception) {
console.error("[WebRTC] : Answer : TimeOut : Exception : ", exception.message);
}
}, (30 * 1000) );
9. ✅ WebRTC 뷰어 입장 closeConnection 코드 추가 첨부 :
function closeConnection(){
console.log("[closeConnection] : WebRTC Connection 연결 종료 처리 수행");
try {
// --------------------------------------
// Clear : Answer TimeOut Timer
// --------------------------------------
if (answerTimer != null){
clearTimeout(answerTimer);
answerTimer = null;
}
}
catch (exception) {
console.error("[WebRTC] : Answer : TimeOut : Close : Exception : ", exception.message);
}
try{
// -----------------------------------------
// [signalingClient close : ICE candidate 교환이나 SDP 메시지 전송이 중단]
// -----------------------------------------
if (signalingClient != null){
console.log("[closeConnection] : signalingClient : close : Success");
signalingClient.close();
}
}
catch (exception) {
console.error("[closeConnection] : [signalingClient] : [Exception] : 예외 상황 발생");
}
try{
// -----------------------------------------
// [peerConnection removeTrack 처리 수행]
// -----------------------------------------
if (peerConnection != null){
console.log("[closeConnection] : peerConnection : removeTrack : Success");
const transceivers = peerConnection.getTransceivers?.() || [];
transceivers.forEach(t => {
try { if (typeof t.stop === 'function') t.stop(); } catch (e) {
console.warn('[cleanup] transceiver.stop error:', e);
}
});
const senders = peerConnection.getSenders?.() || [];
senders.forEach(s => {
try { peerConnection.removeTrack(s); } catch (e) {
console.warn('[cleanup] removeTrack error:', e);
}
});
}
}
catch (exception) {
console.error("[closeConnection] : peerConnection : removeTrack : 예외 상황 발생");
}
try{
// -----------------------------------------
// [peerConnection 도 연결 종료 처리 수행]
// -----------------------------------------
if (peerConnection != null){
console.log("[closeConnection] : peerConnection : close : Success");
peerConnection.close(); // 미디어 스트림 종료
}
}
catch (exception) {
console.error("[closeConnection] : [peerConnection] : [Exception] : 예외 상황 발생");
}
/*
try{
// -----------------------------------------
// [로컬 스트림도 종료 처리 수행]
// -----------------------------------------
localStream.getTracks().forEach(track => track.stop());
console.log("[closeConnection] : localStream : stop : Success");
}
catch (exception) {
console.error("[closeConnection] : [localStream] : [Exception] : 예외 상황 발생");
}
// */
try{
// -----------------------------------------
// [객체 null 초기화 수행]
// -----------------------------------------
signalingClient = null;
peerConnection = null;
// localStream = null;
console.log("[closeConnection] : object : clear : Success");
}
catch (exception) {
console.error("[closeConnection] : [object] : [Exception] : 예외 상황 발생");
}
};
// --------------------------------------------------------------------------------------
// --------------------------------------------------------------------------------------
[참고 사이트]
// --------------------------------------------------------------------------------------
[Aws Kinesis Video Streams] WebRTC 중복 SDP Offer 호출 시 Answer 응답 누락 상태 정리
https://kkh0977.tistory.com/8582
https://blog.naver.com/kkh0977/224153654842
[업무 이슈] AWS KVS WebRTC 뷰어 실시간 영상 재생 시 짧은 시간 내에 다중 접속 시도 시 SDP Answer 응답을 받지 못하는 이슈
https://blog.naver.com/kkh0977/224119423929
[업무 이슈] AWS WebRTC 마스터 SDK 초기화 시 STS 임시 자격 증명 UserId 값에 특수 문자 포함으로 인증 에러 발생 이슈
https://blog.naver.com/kkh0977/224145469350
[업무 이슈] AWS WebRTC 뷰어 실시간 영상 재생 중 네트워크 환경 변경 (Wifi To LTE) 으로 인한 영상 재생 중지 이슈
https://blog.naver.com/kkh0977/224156200130
[Aws Kinesis Video Streams] WebRTC SDP 협상 과정 프로세스 정리 정리
https://blog.naver.com/kkh0977/224030054470
[자바스크립트 AWS WebRTC 실시간 동영상 재생 수행]
https://blog.naver.com/kkh0977/223170500993?trackingCode=blog_bloghome_searchlist
[Aws Kvs WebRTC 실시간 영상 재생 관련 구성 요소 및 용어 정리]
https://blog.naver.com/kkh0977/223858189791
[Aws Kinesis Video Streams] WebRTC remote sender clientId 클라이언트 아이디 설명, 규격 및 제한 정리
https://blog.naver.com/kkh0977/224083731976
[업무 이슈] AWS WebRTC 실시간 비디오 재생 시 Client 클라이언트 연결 접속 및 해제 상태 확인 이슈
https://blog.naver.com/kkh0977/223966952222
// --------------------------------------------------------------------------------------
728x90
반응형
'투케이2K 로직정리' 카테고리의 다른 글
Comments
