그룹통화 만들기(Android)

그룹통화란?

다수의 참여자가 통화에 참여하는 서비스를 위한 기능입니다. 참여자는 앱을 이용하는 나와 그 외 참여자로 구분할 수 있습니다. 아래에서는 나와 참여자로 줄여서 표시합니다. 한 회기의 그룹통화는 RemonConference 클래스의 인스턴스로 대표됩니다. 나는 통화 연결, 참여자들의 입장/퇴장 알림 등 대부분의 일을 RemonConference 객체에게 위임합니다.

RemonConference

안드로이드 SDK 버전 v2.7.0 이상 그룹통화를 위해 RemonConference 객체를 생성하고, 설정을 진행합니다.
RemonConference 클래스는 그룹통화를 위해 아래 메소드를 제공합니다.
1
create( String roomName, Config config, OnEventCallback callback);
2
leave()
Copied!
RemonConference 클래스는 콜백으로 사용하기 위해 아래 메소드를 제공합니다. 이하 콜백용 메소드라고 합니다. 콜백용 메소드는 위에서 언급한 메소드의 콜백으로만 호출하며, 일반적인 메소드처럼 호출하지 않습니다.
1
// 룸의 콜백용 메소드
2
on( "onRoomCreate" ) { participant:RemonParticipant ->
3
}.on( "onUserJoined" ) { participant:RemonParticipant ->
4
}.on( "onUserStreamConnected" ) { particpant:RemonParticipant ->
5
}.on( "onUserLeft" ) { participant:RemonParticipant ->
6
}.close {
7
}.error { error:RemonException ->
8
}
9
10
// participant 콜백용 메소드
11
.on( "onComplete" ) { participant:RemonParticipant ->
12
}
Copied!

레이아웃 작업

