투케이2K

185. (TWOK/WORK) [업무 이슈] 안드로이드 WebRTC 재생 시 offer 전송 부분 자동 이스케이프 처리로 기기에서 a=rtpmap:0 H264/90000 반환 이슈 본문

투케이2K 업무정리

185. (TWOK/WORK) [업무 이슈] 안드로이드 WebRTC 재생 시 offer 전송 부분 자동 이스케이프 처리로 기기에서 a=rtpmap:0 H264/90000 반환 이슈

투케이2K 2025. 10. 20. 21:06
728x90

[제 목]

[업무 이슈] 안드로이드 WebRTC 재생 시 offer 전송 부분 자동 이스케이프 처리로 기기에서 a=rtpmap:0 H264/90000 반환 이슈

 

[내 용]

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

- 제 목 : [업무 이슈] 안드로이드 WebRTC 재생 시 offer 전송 부분 자동 이스케이프 처리로 기기에서 a=rtpmap:0 H264/90000 반환 이슈


- 테스트 환경 : Mobile / Samsung / 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'
    }

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





------------------------------------------------------------------------------
[이슈 사항]
------------------------------------------------------------------------------

1. 안드로이드에서 AWS KVS WebRTC 뷰어 역할로 WebRTC 연결 수행 후 SDP_OFFER 오퍼 전송 시 (H264 비디오 코덱)

   올바르지 않은 오퍼 요청으로 마스터 기기로부터 a=rtpmap:0 H264/90000 answer 반환 이슈

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





------------------------------------------------------------------------------
[원인 파악 및 증상 재현]
------------------------------------------------------------------------------

1. 안드로이드에서 WebRTC 뷰어 영상 재생 화면 접속 수행


2. 안드로이드 EglBase.create 생성 및 PeerConnectionFactory 초기화 수행


3. 안드로이드 WebRTC Signaling Endpoint 조회 수행 - IceServer 조회 및 WebSocket 연결 시 사용

    SingleMasterChannelEndpointConfiguration endpointConfig = new SingleMasterChannelEndpointConfiguration()
            .withProtocols("WSS", "HTTPS")
            .withRole("VIEWER");


4. 조회 된 HTTPS 주소를 사용해 ice-server 조회 AWS4Signer 헤더 생성 및 okhttp 를 사용해 ice 서버 리스트 조회 수행

    // 요청 URL 설정
    String iceUrl = httpEndpoint;
    String icePath = "/v1/get-ice-server-config";
    iceUrl += icePath;


    // 요청 ServiceName
    String iceServiceName = "kinesisvideo";


    // AWS Request 객체 생성
    com.amazonaws.Request<?> aws_ice_sign_request = new DefaultRequest<Void>(iceServiceName);
    aws_ice_sign_request.setHttpMethod(HttpMethodName.POST);
    aws_ice_sign_request.setEndpoint(URI.create(httpEndpoint));
    aws_ice_sign_request.setEncodedResourcePath(icePath);
    aws_ice_sign_request.addHeader("Content-Type", "application/json");
    aws_ice_sign_request.setContent(new ByteArrayInputStream(iceBody.getBytes(StandardCharsets.UTF_8)));

    // 서명자 설정

    BasicAWSCredentials aws_ice_creds = new BasicAWSCredentials(ACCESS_KEY, SECRET_KEY);

    AWS4Signer aws_ice_signer = new AWS4Signer();
    aws_ice_signer.setServiceName(iceServiceName);
    aws_ice_signer.setRegionName(REGION);
    aws_ice_signer.sign(aws_ice_sign_request, aws_ice_creds);


5. 응답 받은 ice 서버 리스트 데이터 파싱 및 List<PeerConnection.IceServer> 에 추가 실시


6. 조회 된 WSS 주소를 사용해 WeSocket 웹소켓 연결 AWS4Signer 헤더 생성 및 presignedUrl 생성 실시

    // 요청 ServiceName
    String iceServiceName = "kinesisvideo";

    // AWS Request 객체 생성
    com.amazonaws.Request<?> socket_sign_request = new DefaultRequest<Void>(iceServiceName);
    socket_sign_request.setHttpMethod(HttpMethodName.GET);
    socket_sign_request.setEndpoint(URI.create(wssEndpoint)); // TODO Set URL
    socket_sign_request.setEncodedResourcePath("/");

    // Query Parameter 추가
    Map<String, String> queryParams = new HashMap<>();
    queryParams.put("X-Amz-ChannelARN", CHANNEL_ARN); // Set Channel Arn
    queryParams.put("X-Amz-ClientId", CLIENT_ID); // Ser Viwer ClinetId

    socket_sign_request.setParameters(queryParams);

    // 서명자 설정

    BasicAWSCredentials aws_socket_creds = new BasicAWSCredentials(ACCESS_KEY, SECRET_KEY);

    AWS4Signer aws_socket_signer = new AWS4Signer();
    aws_socket_signer.setServiceName(iceServiceName);
    aws_socket_signer.setRegionName(REGION);

    Date expiration = new java.util.Date(System.currentTimeMillis() + 60 * 1000); // 1분
    aws_socket_signer.presignRequest(socket_sign_request, aws_socket_creds, expiration);


    // TODO 실제 소켓 연결 Presigned URL 생성
    String presignedUrl = "";

    StringBuilder url = new StringBuilder(socket_sign_request.getEndpoint().toString());
    url.append("?");
    boolean first = true;
    for (Map.Entry<String, String> entry : socket_sign_request.getParameters().entrySet()) {
        if (!first) url.append("&");
        first = false;
        url.append(URLEncoder.encode(entry.getKey(), StandardCharsets.UTF_8.name()))
                .append("=")
                .append(URLEncoder.encode(entry.getValue(), StandardCharsets.UTF_8.name()));
    }
    presignedUrl = url.toString();


