투케이2K

167. (TWOK/LOGIC) [Aws] 자바스크립트 AWS KVS WebRTC 뷰어 실시간 영상 재생 시 SDP Answer 응답을 받지 못한 경우 Retry 재시도 로직 본문

투케이2K 로직정리

167. (TWOK/LOGIC) [Aws] 자바스크립트 AWS KVS WebRTC 뷰어 실시간 영상 재생 시 SDP Answer 응답을 받지 못한 경우 Retry 재시도 로직

투케이2K 2025. 12. 24. 08:53
728x90

[로직 정리]

정리 로직 : Aws / Web / JavaScript / WebRTC

상태 : [Aws] 자바스크립트 AWS KVS WebRTC 뷰어 실시간 영상 재생 시 SDP Answer 응답을 받지 못한 경우 Retry 재시도 로직

 

[설 명]

// --------------------------------------------------------------------------------------
[사전) 설정 및 정보 확인 사항]
// --------------------------------------------------------------------------------------

1. 제 목 : [Aws] 자바스크립트 AWS KVS WebRTC 뷰어 실시간 영상 재생 시 SDP Answer 응답을 받지 못한 경우 Retry 재시도 로직


2. 테스트 환경 : JavaScript (자바스크립트) / AWS / KVS / WebRTC


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 ClientId 규격 및 제한 설명 : clientId는 각 연결을 구분하는 세션 키 역할을 합니다

  >> 문자 허용 범위 : 

    - 영문자 (a-z, A-Z) : 대소문자 구분 있음

    - 숫자 (0-9)

    - 일부 특수문자 ( - (하이픈) , _ (언더바) , . (닷지 == 점) ) 허용 (URL-safe 문자는 대부분 허용)

    - 공백, 슬래시(/), 백슬래시(\), 콜론(:), 세미콜론(;), 따옴표, @ (엣) , # (샵) 등은 허용되지 않음 (AWS SDK 내부에서 URL-safe encoding을 요구하기 때문)

  >> 길이 제한 : 

    - 최대 256자 (AWS 공식 문서 기준) : 너무 긴 값은 Signaling API에서 오류 발생 가능

  >> 중복 불가 : 동일 채널에서 중복된 clientId 사용 시 기존 연결이 끊김 (각 뷰어는 반드시 고유한 clientId를 사용해야 함)


5. 사전) 자바스크립트에서 CDN 의존성 설정 코드 : 

  <script src="https://unpkg.com/amazon-kinesis-video-streams-webrtc/dist/kvs-webrtc.min.js"></script>
  <script src="https://sdk.amazonaws.com/js/aws-sdk-2.1416.0.min.js"></script>

// --------------------------------------------------------------------------------------






// --------------------------------------------------------------------------------------
[로직 설명]
// --------------------------------------------------------------------------------------

1. 사전) WebRTC Offer 전송 후 Answer 응답을 받지 못하는 주요 상황 정리 : 
  
    >> AWS 접속 정보가 잘못 된 경우 (ex : region, accessKey, secretKey)
    
    >> AWS WebRTC 신호채널에 생성 된 기기 채널 ARN 정보가 없는 경우 , 잘못 설정한 경우
    
    >> clientId 형식이 규격 외로 벗어난 경우 (ex : 특수문자 포함됨)

    >> 동일한 clientId 로 계속 접속을 시도하는 경우 (ex : 동일한 클라이언트 아이디를 사용해 60초 내 접속 시도 시 세션 초기화 문제 발생 가능)
    
    >> 일정 시간 내에 WebRTC 연결 요청이 다수 일어난 경우 (과도한 SDP 메시지 교환 처리)
    
    >> WebRTC 연결 중 네트워크 환경 상태 변화가 일어난 경우 (ex : 와이파이 환경에서 시작 > 모바일 네트워크 전환)

    >> 잘못 된 뷰어의 SDP Offer 오퍼 전문 전송 (video, audio 잘못 사용 true 값 지정 등)

    >> 연결 된 뷰어 최대 할당 개수 초과

    >> 사용 만료 된 getIceServerConfig IceServerList 정보를 사용하는 경우


2. AWS IAM 계정 정보를 사용해 AWS.KinesisVideo 객체 초기화 수행

    kinesisVideoClient = new AWS.KinesisVideo({
        region, // 리전
        accessKeyId, // IAM 액세스 키
        secretAccessKey, // IAM 시크릿 키
        correctClockSkew: true,
    });


