투케이2K

167. (TWOK/UTIL) [Web/JavaScript] AWS Kvs WebRtc 뷰어 Viewer 실시간 비디오 및 오디오 영상 통화 수행 본문

투케이2K 유틸파일

167. (TWOK/UTIL) [Web/JavaScript] AWS Kvs WebRtc 뷰어 Viewer 실시간 비디오 및 오디오 영상 통화 수행

투케이2K 2025. 9. 14. 14:44
728x90

[설 명]

프로그램 : Web / JavaScript

설 명 : [Web/JavaScript] AWS Kvs WebRtc 뷰어 Viewer 실시간 비디오 및 오디오 영상 통화 수행

 

[소스 코드]

-----------------------------------------------------------------------------------------
[사전 설명 및 설정 사항]
-----------------------------------------------------------------------------------------

- 개발 환경 : Web


- 개발 기술 : JavaScript (자바스크립트) / AWS / Kvs / WebRTC / Viewer


- 사전) AWS KVS WebRTC 설명 : 

  >> WebRTC 란 웹, 애플리케이션, 디바이스 간 중간자 없이 오디오나 영상 미디어를 포착하고 실시간 스트림할 뿐 아니라, 임의의 데이터도 교환할 수 있도록 하는 기술입니다

  >> WebRTC 는 간단한 API 를 통해 웹 브라우저, 모바일 애플리케이션 및 커넥티드 디바이스 간에 실시간 통신을 활성화할 수 있습니다

  >> WebRTC 주요 용어 : 

    - SDP (Session Description Protocol) : 오디오/비디오 코덱, 해상도, 포트 등 스트리밍 정보를 담은 텍스트 포맷
    - Offer / Answer	: 통신 연결을 협상하기 위한 SDP 메시지 (초기 연결 설정)
    - ICE (Interactive Connectivity Establishment) : NAT/P2P 환경에서도 연결 가능한 경로(IP, 포트 등)를 찾기 위한 기술
    - Candidate : 가능한 연결 경로 (IP + Port 조합)

  >> WebRTC [ICE] 연결 형태 : 

    - Relayed Address : TURN 서버가 패킷 릴레이를 위해 할당하는 주소
    - Server Reflexive Address : NAT 가 매핑한 클라이언트의 공인망 (Public IP, Port)
    - Local Address : 클라이언트의 사설주소 (Private IP, Port)

  >> WebRTC STUN 및 TURN 서버 설명 : 

    - (같은 와이파이 망) STUN 서버는 HOST 를 거쳐 >> Server Reflexive Address 만을 응답하지만,
      (릴레이서버 사용) TURN 서버는 Relayed Address와 Server Reflexive Address 를 모두 응답한다
    - STUN, TURN 서버를 이용해 SDP Answer IP주소 를 취득 >> RTCPeerConnection Remote 연결 수행

  >> WebRTC SDP 오퍼 생성 (뷰어) 및 응답 (마스터) 스트리밍 플로우 : 

    [Viewer → Signaling Server] -- SDP Offer --> [Master] : 뷰어는 마스터로 스트리밍 오퍼 신호 보낸다
    [Master] -- SDP Answer --> [Viewer] : 마스터는 특정 뷰어의 오퍼 신호 확인 후 응답을 보낸다

    [Viewer] -- ICE Candidate --> [Master] : 스트리밍을 할 수 있는 경로 확인
    [Master] -- ICE Candidate --> [Viewer] : 스트리밍을 할 수 있는 경로 확인

    P2P 연결 성립 → 스트리밍 시작

-----------------------------------------------------------------------------------------





-----------------------------------------------------------------------------------------
[소스 코드]
-----------------------------------------------------------------------------------------

