Notice
Recent Posts
Recent Comments
Link
투케이2K
1040. (Android/Java) [간단 소스] 안드로이드 AWS WebRTC 뷰어에서 SDP Answer 응답 부분 base64 디코딩 처리 소스 코드 본문
Android
1040. (Android/Java) [간단 소스] 안드로이드 AWS WebRTC 뷰어에서 SDP Answer 응답 부분 base64 디코딩 처리 소스 코드
투케이2K 2025. 10. 20. 21:42728x90
반응형
[개발 환경 설정]
개발 툴 : 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
반응형
'Android' 카테고리의 다른 글
Comments