그룹통화 화면을 나의 영상 한 개와 그룹 참여자의 영상 여러 개로 구성합니다. 레이아웃에 영상을 표시할 view를 만들고 인덱스를 지정하여 참여자의 영상을 원하는 위치에 표시할 수 있도록 합니다.
1
<layout>
2
<RelativeLayout
3
android:id="@+id/rootLayout"
4
android:layout_width="match_parent"
5
android:layout_height="match_parent"
6
android:background="#000">
7
8
9
<androidx.constraintlayout.widget.ConstraintLayout
10
android:id="@+id/constraintLayout"
11
android:layout_width="match_parent"
12
android:layout_height="match_parent">
13
14
<!-- Local -->
15
<RelativeLayout
16
android:id="@+id/layout0"
17
android:layout_width="0dp"
18
android:layout_height="0dp"
19
android:layout_margin="10dp"
20
android:background="@drawable/view_shape"
21
app:layout_constraintDimensionRatio="H,1:1.33"
22
app:layout_constraintStart_toStartOf="parent"
23
app:layout_constraintTop_toTopOf="parent"
24
app:layout_constraintEnd_toEndOf="parent"
25
app:layout_constraintBottom_toBottomOf="parent"
26
>
27
28
<org.webrtc.SurfaceViewRenderer
29
android:id="@+id/surfRendererLocal"
30
android:layout_width="match_parent"
31
android:layout_height="match_parent"
32
android:visibility="visible"
33
/>
34
35
</RelativeLayout>
36
37
38
39
<!-- Remote 1 -->
40
<FrameLayout
41
android:id="@+id/layout1"
42
android:layout_width="80dp"
43
android:layout_height="0dp"
44
android:layout_margin="18dp"
45
app:layout_constraintDimensionRatio="H,1:1.33"
46
app:layout_constraintVertical_bias="0.1"
47
app:layout_constraintEnd_toEndOf="parent"
48
app:layout_constraintTop_toTopOf="parent"
49
app:layout_constraintBottom_toBottomOf="parent"
50
>
51
52
53
<org.webrtc.SurfaceViewRenderer
54
android:id="@+id/surfRendererRemote1"
55
android:layout_width="match_parent"
56
android:layout_height="match_parent"
57
android:visibility="invisible"
58
/>
59
</FrameLayout>
60
61
62
<!-- Remote 2 -->
63
<FrameLayout
64
android:id="@+id/layout2"
65
android:layout_width="80dp"
66
android:layout_height="0dp"
67
android:layout_margin="18dp"
68
app:layout_constraintDimensionRatio="H,1:1.33"
69
app:layout_constraintVertical_bias="0.3"
70
app:layout_constraintEnd_toEndOf="parent"
71
app:layout_constraintTop_toTopOf="parent"
72
app:layout_constraintBottom_toBottomOf="parent"
73
>
74
75
76
<org.webrtc.SurfaceViewRenderer
77
android:id="@+id/surfRendererRemote2"
78
android:layout_width="match_parent"
79
android:layout_height="match_parent"
80
android:visibility="invisible"
81
/>
82
83
</FrameLayout>
84
85
86
<!-- Remote 3 -->
87
<FrameLayout
88
android:id="@+id/layout3"
89
android:layout_width="80dp"
90
android:layout_height="0dp"
91
android:layout_margin="18dp"
92
app:layout_constraintDimensionRatio="H,1:1.33"
93
app:layout_constraintVertical_bias="0.5"
94
app:layout_constraintEnd_toEndOf="parent"
95
app:layout_constraintTop_toTopOf="parent"
96
app:layout_constraintBottom_toBottomOf="parent"
97
>
98
99
100
<org.webrtc.SurfaceViewRenderer
101
android:id="@+id/surfRendererRemote3"
102
android:layout_width="match_parent"
103
android:layout_height="match_parent"
104
android:visibility="invisible"
105
/>
106
107
</FrameLayout>
108
109
110
<!-- Remote 4 -->
111
<FrameLayout
112
android:id="@+id/layout4"
113
android:layout_width="80dp"
114
android:layout_height="0dp"
115
android:layout_margin="18dp"
116
app:layout_constraintDimensionRatio="H,1:1.33"
117
app:layout_constraintVertical_bias="0.7"
118
app:layout_constraintEnd_toEndOf="parent"
119
app:layout_constraintTop_toTopOf="parent"
120
app:layout_constraintBottom_toBottomOf="parent"
121
>
122
123
124
<org.webrtc.SurfaceViewRenderer
125
android:id="@+id/surfRendererRemote4"
126
android:layout_width="match_parent"
127
android:layout_height="match_parent"
128
android:visibility="invisible"
129
/>
130
131
</FrameLayout>
132
133
134
135
<!-- Remote 5 -->
136
<FrameLayout
137
android:id="@+id/layout5"
138
android:layout_width="80dp"
139
android:layout_height="0dp"
140
android:layout_margin="18dp"
141
142
app:layout_constraintDimensionRatio="H,1:1.33"
143
app:layout_constraintVertical_bias="0.9"
144
app:layout_constraintEnd_toEndOf="parent"
145
app:layout_constraintTop_toTopOf="parent"
146
app:layout_constraintBottom_toBottomOf="parent"
147
>
148
149
150
<org.webrtc.SurfaceViewRenderer
151
android:id="@+id/surfRendererRemote5"
152
android:layout_width="match_parent"
153
android:layout_height="match_parent"
154
android:visibility="invisible"
155
/>
156
157
</FrameLayout>
158
159
160
</androidx.constraintlayout.widget.ConstraintLayout>
161
</RelativeLayout>
162
</layout>
Copied!

레이아웃 초기화

레이아웃을 바인딩하고, 각 view를 배열에 담아 index 로 접근이 가능하도록 설정합니다.
1
var surfaceRendererArray:Array<SurfaceViewRenderer>
2
3
binding = DataBindingUtil.setContentView( this, R.layout.activity_name )
4
surfaceRendererArray = arrayOf(
5
binding.surfRendererLocal,
6
binding.surfRendererRemote1,
7
binding.surfRendererRemote2,
8
binding.surfRendererRemote3,
9
binding.surfRendererRemote4,
10
binding.surfRendererRemote5
11
)
12
13
// 비어있는 뷰를 처리하기 위한 배열입니다. 각 서비스에 따라 구
14
var availableView:Array<Boolean>
15
availableView = Array(mSurfaceViewArray.size) {false}
Copied!

RemonConference 객체 생성

RemonConference 객체를 생성하고, 나의 영상을 송출하기 위한 설정을 합니다.
1
private var remonConference = RemonConference()
2
3
var config = Config()
4
config.context = this
5
config.serviceId = "콘솔을 통해 발급 받은 Service Id"
6
config.key = "콘솔을 통해 발급 받은 Secret Key"
7
8
remonConference.create( "방이름", config) { participant ->
9
// 마스터 유저(송출자,나자신) 초기화
10
participant.localView = surfaceRendererArray[0]
11
12
// 뷰 설정
13
availableView[0] = true
14
}.close {
15
// 마스터 유저가 연결된 채널이 종료되면 호출됩니다.
16
// 송출이 중단되면 그룹통화에서 끊어진 것이므로, 다른 유저와의 연결도 모두 끊어집니다.
17
}.error { error:RemonException ->
18
// 마스터 유저가 연결된 채널에서 에러 발생 시 호출됩니다.
19
// 오류로 연결이 종료되면 error -> close 순으로 호출됩니다.
20
}
Copied!

