2021. 1. 17. 2:30
https://blog.naver.com/nagne2011/222209854461
안드로이드 영상통화 peerjs - 3 - activity_call
#안드로이드 #영상통화 #peerjs 통화 연결 부분 엑티비티는 내용이 좀 많다 안드로이드는 xml작업이 너무 ...
blog.naver.com
#안드로이드 #영상통화 #peerjs
통화 연결 부분 엑티비티는 내용이 좀 많다
안드로이드는 xml작업이 너무 귀찮은듯.. 일단 진행!
순서대로 진행해보자
1. 웹뷰 추가
화면 전체에 웹뷰를 채워주자
<WebView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:id="@+id/webView"/>
2. '통화 요청' 팝업 추가
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
tools:text="Someone is calling..."
android:padding="20dp"
android:textStyle="bold"
android:id="@+id/incomingCallText"/>
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@drawable/ic_baseline_call_24"
android:id="@+id/acceptBtn"
android:layout_toStartOf="@id/rejectBtn"
android:padding="20dp"/>
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@drawable/ic_baseline_call_end_24"
android:id="@+id/rejectBtn"
android:padding="20dp"
android:layout_alignParentEnd="true"/>

사진 설명을 입력하세요.
이렇게 통화가 왔을때 상단에 통화를 받을지, 거부할지 뜨게된다
그런데 이건 통화가 왔을때 떠야하므로 평소엔 사라지도록 해야한다
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="#ccc"
android:visibility="gone"
android:id="@+id/callLayout">
아까 코드에 RelativeLayouy부분에 android:visiblity="gone"을 넣어주면 된다
3. 통화 신청
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="16dp"
android:layout_alignParentBottom="true"
android:id="@+id/inputLayout">
<EditText
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:id="@+id/friendNameEdit"
android:layout_toStartOf="@id/callBtn"
android:hint="Who do you want to call?"/>
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="call"
android:id="@+id/callBtn"
android:layout_alignParentEnd="true"/>
</RelativeLayout>
4. 영상 on/off, 마이크 on/off
영상 통화 도중 영상이나 마이크를 껐다켰다 하는 버튼 추가
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:visibility="gone"
android:layout_alignParentBottom="true"
android:layout_marginBottom="64dp"
android:id="@+id/callControlLayout">
<ImageView
android:layout_width="36dp"
android:layout_height="36dp"
android:background="#99000000"
android:padding="4dp"
android:src="@drawable/ic_baseline_videocam_24"
android:id="@+id/toggleVideoBtn"/>
<ImageView
android:layout_width="36dp"
android:layout_height="36dp"
android:padding="4dp"
android:background="#99000000"
android:layout_marginStart="36dp"
android:id="@+id/toggleAudioBtn"
android:src="@drawable/ic_baseline_mic_24"/>
</LinearLayout>

