투케이2K

1040. (Android/Java) [간단 소스] 안드로이드 AWS WebRTC 뷰어에서 SDP Answer 응답 부분 base64 디코딩 처리 소스 코드 본문

Android

1040. (Android/Java) [간단 소스] 안드로이드 AWS WebRTC 뷰어에서 SDP Answer 응답 부분 base64 디코딩 처리 소스 코드

투케이2K 2025. 10. 20. 21:42
728x90
반응형

[개발 환경 설정]

개발 툴 : AndroidStudio

개발 언어 : Java / Kotlin

 

[소스 코드]

// --------------------------------------------------------------------------------------
[개발 및 테스트 환경]
// --------------------------------------------------------------------------------------

- 언어 : Java / Kotlin


- 개발 툴 : AndroidStudio


- 기술 구분 : Android / Android / AWS / 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 연결 성립 → 스트리밍 시작


- 사전) 안드로이드 Build.gradle 설정 사항 : 

    android {

        // [컴파일 버전]
        compileSdk 34

        // [Config 셋팅]
        defaultConfig {
            // ----------------------------
            applicationId "com.example.javaproject" // 앱 아이디
            // ----------------------------
            versionCode 1 // 빌드 버전
            // ----------------------------
            versionName '1.0.1' // 빌드 네임
            // ----------------------------
            minSdk 24 // 최소 빌드 버전
            // ----------------------------
            targetSdk 34 // TODO 타겟 빌드 버전
            // ----------------------------
        }

        // [컴파일 자바 버전 지정]
        compileOptions {
            sourceCompatibility JavaVersion.VERSION_1_8
            targetCompatibility JavaVersion.VERSION_1_8
        }

        // [아파치 http 사용 설정]
        useLibrary ('org.apache.http.legacy')
    }

    dependencies {

        implementation 'com.amazonaws:aws-android-sdk-iot:2.57.0'
        implementation 'com.amazonaws:aws-android-sdk-mobile-client:2.57.0'

        implementation 'com.amazonaws:aws-android-sdk-kinesisvideo:2.57.0'
    
        //implementation files('libs/google-webrtc-1.0.32006.aar')
        implementation 'com.infobip:google-webrtc:1.0.0035529'
    }

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






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