7. peerConnection 객체 생성 수행 및 OkHttpClient 사용해 webSocket 접속 수행


8. webSocket onOpen 이 완료 된 경우 > peerConnection.createOffer 생성 수행

    MediaConstraints constraints = new MediaConstraints();
    constraints.mandatory.add(new MediaConstraints.KeyValuePair("OfferToReceiveAudio", "true"));
    constraints.mandatory.add(new MediaConstraints.KeyValuePair("OfferToReceiveVideo", "true"));

    peerConnection.createOffer(new org.webrtc.SdpObserver() {
        @Override
        public void onCreateSuccess(SessionDescription sdp) {
            S_Log._W_("STEP :: peerConnection :: createOffer :: onCreateSuccess", new String[]{ "type :: " + String.valueOf(sdp.type), "description :: " + String.valueOf(sdp.description) });

            peerConnection.setLocalDescription(this, sdp); // Set Local Description
        }
        @Override public void onSetSuccess() {
            S_Log._W_("STEP :: peerConnection :: createOffer :: onSetSuccess", null);
        }
        @Override public void onCreateFailure(String s) {
            S_Log._E_("STEP :: peerConnection :: createOffer :: onCreateFailure", new String[]{ String.valueOf(s) });
        }
        @Override public void onSetFailure(String s) {
            S_Log._E_("STEP :: peerConnection :: createOffer :: onSetFailure", new String[] { String.valueOf(s) });
        }
    }, constraints); // Set MediaConstraints


9 peerConnection offer 생성이 완료 (onCreateSuccess) 된 경우 SDP Offer 전문 생성 및 WebRTC 소켓 데이터 전송 수행

    peerConnection.setLocalDescription(sdpObserver, sdp);

    try {
        JSONObject msg = new JSONObject();
        msg.put("action", "SDP_OFFER"); // 기존 필드
        msg.put("messageType", "SDP_OFFER"); // 추가 필드
        msg.put("recipientClientId", ""); // 상대 clientId
        msg.put("senderClientId", WEB_RTC_CLIENT_ID);

        JSONObject payload = new JSONObject();
        payload.put("type", "offer");
        payload.put("sdp", sdp.description);


        // ---------------------------------------------
        // TODO [전송 Payload 전문 Base64 인코딩]
        // ---------------------------------------------
        msg.put("messagePayload", Base64.encodeToString(payload.toString().getBytes(), Base64.NO_WRAP));
        // ---------------------------------------------

        if (webSocket != null) webSocket.send(msg.toString()); // TODO Send WebSocket

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


10. 마스터가 보낸 SDP Answer 응답에서 올바르지 않은 비디오 코덱 정보가 내려오는 것 확인 

  >> a=rtpmap:0 H264/90000


11. 참고 사항 : 

  >> 자바스크립트, IOS 플랫폼에서는 SDP Offer 전송 시 자동으로 unEscape 처리 되어 전문 데이터가 날아감

  >> 안드로이드 플랫폼에서는 JSONObject 로 SDP Offer 생성 후 전송 시 자동 unEscape 처리 되지 않고 전송 되는 문제 확인

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





------------------------------------------------------------------------------
[조치 내용]
------------------------------------------------------------------------------

1. 안드로이드에서 SDP Offer 전송 시 messagePayload 부분에서 이스케이프 된 문자를 unEscape 처리 후 전송 하도록 변경 수행


2. 변경 된 SDP Offer 오퍼 전송 JSON 전문 : 

    peerConnection.setLocalDescription(sdpObserver, sdp);

    try {
        JSONObject msg = new JSONObject();
        msg.put("action", "SDP_OFFER"); // 기존 필드
        msg.put("messageType", "SDP_OFFER"); // 추가 필드
        msg.put("recipientClientId", ""); // 상대 clientId
        msg.put("senderClientId", WEB_RTC_CLIENT_ID);

        JSONObject payload = new JSONObject();
        payload.put("type", "offer");
        payload.put("sdp", sdp.description);

        // ---------------------------------------------
        // TODO [JSON unEscape 처리 문자]
        // ---------------------------------------------
        // TODO a=rtpmap:127 H264\/90000 >> a=rtpmap:127 H264/90000
        // ---------------------------------------------
        String unEscape = payload.toString()
                .replaceAll("\\\\/", "/"); // TODO \/ → /;

        S_Log._W_("SDP_Offer_unEscape", new String[]{"JSON :: " + payload.toString(), "UNESCAPE :: " + unEscape});

        msg.put("messagePayload", Base64.encodeToString(unEscape.getBytes(), Base64.NO_WRAP));
        // ---------------------------------------------

        if (webSocket != null) webSocket.send(msg.toString()); // TODO Send WebSocket

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


3. 정상적으로 SDP Answer 응답 받은 이후 실시간 영상 스트리밍이 재생 되는 것 확인

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





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

[업무 이슈] 안드로이드 슬러시 (특수 문자) 포함 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

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