아까처럼 통화 중일때 표시해야되니까 일단 android:visiblity="gone" 추가
드디어 끝났다
이제 액티비티로 넘어가자
이름은 CallActivity
사용할 변수와 게터세터를 만들자
이 작업이 제일 귀찮다
일단 넘기고 필요할때 하나하나 만들어도 된다..
@NotNull
private String username = "";
@NotNull
private String friendsUsername = "";
private boolean isPeerConnected;
@NotNull
private DatabaseReference firebaseRef;
private boolean isAudio;
private boolean isVideo;
@NotNull
private String uniqueId;
@NotNull
public final String getUsername() {
return this.username;
}
public final void setUsername(@NotNull String var1) {
Intrinsics.checkNotNullParameter(var1, "<set-?>");
this.username = var1;
}
@NotNull
public final String getFriendsUsername() {
return this.friendsUsername;
}
public final void setFriendsUsername(@NotNull String var1) {
Intrinsics.checkNotNullParameter(var1, "<set-?>");
this.friendsUsername = var1;
}
public final boolean isPeerConnected() {
return this.isPeerConnected;
}
public final void setPeerConnected(boolean var1) {
this.isPeerConnected = var1;
}
@NotNull
public final DatabaseReference getFirebaseRef() {
return this.firebaseRef;
}
public final void setFirebaseRef(@NotNull DatabaseReference var1) {
Intrinsics.checkNotNullParameter(var1, "<set-?>");
this.firebaseRef = var1;
}
public final boolean isAudio() {
return this.isAudio;
}
public final void setAudio(boolean var1) {
this.isAudio = var1;
}
public final boolean isVideo() {
return this.isVideo;
}
public final void setVideo(boolean var1) {
this.isVideo = var1;
}
뭐이렇게 쓰이는게 많지만 핵심적인건 네가지
1. 자기 아이디
2. 상대방 아이디
3. peer연결 확인bool
4. 영상 및 음성 on/off 구분
(사실 코틀린으로 작성할땐 간단했는데 자바스크립트로 바꾸니까 저렇게 엄청난 코드가 되버렸다)
기본 생성자 및 종료함수등 부터 잡자
public void onBackPressed() {
this.finish();
}
protected void onDestroy() {
this.firebaseRef.child(this.username).setValue((Object)null);
WebView webView = (WebView)this.findViewById(R.id.webView);
webView.loadUrl("about:blank");
super.onDestroy();
}
public CallActivity() {
DatabaseReference var10001 = DatabaseKt.getDatabase(Firebase.INSTANCE).getReference("users");
Intrinsics.checkNotNullExpressionValue(var10001, "Firebase.database.getReference(\"users\")");
this.firebaseRef = var10001;
this.isAudio = true;
this.isVideo = true;
this.uniqueId = "";
}
웹뷰 종료 부분을 생성해주고
생성자에서는 Firebase에 유저 정보를 전송하고 시작한다
관련 변수 초기화는 필수
그 다음은 onCreate함수
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
this.setContentView(R.layout.activity_call);
String var10001 = this.getIntent().getStringExtra("username");
Intrinsics.checkNotNull(var10001);
this.username = var10001;
Button callbtn = (Button)this.findViewById(R.id.callBtn);
final EditText friendNameEdit = (EditText)this.findViewById(R.id.friendNameEdit);
callbtn.setOnClickListener((OnClickListener)(new OnClickListener() {
public final void onClick(View it) {
CallActivity var10000 = CallActivity.this;
EditText var10001 = friendNameEdit;
Intrinsics.checkNotNullExpressionValue(var10001, "friendNameEdit");
var10000.setFriendsUsername(var10001.getText().toString());
CallActivity.this.sendCallRequest();
}
}));
final ImageView toggleAudioBtn = (ImageView)this.findViewById(R.id.toggleAudioBtn);
toggleAudioBtn.setOnClickListener((OnClickListener)(new OnClickListener() {
public final void onClick(View it) {
CallActivity.this.setAudio(!CallActivity.this.isAudio());
CallActivity.this.callJavascriptFunction("javascript:toggleAudio(\"" + CallActivity.this.isAudio() + "\")");
toggleAudioBtn.setImageResource(CallActivity.this.isAudio() ? R.drawable.ic_baseline_mic_24 : R.drawable.ic_baseline_mic_off_24);
}
}));
final ImageView toggleVideoBtn = (ImageView)this.findViewById(R.id.toggleVideoBtn);
toggleVideoBtn.setOnClickListener((OnClickListener)(new OnClickListener() {
public final void onClick(View it) {
CallActivity.this.setVideo(!CallActivity.this.isVideo());
CallActivity.this.callJavascriptFunction("javascript:toggleVideo(\"" + CallActivity.this.isVideo() + "\")");
toggleVideoBtn.setImageResource(CallActivity.this.isVideo() ? R.drawable.ic_baseline_videocam_24 : R.drawable.ic_baseline_videocam_off_24);
}
}));
this.setupWebView();
}
매우 길지만.. 하나씩 살펴보면 별거없다
일단 xml과 연결해주는것으로 시작
this.setContentView(R.layout.activity_call);
그리고 intent로부터 유저 이름을 받는다
String var10001 = this.getIntent().getStringExtra("username");
Intrinsics.checkNotNull(var10001);
this.username = var10001;
그다음 'EditText'에 이름을 입력하고 call버튼을 눌렀을때 동작을 위한 코드를 넣어준다
sendCallRequest() 함수를 동작하도록 연결시켜준다
Button callbtn = (Button)this.findViewById(R.id.callBtn);
final EditText friendNameEdit = (EditText)this.findViewById(R.id.friendNameEdit);
callbtn.setOnClickListener((OnClickListener)(new OnClickListener() {
public final void onClick(View it) {
CallActivity var10000 = CallActivity.this;
EditText var10001 = friendNameEdit;
Intrinsics.checkNotNullExpressionValue(var10001, "friendNameEdit");
var10000.setFriendsUsername(var10001.getText().toString());
CallActivity.this.sendCallRequest();
}
}));
final ImageView toggleAudioBtn = (ImageView)this.findViewById(R.id.toggleAudioBtn);
toggleAudioBtn.setOnClickListener((OnClickListener)(new OnClickListener() {
public final void onClick(View it) {
CallActivity.this.setAudio(!CallActivity.this.isAudio());
CallActivity.this.callJavascriptFunction("javascript:toggleAudio(\"" + CallActivity.this.isAudio() + "\")");
toggleAudioBtn.setImageResource(CallActivity.this.isAudio() ? R.drawable.ic_baseline_mic_24 : R.drawable.ic_baseline_mic_off_24);
}
}));
final ImageView toggleVideoBtn = (ImageView)this.findViewById(R.id.toggleVideoBtn);
toggleVideoBtn.setOnClickListener((OnClickListener)(new OnClickListener() {
public final void onClick(View it) {
CallActivity.this.setVideo(!CallActivity.this.isVideo());
CallActivity.this.callJavascriptFunction("javascript:toggleVideo(\"" + CallActivity.this.isVideo() + "\")");
toggleVideoBtn.setImageResource(CallActivity.this.isVideo() ? R.drawable.ic_baseline_videocam_24 : R.drawable.ic_baseline_videocam_off_24);
}
}));
추가 기능으로 영상 및 음성 on/off 기능과 버튼을 연결시켜주자
통화가 활성화된 후 활성화 되게 만들고 클릭시 on/off 이미지가 변경되도록 넣어준다
중요한 부분은 callJavascriptFunction부분
자바스크립트와 연결해주는 부분이다
여기서 toggleVideo(bool값) 함수를 동작하도록 넣어줄 예정
마지막으로 웹뷰 준비함수 동작하자
setupWebView();
private final void setupWebView() {
WebView webView = (WebView)this.findViewById(R.id.webView);
Intrinsics.checkNotNullExpressionValue(webView, "webView");
webView.setWebChromeClient((WebChromeClient)(new WebChromeClient() {
public void onPermissionRequest(@Nullable PermissionRequest request) {
if (request != null) {
request.grant(request.getResources());
}
}
}));
WebSettings var10000 = webView.getSettings();
Intrinsics.checkNotNullExpressionValue(var10000, "webView.settings");
var10000.setJavaScriptEnabled(true);
var10000 = webView.getSettings();
Intrinsics.checkNotNullExpressionValue(var10000, "webView.settings");
var10000.setMediaPlaybackRequiresUserGesture(false);
webView.addJavascriptInterface(new JavascriptInterface(this), "Activity");
this.loadVideoCall();
}
웹뷰 등록 코드다
요즘엔 대부분 기기에서 크롬을 지원하니까 크롬 웹뷰를 연결한다
물론 기기에 따라 모든 플랫폼에 대응하도록 만들어도 되지만.. 일단 생략
먼저 웹뷰에 권한을 요청한뒤
웹뷰에 자바스크립트 동작 허용, 미디어 자동 재생 기능을 각각 활성화 한다
자세한건 링크 참고
https://developer.android.com/reference/android/webkit/WebSettings
https://developer.android.com/reference/android/webkit/WebSettings
추가 설명은 아래 포스팅을 참고하자
https://zerodice0.tistory.com/207
https://soulduse.tistory.com/45
이제 추가로 자바스크립트를 사용하기 위한 클래스를 추가해야한다
public final class JavascriptInterface {
@NotNull
private final CallActivity callActivity;
@android.webkit.JavascriptInterface
public final void onPeerConnected() {
this.callActivity.onPeerConnected();
}
@NotNull
public final CallActivity getCallActivity() {
return this.callActivity;
}
public JavascriptInterface(@NotNull CallActivity callActivity) {
Intrinsics.checkNotNullParameter(callActivity, "callActivity");
this.callActivity = callActivity;
}
}
새로운 클래스를 만들고 CallActivty와 연결시켜준다
그리고 이전 포스트에서 peer서버 접속시 동작하는 onPeerConnected()함수를 생성해준다
이제 자바스크립트와 액티비티간의 연결을 할수있다
다시 CallActivity에 돌아와 onPeerConnected와 관련 함수를 추가한다
private final void callJavascriptFunction(final String functionString) {
final WebView webView = (WebView)this.findViewById(R.id.webView);
webView.post((Runnable)(new Runnable() {
public final void run() {
webView.evaluateJavascript(functionString, (ValueCallback)null);
}
}));
}
public final void onPeerConnected() {
Log.d("tag", "내용 : 성공1");
this.isPeerConnected = true;
}
이어서 loadVideoCall() 함수도 제작
private final void loadVideoCall() {
String filePath = "file:android_asset/call.html";
WebView webView = (WebView)this.findViewById(R.id.webView);
webView.loadUrl(filePath);
Intrinsics.checkNotNullExpressionValue(webView, "webView");
webView.setWebViewClient((WebViewClient)(new WebViewClient() {
public void onPageFinished(@Nullable WebView view, @Nullable String url) {
CallActivity.this.initializePeer();
}
}));
}
전전 포스트에서 만들었던 html에 접근해서
xml로 만든 웹뷰에 띄워주는 기능
@NotNull
public final String getUniqueId() {
return this.uniqueId;
}
public final void setUniqueId(@NotNull String var1) {
Intrinsics.checkNotNullParameter(var1, "<set-?>");
this.uniqueId = var1;
}
private final void initializePeer() {
this.uniqueId = this.getUniqueID();
this.callJavascriptFunction("javascript:init(\"" + this.uniqueId + "\")");
this.firebaseRef.child(this.username).child("incoming").addValueEventListener((ValueEventListener)(new ValueEventListener() {
public void onCancelled(@NotNull DatabaseError error) {
Intrinsics.checkNotNullParameter(error, "error");
}
public void onDataChange(@NotNull DataSnapshot snapshot) {
Intrinsics.checkNotNullParameter(snapshot, "snapshot");
CallActivity var10000 = CallActivity.this;
Object var10001 = snapshot.getValue();
if (!(var10001 instanceof String)) {
var10001 = null;
}
var10000.onCallRequest((String)var10001);
}
}));
}
추가로 peer 초기화 부분도 만들어주고 아이디 생성부분도 만들어주자
생성된 아이디는 firebase에 저장되서 고유값으로 사용된다
생성 방법은 UUID.randomUUID
private final String getUniqueID() {
String var10000 = UUID.randomUUID().toString();
Intrinsics.checkNotNullExpressionValue(var10000, "UUID.randomUUID().toString()");
return var10000;
}
private final void callJavascriptFunction(final String functionString) {
final WebView webView = (WebView)this.findViewById(R.id.webView);
webView.post((Runnable)(new Runnable() {
public final void run() {
webView.evaluateJavascript(functionString, (ValueCallback)null);
}
}));
}
자바스크립트 연결 함수도 만들어서 마무리
그리고 통화 받기/거절 함수도 이어서 만들자
private final void onCallRequest(String caller) {
if (caller != null) {
final RelativeLayout callLayout = (RelativeLayout)this.findViewById(R.id.callLayout);
TextView incomingCallText = (TextView)this.findViewById(R.id.incomingCallText);
ImageView acceptBtn = (ImageView)this.findViewById(R.id.acceptBtn);
ImageView rejectBtn = (ImageView)this.findViewById(R.id.rejectBtn);
Intrinsics.checkNotNullExpressionValue(callLayout, "callLayout");
callLayout.setVisibility(View.VISIBLE);
Intrinsics.checkNotNullExpressionValue(incomingCallText, "incomingCallText");
incomingCallText.setText((CharSequence)(caller + " is calling..."));
acceptBtn.setOnClickListener((OnClickListener)(new OnClickListener() {
public final void onClick(View it) {
CallActivity.this.getFirebaseRef().child(CallActivity.this.getUsername()).child("connId").setValue(CallActivity.this.getUniqueId());
CallActivity.this.getFirebaseRef().child(CallActivity.this.getUsername()).child("isAvailable").setValue(true);
RelativeLayout var10000 = callLayout;
Intrinsics.checkNotNullExpressionValue(var10000, "callLayout");
var10000.setVisibility(View.GONE);
CallActivity.this.switchToControls();
}
}));
rejectBtn.setOnClickListener((OnClickListener)(new OnClickListener() {
public final void onClick(View it) {
CallActivity.this.getFirebaseRef().child(CallActivity.this.getUsername()).child("incoming").setValue((Object)null);
RelativeLayout var10000 = callLayout;
Intrinsics.checkNotNullExpressionValue(var10000, "callLayout");
var10000.setVisibility(View.GONE);
}
}));
}
}
매우 복잡하지만 하나하나 보자
통화 받기/거절 기능이니까 받기/거절 두가지 리스너로 나눈다
통화요청이, 그러니까 caller의 접근이 확인되면
아까 xml에 만들어놨던 상단부에 유저가 통화요청을 했고 받기/거부 버튼이 생성되도록 한다
'받기'의 경우 firebase에 데이터를 갱신해준다
'현재 상태'를 '통화중'으로 바꾸고 연결id를 갱신한다
그리고 받기 UI를 다시 숨긴다
'거절'의 경우엔 다른 동작없이 UI를 숨긴다
다음은 전화 요청 함수를 활성화
private final void sendCallRequest() {
if (!this.isPeerConnected) {
Toast myToast = Toast.makeText(this.getApplicationContext(),"You're not connected. Check your internet", Toast.LENGTH_SHORT);
myToast.show();
} else {
this.firebaseRef.child(this.friendsUsername).child("incoming").setValue(this.username);
this.firebaseRef.child(this.friendsUsername).child("isAvailable").addValueEventListener((ValueEventListener)(new ValueEventListener() {
public void onCancelled(@NotNull DatabaseError error) {
Intrinsics.checkNotNullParameter(error, "error");
String var2 = "Not yet implemented";
boolean var3 = false;
}
public void onDataChange(@NotNull DataSnapshot snapshot) {
Intrinsics.checkNotNullParameter(snapshot, "snapshot");
if (Intrinsics.areEqual(String.valueOf(snapshot.getValue()), "true")) {
CallActivity.this.listenForConnId();
}
}
}));
}
}
일단 peer 연결을 확인하고
연결 안되어있으면 에러메세지 출력
연결이 되어있어서 진행이 가능할 경우 "friendsUsername"의 데이터베이스의 '연결된 id'에 자기 이름을 올리고 '통화중'으로 갱신한다
취소했을 경우의 동작도 넣어주고
요청 성공했을땐 listenForConnId()함수를 동작해주자
private final void listenForConnId() {
this.firebaseRef.child(this.friendsUsername).child("connId").addValueEventListener((ValueEventListener)(new ValueEventListener() {
public void onCancelled(@NotNull DatabaseError error) {
Intrinsics.checkNotNullParameter(error, "error");
String var2 = "Not yet implemented";
boolean var3 = false;
}
public void onDataChange(@NotNull DataSnapshot snapshot) {
Intrinsics.checkNotNullParameter(snapshot, "snapshot");
if (snapshot.getValue() != null) {
CallActivity.this.switchToControls();
CallActivity.this.callJavascriptFunction("javascript:startCall(\"" + snapshot.getValue() + "\")");
}
}
}));
}
이제 반대로 "friendsUsername"정보를 가져와준다음
해당 아이디에 기존에 만들었던 startcall(유저이름) 함수에 넣어서 돌려주자
이러면 기존 html에서 통화거는 동작과 동일한 동작이 진행된다
차이점이라면 firebase를 통해서 id를 주고받고, 서로 연결여부 등을 갱신한다는 점이다
마지막으로 영상 데이터를 출력하는 부분을 완성하자
사진 설명을 입력하세요.