webSocket = okClient.newWebSocket(okSocketReqBuilder.build(), new WebSocketListener() {
    @Override
    public void onOpen(WebSocket webSocket, Response response) {
        S_Log._W_("STEP :: webSocket :: open :: receive", new String[]{ String.valueOf(response.toString()) });


        // ---------------------------------------------
        // TODO WebSocket onOpen() 안에서 바로 호출하는 대신, 메인 스레드에서 호출하도록 처리 수행
        // ---------------------------------------------
        new Handler(Looper.getMainLooper()).postDelayed(new Runnable() {
            @Override
            public void run() {

                if (peerConnection != null) {

                    // TODO ----------------------------------------------------------------------
                    // TODO AWS KVS SDK 기반에서는 addTransceiver() 방식이 더 명확하게 SDP를 구성할 수 있습니다.
                    // TODO SEND_ONLY: 송출만 (Master 역할)
                    // TODO SEND_RECV: 양방향 통화 가능 (양쪽에서 송출/수신 둘 다 가능)
                    // TODO ----------------------------------------------------------------------


                    // TODO ----------------------------------------------------------------------
                    // TODO Viewer 입장 (수신자) : recvonly 는 수신만 받겠다 / sendRecv 는 양방향 통화 설정
                    // TODO ----------------------------------------------------------------------
                    RtpTransceiver.RtpTransceiverInit recvOnly = new RtpTransceiver.RtpTransceiverInit(RtpTransceiver.RtpTransceiverDirection.RECV_ONLY);
                    peerConnection.addTransceiver(MediaStreamTrack.MediaType.MEDIA_TYPE_VIDEO, recvOnly);
                    peerConnection.addTransceiver(MediaStreamTrack.MediaType.MEDIA_TYPE_AUDIO, recvOnly);
                    // -------------------------------------------
                    //RtpTransceiver.RtpTransceiverInit sendRecv = new RtpTransceiver.RtpTransceiverInit(RtpTransceiver.RtpTransceiverDirection.SEND_RECV);
                    //peerConnection.addTransceiver(MediaStreamTrack.MediaType.MEDIA_TYPE_VIDEO, sendRecv);
                    //peerConnection.addTransceiver(MediaStreamTrack.MediaType.MEDIA_TYPE_AUDIO, sendRecv);
                    // TODO ----------------------------------------------------------------------


                    // TODO ----------------------------------------------------------------------
                    // TODO SDP 생성 시 미디어 관련 제약 조건
                    // TODO Viewer가 오디오/비디오 트랙을 받을 의사가 있음을 명시
                    // TODO → Master가 송출자일 경우에도 반드시 true로 설정해두면 SDP가 안정적으로 구성됩니다.
                    // TODO ----------------------------------------------------------------------
                    MediaConstraints constraints = new MediaConstraints();
                    constraints.mandatory.add(new MediaConstraints.KeyValuePair("OfferToReceiveAudio", "true"));
                    constraints.mandatory.add(new MediaConstraints.KeyValuePair("OfferToReceiveVideo", "true"));


                    // TODO ----------------------------------------------------------------------
                    // TODO 선택적 설정 (네트워크 상태에 따라 다름)
                    // TODO DTLS SRTP 연결 안정화용 (일부 단말에서 필요)
                    // TODO ----------------------------------------------------------------------
                    constraints.optional.add(new MediaConstraints.KeyValuePair("DtlsSrtpKeyAgreement", "true"));


                    // TODO ----------------------------------------------------------------------
                    // TODO Offer 생성 : 기본 new MediaConstraints() 만 사용 하면, remote track 요청이 없으면 createOffer 콜백이 안 올 수 있음 :: 따라서 OfferToReceiveAudio/Video 설정 필수
                    // TODO 안드로이드 WebRTC에서 MediaConstraints와 RtpTransceiver는 각각 다른 목적을 가지고 있고, 생성 시점에 따라 SDP에 반영되는 내용이 달라집니다.
                    // TODO ----------------------------------------------------------------------

                    // [SdpObserver 설정]
                    peerConnection.createOffer(sdpObserver, constraints);

                    S_Log._W_("STEP :: peerConnection :: createOffer :: Start", null);

                } else {
                    S_Log._E_("STEP :: peerConnection :: createOffer :: ERROR", new String[]{ "peerConnection Is Null" });
                }

            }
        }, 2000);

    }

    @Override
    public void onMessage(WebSocket webSocket, String text) {

        try {

            if (C_Util.stringNotNull(text) == true && C_Util.stringJsonObjectEnable(text) == true){
                S_Log._D_("STEP :: webSocket :: onMessage :: Json receive", new String[]{ String.valueOf(text) });


                // --------------------------------------------
                // TODO String To JSONObject
                // --------------------------------------------
                JSONObject msg = new JSONObject(text);
                // --------------------------------------------


                // ---------------------------------------------
                // TODO [action 및 messageType 커맨드 값 확인]

                // ---------------------------------------------
                String action = "";
                if (msg.has("action")){
                    action = msg.optString("action");
                }
                if (msg.has("messageType")){
                    action = msg.optString("messageType");
                }

                // ---------------------------------------------
                // TODO [messagePayload : Base64 디코딩 처리]
                // ---------------------------------------------
                if ("SDP_ANSWER".equals(action)) {
                    // TODO {"messageType":"SDP_ANSWER", "messagePayload":"eyJ0eXBlIjogImFuc3dlciIsICJzZHAiOiAidj0wXHJcbm89LSA4ODk4MjI4MDcgMiBJTiBJUDQgMTI3LjAuMC4xXHJcbnM9LVxyXG50PTAgMFxyXG5hPWdyb3VwOkJVTkRMRSAwIDFcclxuYT1tc2lkLXNlbWFudGljOiBXTVMga3ZzVmlkZW9TdHJlYW1cclxubT1hdWRpbyA5IFVEUC9UTFMvUlRQL1NBVlBGIDBcclxuYz1JTiBJUDQgMTI3LjAuMC4xXHJcbmE9Y2FuZGlkYXRlOjAgMSB1ZHAgMjEzMDcwNjQzMSAxOTIuMTY4LjEuMjI4IDM0NjQ5IHR5cCBob3N0IHJhZGRyIDAuMC4wLjAgcnBvcnQgMCBnZW5lcmF0aW9uIDAgbmV0d29yay1jb3N0IDk5OVxyXG5hPW1zaWQ6bXRzX3ZpZGVvX3N0cmVhbV8xIG10c19hdWRpb190cmFja18xXHJcbmE9c3NyYzoxMTA5OTA2MjM4IGNuYW1lOnllRnRKVXp4SnRMRHUvTGNcclxuYT1zc3JjOjExMDk5MDYyMzggbXNpZDptdHNfdmlkZW9fc3RyZWFtXzEgbXRzX2F1ZGlvX3RyYWNrXzFcclxuYT1zc3JjOjExMDk5MDYyMzggbXNsYWJlbDptdHNfdmlkZW9fc3RyZWFtXzFcclxuYT1zc3JjOjExMDk5MDYyMzggbGFiZWw6bXRzX2F1ZGlvX3RyYWNrXzFcclxuYT1ydGNwOjkgSU4gSVA0IDAuMC4wLjBcclxuYT1pY2UtdWZyYWc6d2dFNlxyXG5hPWljZS1wd2Q6ZXJ3UXcxZzhqRVIyRnIrV3BWVWJXcUR3XHJcbmE9aWNlLW9wdGlvbnM6dHJpY2tsZVxyXG5hPWZpbmdlcnByaW50OnNoYS0yNTYgNTU6NkQ6ODc6REI6Mzc6OUU6RkI6RDA6ODM6NUE6Nzk6OEU6MTQ6OTI6MzY6Rjg6NTY6RjY6NEE6RTY6MzI6MUY6QkE6MDk6QjY6NjE6Mzk6OTk6NTc6REM6OTU6ODdcclxuYT1zZXR1cDphY3RpdmVcclxuYT1zZW5kb25seVxyXG5hPW1pZDowXHJcbmE9cnRjcC1tdXhcclxuYT1ydGNwLXJzaXplXHJcbmE9cnRwbWFwOjAgUENNVS84MDAwXHJcbmE9cnRjcC1mYjowIG5hY2tcclxuYT1ydGNwLWZiOjAgZ29vZy1yZW1iXHJcbm09dmlkZW8gOSBVRFAvVExTL1JUUC9TQVZQRiAwXHJcbmM9SU4gSVA0IDEyNy4wLjAuMVxyXG5hPWNhbmRpZGF0ZTowIDEgdWRwIDIxMzA3MDY0MzEgMTkyLjE2OC4xLjIyOCAzNDY0OSB0eXAgaG9zdCByYWRkciAwLjAuMC4wIHJwb3J0IDAgZ2VuZXJhdGlvbiAwIG5ldHdvcmstY29zdCA5OTlcclxuYT1tc2lkOm10c192aWRlb19zdHJlYW1fMSBtdHNfdmlkZW9fdHJhY2tfMVxyXG5hPXNzcmM6MTUzMzc0OTg0OCBjbmFtZTp5ZUZ0SlV6eEp0TER1L0xjXHJcbmE9c3NyYzoxNTMzNzQ5ODQ4IG1zaWQ6bXRzX3ZpZGVvX3N0cmVhbV8xIG10c192aWRlb190cmFja18xXHJcbmE9c3NyYzoxNTMzNzQ5ODQ4IG1zbGFiZWw6bXRzX3ZpZGVvX3N0cmVhbV8xXHJcbmE9c3NyYzoxNTMzNzQ5ODQ4IGxhYmVsOm10c192aWRlb190cmFja18xXHJcbmE9cnRjcDo5IElOIElQNCAwLjAuMC4wXHJcbmE9aWNlLXVmcmFnOndnRTZcclxuYT1pY2UtcHdkOmVyd1F3MWc4akVSMkZyK1dwVlViV3FEd1xyXG5hPWljZS1vcHRpb25zOnRyaWNrbGVcclxuYT1maW5nZXJwcmludDpzaGEtMjU2IDU1OjZEOjg3OkRCOjM3OjlFOkZCOkQwOjgzOjVBOjc5OjhFOjE0OjkyOjM2OkY4OjU2OkY2OjRBOkU2OjMyOjFGOkJBOjA5OkI2OjYxOjM5Ojk5OjU3OkRDOjk1Ojg3XHJcbmE9c2V0dXA6YWN0aXZlXHJcbmE9c2VuZG9ubHlcclxuYT1taWQ6MVxyXG5hPXJ0Y3AtbXV4XHJcbmE9cnRjcC1yc2l6ZVxyXG5hPXJ0cG1hcDowIEgyNjQvOTAwMDBcclxuYT1ydGNwLWZiOjAgbmFja1xyXG5hPXJ0Y3AtZmI6MCBnb29nLXJlbWJcclxuIn0="}
                    String sdpAnswer = msg.getString("messagePayload");
                    
                    JSONObject ans = null;
                    
                    if (C_Util.stringJsonObjectEnable(sdpAnswer) == false){ // Json 형식 체크
                        sdpAnswer = new String(Base64.decode(sdpAnswer, Base64.NO_WRAP));

                        ans = new JSONObject(sdpAnswer);
                    }
                    else {
                        ans = new JSONObject(sdpAnswer);
                    }

                    // ---------------------------------------------
                    // TODO [messagePayload 전체 JSON을 SessionDescription 에 넣으면 파싱 오류 발생]
                    // ---------------------------------------------
                    // TODO [sdpAnswer에서 "sdp" 필드만 추출해서 사용] : 이후 onIceConnectionChange, onTrack 등이 정상적으로 호출
                    // ---------------------------------------------

                    S_Log.w("KWON_TWOK", "SDP_ANSWER :: Parsing Json"+"\n"+sdpAnswer + "\n");

                    if (ans.has("sdp")){

                        String sdp = ans.getString("sdp");

                        SessionDescription answer = new SessionDescription(
                                SessionDescription.Type.ANSWER, sdp);

                        peerConnection.setRemoteDescription(new SimpleSdpObserver(), answer);

                        S_Log._W_("STEP :: webSocket :: onMessage :: SDP_ANSWER :: Setting", new String[]{ "type :: " + String.valueOf(answer.type), "description :: " + String.valueOf(answer.description) });

                    }
                    else {
                        S_Log._E_("STEP :: peerConnection :: SDP_ANSWER :: ERROR", new String[]{ "Json Key SDP Not Found" });
                    }

                } else if ("ICE_CANDIDATE".equals(action)) {
                    
                    String iceCandidate = msg.getString("messagePayload");
                    
                    JSONObject cand = null;
                    
                    if (C_Util.stringJsonObjectEnable(iceCandidate) == false){ // Json 형식 체크
                        iceCandidate = new String(Base64.decode(iceCandidate, Base64.NO_WRAP));

                        cand = new JSONObject(iceCandidate);
                    }
                    else {
                        cand = new JSONObject(iceCandidate);
                    }

                    S_Log.w("KWON_TWOK", "ICE_CANDIDATE :: Parsing Json"+"\n"+ String.valueOf(cand) + "\n");

                    if (cand != null){

                        IceCandidate candidate = new IceCandidate(
                                cand.getString("sdpMid"),
                                cand.getInt("sdpMLineIndex"),
                                cand.getString("candidate")
                        );

                        peerConnection.addIceCandidate(candidate);

                        S_Log.d("KWON_TWOK", "STEP :: webSocket :: onMessage :: ICE_CANDIDATE :: Setting");
                    }
                    else {
                        S_Log.e("KWON_TWOK", "STEP :: webSocket :: onMessage :: ICE_CANDIDATE :: Json Error");
                    }

                }

            }
            else {
                S_Log.e("KWON_TWOK","STEP :: webSocket :: onMessage :: Json Type Error :: " + String.valueOf(text));
            }

        } catch (Exception e) {
            e.printStackTrace();
        }
    }
});

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





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

[업무 이슈] 안드로이드 슬러시 (특수 문자) 포함 JSON 정보 전달 시 자동으로 이스케이프 문자 처리되어 WIFI 접속 문제 발생 이슈

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


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

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


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

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


[업무 이슈] AWS WebRTC 실시간 비디오 재생 시 Client 클라이언트 연결 접속 및 해제 상태 확인 이슈

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


[Aws Kinesis Video Streams] WebRTC SDP 협상 과정 프로세스 정리 정리

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


[Aws Kinesis Video Streams] WebRTC getSignalingChannelEndpoint HTTPS, WSS 사용 범위 정리

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


[유틸 파일] unescapeString : 수동 이스케이프 문자 원복 수행

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

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

a

 
728x90
반응형
Comments