그룹통화 콜백

그룹통화가 생성되면 송출이 시작되고, 각 콜백이 호출됩니다. 콜백은 create() 호출후 on("이벤트"){} 형태로 등록할 수 있습니다. 새 참여자가 그룹통화에 입장하면 연결된 on 메소드의 콜백이 호출됩니다. on 메소드 콜백에서 참여자의 RemonParticipant 객체가 제공되므로, 해당 정보를 사용해 설정을 진행합니다.
1
remonConference.create( "방이름", config) {
2
.
3
.
4
}.on( "onRoomCreated" ) { participant ->
5
// 마스터 유저가 접속된 이후에 호출(실제 송출 시작)
6
// TODO: 실제 유저 정보는 각 서비스에서 관리하므로, 서비스에서 채널과 실제 유저 매핑 작업 진행
7
8
// tag 객체에 holder 패턴 형태로 객체를 지정해 사용할 수 있습니다.
9
// 예제에서는 뷰설정을 위해 단순히 view의 index를 저장합니다.
10
participant.tag = 0
11
}.on( "onUserJoined" ) { participant ->
12
Log.d( TAG, "Joined new user" )
13
// 그룹통화에 새로운 잠여자가 입장했을 때 호출됩니다.
14
// 다른 사용자가 입장한 경우 초기화를 위해 호출됨
15
// TODO: 실제 유저 매핑 : it.id 값으로 연결된 실제 유저를 얻습니다.
16
17
18
// 뷰 설정
19
val index = getAvailableView()
20
if( index > 0 ) {
21
participant.config.localView = null
22
participant.config.remoteView = mSurfaceViewArray[index]
23
participant.tag = index
24
}
25
26
// 피어가 연결이 완료되었을때 처리할 작업이 있는 경우
27
participant.on( "onComplete" ) { participant ->
28
// updateView()
29
}
30
}.on( "onUserStreamConnected" ) { participant ->
31
// 해당 유저의 onComplete와 동일
32
// updateView()
33
34
}.on( "onUserLeft" ) { participant ->
35
// 상대방이 그룹통화에서 퇴장한 경우 or 연결이 종료된 경우 호출됩니다.
36
// id 와 tag 를 참조해 어떤 사용자가 퇴장했는지 확인후 퇴장 처리를 합니다.
37
val index = participant.tag as Int
38
availableView[index] = false
39
}
40
41
42
// 비어있는 뷰는 아래처럼 얻어올 수 있습니다.
43
// 서비스에 해당하는 부분이므로 각 서비스 UI에 맞게 구성합니다.
44
private fun getAvailableView(): Int {
45
for( i in 0 until this.mAvailableView.size) {
46
if(!mAvailableView[i]) {
47
mAvailableView[i] = true
48
return i
49
}
50
}
51
return -1
52
}
Copied!

그룹통화 종료

그룹통화에서 퇴장하면 나와 그룹통화의 연결이 종료됩니다. 나와 참여자들 간의 연결도 종료됩니다.
1
remonConference.leave()
Copied!

RemonParticipant

각 참여자들과의 연결은 RemonConference 내부의 RemonParticipant 객체를 통해 이루어집니다. RemonParticipant 객체는 RemonClient를 상속받은 객체이므로, 공통적인 기능은 RemonCall, RemonCast 와 동일합니다. 각 이벤트마다 RemonParticipant 객체가 전달되므로 각 연결은 해당 객체를 통해 제어할 수 있으며, 마스터 객체의 경우 RemonConference 객체에서 얻어올 수 있습니다.
1
// 마스터 유저 얻기
2
RemonParticipant participant = remonConference.me
3
Copied!
RemonParticipant 객체는 RemonClient를 상속받은 객체입니다. onCreate, onClose, onError 콜백은 on으로 재정의되어 RemonConference에서 관리, 사용되고 있으므로, 해당 콜백을 변경하지 마시기 바랍니다.
Last modified 1yr ago