그룹통화 만들기(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" ) { participant:RemonParticipant ->
5
}.on( "onUserLeaved" ) { 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) {
9
participant ->
10
11
// 마스터 유저(송출자,나자신) 초기화
12
participant.localView = surfaceRendererArray[0]
13
14
// 뷰 설정
15
availableView[0] = true
16
}.close {
17
// 마스터 유저가 연결된 채널이 종료되면 호출됩니다.
18
// 송출이 중단되면 그룹통화에서 끊어진 것이므로, 다른 유저와의 연결도 모두 끊어집니다.
19
}.error {
20
error:RemonException ->
21
// 마스터 유저가 연결된 채널에서 에러 발생 시 호출됩니다.
22
// 오류로 연결이 종료되면 error -> close 순으로 호출됩니다.
23
}
Copied!

그룹통화 콜백

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