<!DOCTYPE HTML>
<html lang="ko">
<head>
    <title>javaScriptTest</title>
    <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">


    <!-- 반응형 구조 만들기 -->
    <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0, user-scalable=no">


    <!-- 내부 CSS 스타일 지정 -->
    <style>

        html, body {
            width: 100%;
            height: 100%;
            margin : 0 auto;
            padding : 0;
            border : none;
            background-color: #666;
        }


        #container_remoteView {
            width: 100%;
            height: 98%;
            margin: 0 auto;
            position: relative;
            top: 1%;
        }
        #remoteView {
            width: 100%;
            height: 100%;
            margin: 0 auto;
            background-color: #666;
        }

    </style>





    <!-- [CDN 주소 설정] -->
    <script src="https://code.jquery.com/jquery-latest.min.js"></script>

    <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>






    <!-- [자바스크립트 코드 지정] -->
    <script>

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

        // [전역 변수 선언]
        var kinesisVideoClient = null;

        var region = ''; // [AWS 리젼]
        var clientId = ''; // [클라이언트 아이디]
        var accessKeyId = ''; // [실시간 영상 재생에 필요한 액세스 키]
        var secretAccessKey = ''; // [실시간 영상 재생에 필요한 시크릿 키]
        var channelARN = ''; // [실시간 스트리밍 채널 ARN 주소]

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

        // [html 최초 로드 및 이벤트 상시 대기 실시]

        window.onload = async function() {
            console.log("-");
            console.log("=========================================");
            console.log("[window onload] : [start]");
            console.log("=========================================");
            console.log("-");


            // -----------------------------------------
            // [JSON 데이터 삽입 수행] : 하드 코딩 : SKS AWS 됴쿄 리전
            // -----------------------------------------
            //*
            var webJson = {
                region : "ap-northeast-1",
                clientId : "DEVICE_1", // 중복 되지 않은 고유 클라이언트 값
                accessKeyId : "AK..7Q",
                secretAccessKey : "Zz..xj",
                channelARN : "arn:aws:kinesisvideo:ap-northeast-1:123456789012:channel/DEVICE_1/123456789012"
            };

            setSystem(JSON.stringify(webJson)); // [Web 자체 데이터 생성 호출]
            // */


            // -----------------------------------------
            // [안드로이드 동작 수행 호출]
            // -----------------------------------------
            // window.android.system(); // [JSON 전송]

        };

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

        async function setSystem(jsonData){
            console.log("-");
            console.log("=========================================");
            console.log("[setSystem] : 웹 데이터 전달 받음");
            console.log("-----------------------------------------");
            console.log("[데이터] : " + JSON.stringify(jsonData));
            console.log("=========================================");
            console.log("-");

            try {

                // -----------------------------------------
                // [json 데이터 파싱 수행 실시]
                // -----------------------------------------
                var jsonObject = JSON.parse(jsonData);

                region = String(jsonObject.region); // [AWS 리전]
                clientId = String(jsonObject.clientId); // [클라이언트 아이디]
                accessKeyId = String(jsonObject.accessKeyId); // [실시간 영상 재생에 필요한 액세스 키]
                secretAccessKey = String(jsonObject.secretAccessKey); // [실시간 영상 재생에 필요한 시크릿 키]
                channelARN = String(jsonObject.channelARN); // [실시간 스트리밍 채널 ARN 주소]

                console.log("-");
                console.log("=========================================");
                console.log("[setSystem] : 데이터 파싱 정보");
                console.log("-----------------------------------------");
                console.log("[region] : " + region);
                console.log("-----------------------------------------");
                console.log("[clientId] : " + clientId);
                console.log("-----------------------------------------");
                console.log("[accessKeyId] : " + accessKeyId);
                console.log("-----------------------------------------");
                console.log("[secretAccessKey] : " + secretAccessKey);
                console.log("-----------------------------------------");
                console.log("[channelARN] : " + channelARN);
                console.log("=========================================");
                console.log("-");



                // -----------------------------------------
                // [비디오 플레이 컴포넌트 지정 및 이벤트 지정]
                // -----------------------------------------
                const remoteView = document.getElementById("remoteView");

                //remoteView.onload = (event) => {};
                //remoteView.onchange = (event) => {};
                remoteView.onerror = (event) => {
                    console.error("");
                    console.error("=========================================");
                    console.error("[setSystem] : [remoteView] : 원격 비디오 뷰 컴포넌트 : onerror");
                    console.error("=========================================");
                    console.error("");
                };
                remoteView.onwaiting = (event) => {
                    console.log("");
                    console.log("=========================================");
                    console.log("[setSystem] : [remoteView] : 원격 비디오 뷰 컴포넌트 : onwaiting");
                    console.log("=========================================");
                    console.log("");
                };
                remoteView.onpause = (event) => {
                    console.error("");
                    console.error("=========================================");
                    console.error("[setSystem] : [remoteView] : 원격 비디오 뷰 컴포넌트 : onpause");
                    console.error("=========================================");
                    console.error("");
                };
                remoteView.onplaying = (event) => {
                    console.warn("");
                    console.warn("=========================================");
                    console.warn("[setSystem] : [remoteView] : 원격 비디오 뷰 컴포넌트 : onplaying");
                    console.warn("=========================================");
                    console.warn("");
                };
                // video.onplay = (event) => {};



                // -----------------------------------------
                // [KVS 클라이언트 생성]
                // -----------------------------------------
                kinesisVideoClient = new AWS.KinesisVideo({
                    region,
                    accessKeyId,
                    secretAccessKey,
                    correctClockSkew: true,
                });

                console.log("-");
                console.log("=========================================");
                console.log("[setSystem] : [KinesisVideo] : 비디오 재생 및 정보 관련 객체 초기화 수행]");
                console.log("=========================================");
                console.log("-");



                // -----------------------------------------
                // [신호 채널 끝점 가져오기]
                // -----------------------------------------
                // 각 신호 채널에는 데이터 플레인 작업을 위해 연결할 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;
                }, {});



                // -----------------------------------------
                // [KVS 시그널링 클라이언트 만들기]
                // -----------------------------------------
                // 응답 의 HTTPS 끝점은 GetSignalingChannelEndpoint이 클라이언트와 함께 사용됩니다
                // -----------------------------------------
                // 이 클라이언트는 실제 신호가 아닌 ICE 서버를 가져오는 데만 사용됩니다
                // -----------------------------------------
                const kinesisVideoSignalingChannelsClient = new AWS.KinesisVideoSignalingChannels({
                    region: region,
                    accessKeyId,
                    secretAccessKey,
                    endpoint: endpointsByProtocol.HTTPS,
                    correctClockSkew: true,
                });

                console.log("-");
                console.log("=========================================");
                console.log("[setSystem] : [KinesisVideoSignalingChannels] : iceServers 신호 채널 정보 확인 객체 초기화 수행]");
                console.log("=========================================");
                console.log("-");



                // -----------------------------------------
                // [ICE 서버 구성 가져오기]
                // -----------------------------------------
                // 최상의 성능을 위해 STUN 및 TURN ICE 서버 구성을 수집합니다
                // -----------------------------------------
                // [사용 서버 정의]
                // -----------------------------------------
                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); // [추가]

                console.log("-");
                console.log("=========================================");
                console.log("[setSystem] : [iceServers] : [서버 연결 정보 확인]");
                console.log("-----------------------------------------");
                console.log("[스턴 : stunIceServers] : " + JSON.stringify(stunIceServers));
                console.log("-----------------------------------------");
                console.log("[턴 : tunIceServers] : " + JSON.stringify(tunServerList));
                console.log("-----------------------------------------");
                console.log("[전체 : ALL] : " + JSON.stringify(iceServers));
                console.log("=========================================");
                console.log("-");



                // -----------------------------------------
                // [RTCPeerConnection 만들기]
                // -----------------------------------------
                // RTCPeerConnection은 웹 에서 WebRTC 통신을 위한 기본 인터페이스입니다
                // -----------------------------------------
                const config = {
                    iceServers: iceServers,
                    iceTransportPolicy: "all", // all | relay
                };

                const peerConnection = new RTCPeerConnection(config);


                // -----------------------------------------
                // [WebRTC 시그널링 클라이언트 만들기]
                // -----------------------------------------
                // 이것은 신호 채널을 통해 메시지를 보내는 데 사용되는 실제 클라이언트입니다
                // -----------------------------------------
                const signalingClient = new KVSWebRTC.SignalingClient({
                    channelARN,
                    channelEndpoint: endpointsByProtocol.WSS,
                    clientId,
                    role: KVSWebRTC.Role.VIEWER,
                    region,
                    credentials: {
                        accessKeyId : accessKeyId,
                        secretAccessKey : secretAccessKey,
                    },
                    systemClockOffset: kinesisVideoClient.config.systemClockOffset,
                });

                console.log("-");
                console.log("=========================================");
                console.log("[setSystem] : [RTCPeerConnection 및 SignalingClient 생성 이벤트 수신 대기 수행]");
                console.log("=========================================");
                console.log("-");



                // -----------------------------------------
                // [시그널링 클라이언트 이벤트 리스너 추가]
                // -----------------------------------------
                signalingClient.on('open', async () => {
                    console.log("-");
                    console.log("=========================================");
                    console.log("[setSystem] : [로컬 <-> 원격 연결 신호 확인] : [signalingClient] : [open] : [Start]");
                    console.log("-----------------------------------------");
                    console.log("[VIEWER] Connected to signaling service");
                    console.log("-----------------------------------------");
                    console.log("[로 직] : ", "신호 채널 연결 확인 >> Viewer 의 SDP Offer 수신 대기 수행");
                    console.log("=========================================");
                    console.log("-");

                    // -----------------------------------------
                    // [카메라 및 오디오 사용 권한 요청 >> 트랙 스트림 추가]
                    // -----------------------------------------
                    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);
                });

                // [SDP 응답이 마스터로부터 다시 수신되면 피어 연결에 추가하십시오]
                signalingClient.on('sdpAnswer', async answer => {
                    console.warn("");
                    console.warn("=========================================");
                    console.warn("[setSystem] : [로컬 <-> 원격 연결 신호 확인] : [signalingClient] : [sdpAnswer] : [Start]");
                    console.warn("-----------------------------------------");
                    console.warn("[VIEWER] Received SDP answer : ", JSON.stringify(answer));
                    console.warn("-----------------------------------------");
                    console.warn("[설 명] : 원격 접속 연결 시도 리스트 확인");
                    console.warn("-----------------------------------------");
                    console.warn("[로 직] : ", "sdp 자격 인증 확인 완료 >> peerConnection.setRemoteDescription 피어 연결에 추가 실시");
                    console.warn("=========================================");
                    console.warn("");

                    // -----------------------------------------
                    // [peerConnection.setRemoteDescription 에 응답 받은 answer 추가]
                    // -----------------------------------------
                    await peerConnection.setRemoteDescription(answer);
                });

                // [마스터로부터 ICE 후보를 수신하면 피어 연결에 추가]
                signalingClient.on('iceCandidate', candidate => {
                    console.log("");
                    console.log("=========================================");
                    console.log("[setSystem] : [로컬 <-> 원격 연결 신호 확인] : [signalingClient] : [iceCandidate] : [Start]");
                    console.log("-----------------------------------------");
                    console.log("[VIEWER] Received ICE candidate : ", JSON.stringify(candidate));
                    console.log("-----------------------------------------");
                    console.log("[설 명] : 원격 접속 연결 시도 리스트 추가");
                    console.log("-----------------------------------------");
                    console.log("[로 직] : ", "peerConnection.addIceCandidate 피어 연결에 추가");
                    console.log("=========================================");
                    console.log("");

                    peerConnection.addIceCandidate(candidate);
                });

                // [ICE 종료 핸들러]
                signalingClient.on('close', () => {
                    console.error("");
                    console.error("=========================================");
                    console.error("[setSystem] : [로컬 <-> 원격 연결 신호 확인] : [signalingClient] : [close] : [Start]");
                    console.error("-----------------------------------------");
                    console.error("[VIEWER] Disconnected from signaling channel");
                    console.error("-----------------------------------------");
                    console.error("[설 명] 원격 연결 신호 끊김 >> 원격 비디오 재생 종료 됨");
                    console.error("=========================================");
                    console.error("");
                });

                // [ICE 에러 핸들러]
                signalingClient.on('error', error => {
                    console.error("");
                    console.error("=========================================");
                    console.error("[setSystem] : [로컬 <-> 원격 연결 신호 확인] : [signalingClient] : [error] : [Start]");
                    console.error("-----------------------------------------");
                    console.error("[VIEWER] Signaling client error : ", JSON.stringify(error));
                    console.error("=========================================");
                    console.error("");
                });



                // -----------------------------------------
                // [피어 연결 이벤트 리스너 추가]
                // -----------------------------------------
                // [피어 연결에 의해 생성된 모든 ICE 후보를 다른 피어로 보냅니다]
                // -----------------------------------------
                peerConnection.addEventListener('icecandidate', ({ candidate }) => {
                    if (candidate) {
                        console.log("");
                        console.log("=========================================");
                        console.log("[setSystem] : [로컬 <-> 원격 접속 상태 확인] : [peerConnection] : [icecandidate] : [Start]");
                        console.log("-----------------------------------------");
                        console.log("[Event] icecandidate : ", JSON.stringify(candidate));
                        console.log("-----------------------------------------");
                        console.log("[설 명] : 원격 접속 연결 시도 수행");
                        console.log("-----------------------------------------");
                        console.log("[로 직] : ", "signalingClient.sendIceCandidate 보내기");
                        console.log("=========================================");
                        console.log("");

                        signalingClient.sendIceCandidate(candidate);
                    }
                    else {
                        // [더 이상 ICE 후보가 생성되지 않습니다]
                        console.log("");
                        console.log("=========================================");
                        console.log("[setSystem] : [로컬 <-> 원격 접속 상태 확인] : [peerConnection] : [close] : [Start]");
                        console.log("-----------------------------------------");
                        console.log("[Event] icecandidate : ", JSON.stringify(candidate));
                        console.log("-----------------------------------------");
                        console.log("[설 명] : 더이상 원격 접속 연결 시도할 것이 없음");
                        console.log("=========================================");
                        console.log("");
                    }
                });

                // [원격 트랙이 수신되면 원격 보기에 추가합니다]
                peerConnection.addEventListener('track', event => {
                    console.log("");
                    console.log("=========================================");
                    console.log("[setSystem] : [로컬 <-> 원격 접속 상태 확인] : [peerConnection] : [track] : [Start]");
                    console.log("-----------------------------------------");
                    console.log("[VIEWER] Received remote track ::", JSON.stringify(event));
                    console.log("=========================================");
                    console.log("");

                    // [스트림 재생 src 지정] : 마스터가 전달한 스트림 데이터 출력
                    remoteView.srcObject = event.streams[0];

                    console.warn("");
                    console.warn("=========================================");
                    console.warn("[setSystem] : [로컬 <-> 원격 접속 상태 확인] : [peerConnection] : [track] : [Remote Src]");
                    console.warn("-----------------------------------------");
                    console.warn("[REMOTE SRC] ::", JSON.stringify(event.streams[0]));
                    console.warn("-----------------------------------------");
                    console.warn("[로 직] : ", "원격 remoteView.srcObject 주소 지정 >> 원격 비디오 재생 REMOTE SRC 지정 및 비디오 재생 실시");
                    console.warn("=========================================");
                    console.warn("");

                });


                // -----------------------------------------
                // [신호 연결 수행]
                // -----------------------------------------
                signalingClient.open(); // [signalingClient 열기]

            }
            catch (exception) {
                console.error("-");
                console.error("=========================================");
                console.error("[setSystem] : [Exception] : 예외 상황 발생");
                console.error("-----------------------------------------");
                console.error("[ERROR] : ", JSON.stringify(exception.message));
                console.error("=========================================");
                console.error("-");
            }
        };

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

    </script>


</head>


<body>

<!-- [컨테이너 생성] -->
<div id="container_remoteView">
    <video autoplay="true" id="remoteView"></video>
</div>

</body>

</html>

-----------------------------------------------------------------------------------------





-----------------------------------------------------------------------------------------
[참고 사이트]
-----------------------------------------------------------------------------------------

[자바스크립트 AWS WebRTC 실시간 동영상 재생 수행]

https://blog.naver.com/kkh0977/223170500993?trackingCode=blog_bloghome_searchlist


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

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


[Aws KVS WebRTC 채널 생성 및 삭제에 관한 요금 정책 정리]

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

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