일단 styles로 가서
<style name="AppTheme.fullscreen" parent="Theme.MaterialComponents.DayNight.DarkActionBar">
<item name="windowActionBar">false</item>
<item name="windowNoTitle">true</item>
<item name="android:windowFullscreen">true</item>
</style>
상하단 네비게이션 바를 제거하고 시작
<activity android:name=".CallActivity"
android:theme="@style/AppTheme.fullscreen"></activity>
그리고 androidmanifest에 callactivty는 풀스크린으로 뜨도록 바꾼다
private final void switchToControls() {
RelativeLayout inputLayout = (RelativeLayout)this.findViewById(R.id.inputLayout);
LinearLayout callControlLayout = (LinearLayout)this.findViewById(R.id.callControlLayout);
Intrinsics.checkNotNullExpressionValue(inputLayout, "inputLayout");
inputLayout.setVisibility(View.GONE);
Intrinsics.checkNotNullExpressionValue(callControlLayout, "callControlLayout");
callControlLayout.setVisibility(View.VISIBLE);
}
통화 받을경우 CallActivty의 xml을 동작하고
영상/음성 on/off 관련 UI도 활성화시켜주면 끝
이제 본격적으로 진행을 위해 peer서버를 갱신해준다 (이전 포스트 하단 참고)
그리고 두 대의 기기를 준비해서 각각 임의 id로 접속
peer서버와 firebase서버에 각각 데이터가 등록됬는지 확인한 뒤
한쪽에서 다른 한쪽의 id로 접속을 시도하면 끝
*끝으로..
몇번 언급했지만 기존 코틀린 코드를 자바로 바꾸면서 오류도 많았고 변수명 등도 임의로 생성되는 부분도 많았고
무엇보다 코드가 엄청 길어졌다..
개인적으로는 C#을 쓰던 깜냥덕에 코틀린이 편하긴 한듯
다음에 사용하기 위해서 코드 정리를 해야할 필요도 있을꺼같지만 그건 아득히 먼 얘기..!
'안드로이드' 카테고리의 다른 글
안드로이드 Fragment - Dialog구현 (0) | 2022.02.15 |
---|---|
안드로이드 Fragment - 초기화, 이동(replace, add, remove) (0) | 2022.02.15 |
안드로이드 영상통화 peerjs - 2 - firebase (0) | 2022.02.15 |
안드로이드 영상통화 peerjs - 1 (0) | 2022.02.15 |
안드로이드 배너 - autoscroll-viewpager-indicator (0) | 2022.02.15 |