3. 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;
    }, {});


4. 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
    };


5. 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,
    });


6. 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 에러 핸들러


7. signalingClient.open(); 이 정상적으로 완료 된 경우 sdp Offer 메시지 전송 수행 및 SDP Answer 응답 대기 타이머 등록 실시

    >> signalingClient.on('open', async () => {

        // -----------------------------------------
        // ✅ [SDP 오퍼 생성 및 마스터에게 전송]
        // -----------------------------------------
        // 마스터의 비디오 데이터만 수신해 비디오 객체에 출력 설정
        // -----------------------------------------
        const offer = await peerConnection.createOffer({
            offerToReceiveAudio: false,
            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);
            }

        }, (15 * 1000) );

    });


8. 타이머 핸들러 내에서 일정 시간 내에 응답을 받지 못한 경우 WebRTC 연결에 사용 된 객체 초기화 및 재연결 시도 로직 작성

    answerTimer = setTimeout(() => {
        console.error("[WebRTC] : Answer : TimeOut : Run");

        try {

            if (GLOBAL_RETRY > 0){ // 재시도 이력 있음 >> 에러 팝업창 표시

                // -----------------------------------------
                // [에러 팝업창 알림]
                // -----------------------------------------
                C_SweetAlert_Error_OK("알 림", "[1] SDP Answer 응답을 받는데, 너무 오랜 시간이 소요되고 있습니다. WebRTC 접속 정보 재확인 및 기기 구동 상태를 다시 확인 후 접속해주세요.", "확인");

            }
            else {

                if (GLOBAL_JSON != null){

                    closeConnection(); // ✅ WebRTC 연결 객체 종료 처리 수행

                    GLOBAL_RETRY ++; // ✅ 전역 변수 시도 횟수 카운트 증가

                    const connectTimer = setTimeout(() => {
                        console.error("[WebRTC] : Reconnect : Start");

                        // -----------------------------------------
                        // [로딩 프로그레스 종료]
                        // -----------------------------------------
                        startWebRTCReconnnect();

                    }, 4000 ); // ✅ 일정 시간 대기 후 재연결 시도

                }
                else {

                    // -----------------------------------------
                    // [에러 팝업창 알림]
                    // -----------------------------------------
                    C_SweetAlert_Error_OK("알 림", "[2] SDP Answer 응답을 받는데, 너무 오랜 시간이 소요되고 있습니다. WebRTC 접속 정보 재확인 및 기기 구동 상태를 다시 확인 후 접속해주세요.", "확인");

                }

            }

        }
        catch (exception) {
            console.error("[WebRTC] : Answer : TimeOut : Exception : ", exception.message);
        }

    }, (15 * 1000) );


9. WebRTC 연결 재시도 후 정상적으로 영상이 출력 되는 경우 시청 및 재연결 시에도 에러가 발생한 경우 에러 팝업창 표시


10. 참고 : WebRTC 연결 객체 초기화 코드 : 

    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 KVS WebRTC 뷰어 실시간 영상 재생 시 짧은 시간 내에 다중 접속 시도 시 SDP Answer 응답을 받지 못하는 이슈

https://kkh0977.tistory.com/8519

https://blog.naver.com/kkh0977/224119423929


[업무 이슈] AWS WebRTC 뷰어에서 동일한 clientId 사용해 스트리밍 시청 시 AWS 세션 초기화 이슈로 60 초내외 재사용 필요 이슈

https://kkh0977.tistory.com/8442

https://blog.naver.com/kkh0977/224094606649


[Aws Kvs WebRTC 실시간 영상 재생 관련 구성 요소 및 용어 정리]

https://blog.naver.com/kkh0977/223858189791


[Aws Kinesis Video Streams] WebRTC remote sender clientId 클라이언트 아이디 설명, 규격 및 제한 정리

https://kkh0977.tistory.com/8415

https://blog.naver.com/kkh0977/224083731976


[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 WebRTC 실시간 비디오 재생 시 Client 클라이언트 연결 접속 및 해제 상태 확인 이슈

https://blog.naver.com/kkh0977/223966952222

// --------------------------------------------------------------------------------------
 
728x90
반응형
Comments