지난 글에서 Zoom Video SDK 가 어떤 SDK 인지에 대해 정리했다. 이번 글에서는 Video SDK 를 사용하는 기초적인 방법에 대해 정리한다. 본 글은 공식 문서의 Essential Guides 를 기반으로 정리한다.
0. Integrate SDK
Zoom Marketplace 에서 Video SDK 를 활성화한 계정으로 로그인을 한 뒤, SDK 페이지에서 SDK 를 다운 받는다.
좌측에 있는 플랫폼을 선택하고, 가장 최신 버전의 SDK 를 [Download] 한다. 본 글은 Android 에서 Video SDK 를 적용시켜보는 과정을 정리할 것이기 때문에, Android 의 SDK 를 다운로드 받았다.
SDK 만 있는 것이 아니라, 샘플 앱 자체가 다운로드 된 것을 확인할 수 있다. 여기서, "mobilertc" 폴더만 SDK 를 추가하고 싶은 프로젝트로 복사한다. 복사-붙여넣기를 하는 것이 아니라, 추가하고 싶은 프로젝트에서 [File] - [Project Structure] 메뉴로 들어가, 모듈로 추가한다.
위와 같이 Modules 목록에 "mobilertc" 가 추가되었다면, OK 버튼을 누르고 마무리한다. 이제 추가한 모듈을 App 모듈에서 인식할 수 있도록 Dependency 를 추가한다.
dependencies {
implementation project(':mobilertc')
}
App 모듈의 "build.gradle" 파일에서, 방금 추가한 mobilertc 모듈을 Dependency 로 추가한다. 추가한 후에 "Sync Project" 까지 해주면 끝이다. 이제 App 모듈 내에서 Video SDK 의 API 에 접근할 수 있다.
추가적으로, Video SDK 를 사용하기 위해 필요한 권한들이 존재한다.
[ Required Permissions ]
- Camera : Required for video
- Microphone : Required for audio
- System Alert Window : Required to show system alert window
- Photo Library : Used for sharing images from the photo library
위 권한들 중 필수적이라 할 수 있는 "Camera", "Microphone", "System Alert Window" 권한을 AndroidManifest 파일에 추가한다.
// AndroidManifest.xml
...
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.RECORD_AUDIO" />
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />
...
Video SDK 를 사용하기 위한 기초 작업을 마쳤다. 이제 Video SDK 를 초기화하여, 그 안에 있는 함수들을 사용해본다.
1. Initialize the SDK
우선, Video SDK 의 API 들을 사용하기 위해서는 SDK 를 초기화 시켜줘야한다. "ZoomSDKInitParams" 객체를 인스턴스화하고, domain 값을 "zoom.us" 로 설정한다. 부가적으로, 로그나 디버깅 목적을 추가하고 싶다면, 다른 설정값을 넣을 수 있다.
SDK 인스턴스를 생성하는 코드는 다음과 같다.
fun initVideoSDK() {
val params = ZoomVideoSDKInitParams().apply {
domain = "https://zoom.us" // Required
logFilePrefix = "MyLogPrefix" // Optional for Debugging
enableLog = true // Optional for Debugging
}
val sdk = ZoomVideoSDK.getInstance()
val initResult = sdk.initialize(this@MainActivity, params)
if (initResult == ZoomVideoSDKErrors.Errors_Success) {
// Initialized Successfully
} else {
// Initialization Failed
}
}
ZoomVideoSDKInitParams 를 생성할 때, logFilePrefix 와 enableLog 프로퍼티는 필수 요소가 아니다. 디버깅을 원할 때 추가적으로 설정하는 부분이고, domain 값만 설정해줘도 SDK 초기화가 가능하다. Params 를 설정하고, SDK 객체를 생성하여 Initailize 메소드를 호출할 때 파라미터로 넘겨준다. 그리고, 초기화 성공 여부에 따른 분기점을 추가할 수 있다.
만약, SDK 객체 초기화에 성공했다면, Video SDK 에 있는 다양한 API 들에 접근이 가능해진다. 또한, 세션 생성 및 종료와 같은 각종 이벤트에 대한 리스너를 설정하는 것도 가능하다.
2. Listen for Callback Events
"ZoomVideoSDKDelegate" 는 Video SDK 를 통해 사용되는 동작들의 이벤트 콜백을 제공한다. 대표적인 예로, 세션이 시작되거나 끊겼을 때에 그에 대한 콜백을 받을 수 있다.
ZoomVideoSDKDelegate 는 콜백 함수들을 담고 있는 Interface 이다. 위 사진처럼 Activity 에 implement 해주면, 제공 받을 수 있는 콜백 함수 내용을 볼 수 있다. 예상보다 많이 있기 때문에, 테스트를 위해서 한단계 거치도록 수정을 해보자.
지금 상태로는 위 사진에 나오는 모든 함수들을 Override 해줘야 한다. 필요 없는 콜백 함수까지 빈 블록으로 Activity 에 넣어두는 것은 비효율적이라 생각하기 때문에, 중간에 한번 상속을 받아서 필요 없는 콜백 함수들은 Default 함수가 되도록 구현해두자.
interface MyVideoSDKDelegate: ZoomVideoSDKDelegate {
override fun onSessionJoin()
override fun onSessionLeave()
override fun onError(errorCode: Int)
override fun onUserJoin(userHelper: ZoomVideoSDKUserHelper?, userList: List<ZoomVideoSDKUser?>?)
override fun onUserLeave(
userHelper: ZoomVideoSDKUserHelper?,
userList: List<ZoomVideoSDKUser?>?
)
override fun onUserVideoStatusChanged(
videoHelper: ZoomVideoSDKVideoHelper?,
userList: List<ZoomVideoSDKUser?>?
) {}
override fun onUserAudioStatusChanged(
audioHelper: ZoomVideoSDKAudioHelper?,
userList: List<ZoomVideoSDKUser?>?
) {}
...
}
ZoomVideoSDKDelegagte 를 상속받는 "MyVideoSDKDelegate" 인터페이스를 생성하고, ZoomVideoSDKDelegate 에 정의된 함수들을 모두 Override 한다. 그리고, 사용하지 않을 함수들의 끝에 "{}" 를 붙여주어서 Default 함수가 되도록 설정한다. 그러면, MyVideoSDKDelegate 를 implement 해도 ZoomVideoSDKDelegate 의 Type 으로 사용할 수 있고, 많은 함수들을 다 안고 갈 필요도 없어진다.
// MainActivity.kt
...
private fun setVideoSDKListener() {
videoSDK.addListener(this)
}
override fun onSessionJoin() {
TODO("Not yet implemented")
}
override fun onSessionLeave() {
TODO("Not yet implemented")
}
override fun onError(errorCode: Int) {
TODO("Not yet implemented")
}
override fun onUserJoin(
userHelper: ZoomVideoSDKUserHelper?,
userList: List<ZoomVideoSDKUser?>?
) {
TODO("Not yet implemented")
}
override fun onUserLeave(
userHelper: ZoomVideoSDKUserHelper?,
userList: List<ZoomVideoSDKUser?>?
) {
TODO("Not yet implemented")
}
...
현재 공식 문서의 예제 코드를 따라해보기 위해, 필자의 주관적인 판단으로 필요하다고 생각되는 함수들만 남겨놨다. 앱을 개발하다보면 모든 함수가 필요할 수도 있다. 그 때는 위와 같은 과정이 필요하지 않을 것이다. 다만, 그 때는 Activity 에 구현을 하지 않고, 별도의 ZoomVideoManager 와 같이 Video SDK 를 전담하는 객체를 만들어서 그곳에 구현을 해둘 예정이다.
다시 본론으로 돌아와서, 콜백 함수들 중에 어떤 것이 필요한지 알기 위해서는 모든 콜백 함수들이 어떤 역할을 하는지 알아볼 필요가 있다. 공식 문서에서 기술한 각 콜백 함수의 목적에 대해 정리한다. ZoomVideoSDKDelegate 인터페이스에 있는 모든 함수가 정리된 것은 아니다.
2-1) onError(int errorCode)
override fun onError(errorCode: Int) {
// See error code documentation
}
onError 콜백 함수는 SDK 와 관련된 동작들의 에러를 알려준다. 에러 코드 문서는 링크 를 첨부한다.
2-2) onUserJoin(ZoomVideoSDKUserHelper userHelper, List<ZoomVideoSDKUser> userList)
override fun onUserJoin(userHelper: ZoomVideoSDKUserHelper?, userList: MutableList<ZoomVideoSDKUser>) {
userList.forEach {
// Access user info for each user that has joined
}
}
사용자가 Session 에 들어왔을 때 알려주는 콜백 함수이다.
2-3) onUserLeave(ZoomVideoSDKUserHelper userHelper, List<ZoomVideoSDKUser> userList)
override fun onUserLeave(userHelper: ZoomVideoSDKUserHelper?, userList: MutableList<ZoomVideoSDKUser>) {
userList.forEach {
// Access user info for each user that has left
}
}
사용자가 Session 에서 나갔을 때 알려주는 콜백 함수이다.
2-4) onUserVideoStatusChanged(List<ZoomVideoSDKUser> userList)
override fun onUserVideoStatusChanged(videoHelper: ZoomVideoSDKVideoHelper?, userList: MutableList<ZoomVideoSDKUser>) {
userList.forEach {
// Check if the current user's video is on
val videoStatus = it.videoStatus
videoStatus.isOn
}
}
사용자의 비디오 상태값이 변했을 때 호출되는 콜백 함수이다.
2-5) onUserAudioStatusChanged(ZoomVideoSDKAudioHelper audioHelper, List<ZoomVideoSDKUser> userList)
override fun onUserAudioStatusChanged(audioHelper: ZoomVideoSDKAudioHelper?, userList: MutableList<ZoomVideoSDKUser>) {
userList.forEach {
// Check the current user to see if they are muted
val audioStatus = it.audioStatus
audioStatus.isMuted
}
}
사용자의 오디오 상태값이 변했을 때 호출되는 콜백 함수이다.
2-6) onUserShareStatusChanged(ZoomVideoSDKShareHelper shareHelper, ZoomVideoSDKUser userInfo, ZoomVideoSDKShareStatus status)
override fun onUserShareStatusChanged(shareHelper: ZoomVideoSDKShareHelper?, userInfo: ZoomVideoSDKUser?, status: ZoomVideoSDKShareStatus?) {
when (status) {
ZoomVideoSDKShareStatus.ZoomVideoSDKShareStatus_Start,
ZoomVideoSDKShareStatus.ZoomVideoSDKShareStatus_Resume -> {
// The user with the corresponding userInfo is now sharing
}
ZoomVideoSDKShareStatus.ZoomVideoSDKShareStatus_Pause,
ZoomVideoSDKShareStatus.ZoomVideoSDKShareStatus_Stop -> {
// The user with the corresponding userInfo is not sharing
}
}
}
화면 공유의 상태값이 변했을 때 호출되는 콜백 함수이다.
2-7) onLiveStreamStatusChanged(ZoomVideoSDKLiveStreamStatus status)
override fun onLiveStreamStatusChanged(status: ZoomVideoSDKLiveStreamStatus) {
// See live stream documentation
}
라이브 스트리밍의 상태값이 변했을 때 호출되는 콜백 함수이다.
2-8) onChatNewMessageNotify(ZoomVideoSDKChatHelper chatHelper, ZoomVideoSDKChatMessage messageItem)
override fun onChatNewMessageNotify(chatHelper: ZoomVideoSDKChatHelper?, messageItem: ZoomVideoSDKChatMessage) {
val content = messageItem.content
val sender = messageItem.senderUser
}
새로운 메시지가 게시됐을 때 호출되는 콜백 함수이다.
2-9) onUserHostChanged(ZoomVideoSDKUserHelper userHelper, ZoomVideoSDKUser userInfo)
override fun onUserHostChanged(userHelper: ZoomVideoSDKUserHelper?, userInfo: ZoomVideoSDKUser?) {
// Recommended action: check if current user is the new host and update UI if applicable
}
Session 의 Host 가 바뀌었을 때 호출되는 함수이다.
2-10) onUserActiveAudioChanged(ZoomVideoSDKAudioHelper audioHelper, List<ZoomVideoSDKUser> list)
override fun onUserActiveAudioChanged(audioHelper: ZoomVideoSDKAudioHelper?, list: MutableList<ZoomVideoSDKUser>) {
list.forEach {
// Check if the current user is talking
val audioStatus = it.audioStatus
audioStatus.isTalking
}
}
활성화된 오디오가 바뀌었을 때 호출되는 콜백함수이다.
2-11) onSessionNeedPassword(ZoomVideoSDKPasswordHandler handler)
override fun onSessionNeedPassword(handler: ZoomVideoSDKPasswordHandler?) {
// Try entering password again
handler?.inputSessionPassword("anotherPassword")
// Leave the session without trying
handler?.leaveSessionIgnorePassword()
}
비밀번호가 필요한 Session 에 비밀번호 없이 입장하려 할때 호출되는 콜백 함수이다.
2-12) onSessionPasswordWrong(ZoomVideoSDKPasswordHandler handler)
override fun onSessionPasswordWrong(handler: ZoomVideoSDKPasswordHandler?) {
// Try entering password again
handler?.inputSessionPassword("anotherPassword")
// Leave the session without trying
handler?.leaveSessionIgnorePassword()
}
Session 에 입장하려 할 때, 비밀번호를 잘못 입력하면 호출되는 콜백 함수이다.
2-13) onSessionJoin()
override fun onSessionJoin() {
// You have successfully joined the session
}
Session 에 성공적으로 입장했을 때 호출되는 콜백 함수이다.
2-14) onSessionLeave()
override fun onSessionLeave() {
// You have successfully left the session
}
Session 에서 성공적으로 나왔을 때 호출되는 콜백 함수이다.
2-15) onUserManagerChanged(ZoomVideoSDKUser zoomVideoSDKUser)
override fun onUserManagerChanged(user: ZoomVideoSDKUser) {
// Recommended action: check if current user is the new host and update UI if applicable
}
현재 사용자가 새로운 호스트로 되었을 때 호출되는 콜백 함수이다.
2-16) onUserNameChanged(ZoomVideoSDKUser zoomVideoSDKUser)
override fun onUserNameChanged(user: ZoomVideoSDKUser) {
// Recommended action: get new username and update UI if applicable
}
사용자의 이름이 변경되었을 때 호출되는 콜백 함수이다.
이 외에도 ZoomVideoSDKDelegate 로 콜백 받을 수 있는 함수가 더 있다. 더 많은 내용은 Zoom 의 JavaDocs 파일에서 확인하면 된다.
3. Create, Join, Leave Session
Session 은 Video SDK 를 사용함에 있어 가장 기초가 되는 부분이다. Session 을 통해 Video, Audio 를 사용한 화상 회의가 가능하고, 좀 더 구현하면 채팅과 화면 공유 기능까지 사용할 수 있다.
3-1) Create a Session
Session 을 생성할 때, 개발자의 JWT 값과 Session Name 이 꼭 필요하다. Session Name 을 통해 생성하거나 참여하고자 하는 Session 이 현재 활성화 되어있는지 확인할 수 있기 때문이다. 만약, Session Name 값을 가지고 "joinSession()" 메소드를 호출하면, 전달된 Session Name 으로 활성화된 Session 이 존재하는지 확인한다. 이미 존재한다면 해당 Session 으로 Join 하게 되고, 없다면 전달받은 Session Name 으로 새로운 Session 을 생성한다.
Session 을 생성하는 코드는 다음과 같다.
private fun createVideoSession() {
val params = ZoomVideoSDKSessionContext().apply {
sessionName = "[SESSION_NAME]"
userName = "[USER_NAME]"
token = "[JWT]"
}
videoSDK.joinSession(params)
}
createSession 과 같은 생성 전용 함수가 있을 것 같지만, "joinSession()" 함수를 통해 생성과 참여가 모두 가능하다. 그 이유는 아까도 언급했듯이 Session Name 을 비교하여 이미 있는 Session 인지 확인하여 생성 및 참여를 하기 때문이다.
Session 을 생성하기 전에 ZoomVideoSDKDelegate 를 통해 콜백 함수들을 추가했었다. 현재 코드로 Session 을 생성하고, 어떤 콜백 함수가 호출되는지 확인해보자. 아까는 콜백 함수를 몇 개 생략했지만, 이번에는 모든 콜백 함수를 구현한 후에 로그로 어떤 콜백 함수가 호출되는지 확인한다.
class MainActivity : AppCompatActivity(), ZoomVideoSDKDelegate {
...
override fun onSessionJoin() {
Log.d(TAG, "onSessionJoin: ")
}
override fun onSessionLeave() {
Log.d(TAG, "onSessionLeave: ")
}
override fun onError(errorCode: Int) {
Log.d(TAG, "onError: ")
}
override fun onUserJoin(
userHelper: ZoomVideoSDKUserHelper?,
userList: MutableList<ZoomVideoSDKUser>?
) {
Log.d(TAG, "onUserJoin: ")
}
override fun onUserLeave(
userHelper: ZoomVideoSDKUserHelper?,
userList: MutableList<ZoomVideoSDKUser>?
) {
Log.d(TAG, "onUserLeave: ")
}
...
}
위 코드와 같이 각 콜백 함수에 로그 출력 함수를 추가했다. 이제 Session 을 생성하기 위해 Session Name, User Name, 그리고 JWT 를 생성한다.
- Session Name : mysession
- User Name : myname
임의로 Session Name 과 User Name 을 정했으면, JWT 를 발급받도록 하자. Production 버전이라면 JWT 값을 앱 코드 내에 하드 코딩하지 말아야하며, 앱과 연결된 백엔드에서 발급 받는것이 좋다. 다만, 지금은 테스트를 해보기 위함이고, 백엔드가 없는 상태이므로, jwt.io 사이트에서 발급 받는다.
JWT 발급 방법에 대한 자세한 설명은 이전 글의 "3. SDK Authentication" 섹션에서 다뤘었다. 본 글에서는 JWT 발급 방법에 대한 자세한 설명을 생략한다.
우선, Zoom Marketplace 에 로그인을 한 후, SDK 페이지("Build Video SDK" 메뉴 클릭) 로 들어간다. Video SDK 페이지의 맨 하단을 보면, SDK Key-Secret 값과 API Key-Secret 값들을 확인할 수 있다. 이 중에 SDK Key-Secret 값이 JWT 발급에 필요한 값이다.
SDK Key 값을 Payload 의 "app_key" 값으로 넣어주고, SDK Secret 값을 Signature 의 "Secret" 값으로 넣어주면 된다.
그 외에 "tpc" 값은 임의로 정했던 Session Name 값을 넣어주고, "version" 값은 1 로 고정한다. "role_type" 은 Host 가 1, Participant 가 0 으로 설정해야하는 부분인데, 현재 Host 의 역할로 Session 을 생성할 것이기 때문에 1 로 설정했다. "user_identity" 는 임의로 정한 User Name 값을 넣어준다. "session_key" 는 role_type 이 Host 일 경우엔 필수값이므로, 아무 값이나 넣어준다. 해당 값은 Video SDK 의 Dashboard 에서 확인할 수 있는 값이다. 마지막으로, "iat" 값은 Session 을 생성하는 현재 시간의 Timestamp 값을 넣어주고, "exp" 값은 만료 시간을 넣어준다. 여기서 주의할 점은, "exp" 와 "iat" 간의 시간 값은 30분에서 48시간 사이가 되어야한다. 만약, 48시간 이상의 값으로 "exp" 를 설정하면, 2003 에러가 발생한다. (Session Error 2003)
이제 JWT 발급에 필요한 값을 모두 넣었다. 그러면 왼쪽의 Encoded 부분에 JWT 값이 생성되어 있을 것이다. 그 값을 복사하여, Video Session 을 생성하는 앱 코드에 넣어준다.
// MainActivity.kt
class MainActivity : AppCompatActivity(), ZoomVideoSDKDelegate {
...
override fun onCreate(savedInstanceState: Bundle?) {
...
createVideoSession()
}
private fun createVideoSession() {
val params = ZoomVideoSDKSessionContext().apply {
sessionName = "mysession"
userName = "myname"
token = "eyJhbGciOiJIU.eyJhcFSjVk3NjAxMzh9.zEz4YPh0g"
}
videoSDK.joinSession(params)
}
...
}
위 코드처럼 마무리해주면, 앱을 실행하자마자 Video Session 을 생성하게 될 것이다. 그러면, ZoomVideoSDKDelegate 의 콜백 함수들에서 반응이 온다.
SDK 초기화가 성공적으로 이루어지고, Session Join 과 User Join 까지 정상적으로 이루어진다. 현재는 Video Rendering 이 추가되어 있지 않기 때문에, Session 생성이 성공적으로 된 것에 만족하도록 한다.
추가적으로, 위에 "createVideoSession()" 메소드를 호출하면 생성되는 ZoomVideoSDKSessionContext 객체의 Property 에 대해 알아보자. 지금은 "sessionName", "userName", "token" 값들을 설정해주었으나, 그 외에 설정할 수 있는 값이 더 존재한다.
Name | Required | Description |
sessionName | O | Session 의 이름이자, UID 를 의미한다. 150자까지 입력할 수 있으며, 특수기호 및 공백까지 포함시킨다. JWT 의 Payload 에서 "tpc" 값과 동일 해야한다. |
userName | O | Session 에 참가한 사용자의 이름. 만약, 빈 값이라면, "null" 로 표시된다. 글자 수 제한은 없다. |
token | O | SDK Credentials 값으로 생성한 JWT Token 값. |
sessionPassword | X | Session 의 비밀번호. 설정하면 해당 회의실은 Private 공간으로 분류되고, 올바른 비밀번호를 입력한 사용자만이 회의에 참석할 수 있게 된다. 비밀번호는 최대 10글자까지 가능하다. |
audioOption | X | ZoomVideoSDKAudioOptions 에 저장된 값의 오디오 세팅값. |
videoOption | X | ZoomVideoSDKVideoOptions 에 저장된 값의 비디오 세팅값. |
3-2) Join a Session
val session = ZoomVideoSDK.getInstance().joinSession(params)
Session 을 생성할 때와 마찬가지로 "joinSession()" 메소드를 통해 Session 에 참가할 수 있다. 위 코드를 보면, joinSession() 메소드의 반환값을 받을 수 있다는 것을 알 수 있다. 그런데, 반환값은 Session 연결 시도를 의미할 뿐, 반환값을 성공적으로 받았다고 해서 Session 에 참여했다는 뜻은 아니다.
Session 에 참여했다는 것을 정확히 아는 방법은 콜백 함수에서 얻는 것이다. ZoomVideoSDKDelegate 에 있는 "onSessionJoin()" 이나 "onUserJoin" 과 같은 콜백 함수를 통해 정상적으로 Session 에 참여했다는 것을 파악할 수 있다.
3-3) Obtain Session Information
Session 참여에 성공한 후에, Session 에 대한 정보들을 얻을 수 있다. SDK 객체로부터 ZoomVideoSDKSession 객체를 받아내면 된다. 코드는 다음과 같다.
val session = ZoomVideoSDK.getInstance().session
session.sessionName
session.sessionPassword
session.sessionHost
session.sessionHostName
Session 참여에 성공하면 "onSessionJoin()" 콜백 함수가 호출되므로, 그 안에 Session 객체의 정보를 로그로 확인해보았다. 정상적으로 동작하며, Session 을 생성할 때 정했던 "mysession" 과 "myname" 값이 똑같이 나온 것을 볼 수 있다. Session Password 는 설정하지 않았기 때문에, "null" 로 출력되었다.
3-4) Leave a Session
Session 생성 및 참가에 대한 코드를 살펴봤으니, 이번에는 Session 에서 떠나는 코드를 알아볼 차례이다. "leaveSession()" 메소드를 호출함으로써 Session 에서 떠날 수 있으며, 만약 Host 상태라면, 해당 Session 을 아예 종료할 것인지 결정해야한다.
// A value of true will end the session if you are the host
ZoomVideoSDK.getInstance().leaveSession(shouldEndSession)
이번 글에서는 SDK 를 직접 Import 시키고, 초기화까지 해보았다. 그리고, 아직 Video Rendering 을 하지 못했지만, Video SDK 의 근본이라 볼 수 있는 Session 을 생성, 참여 및 해제하는 방법까지 알아보았다.
다음 글에서는 Video Rendering 을 해보고, 다른 기본적인 사용 방법에 대해 정리해보도록 한다.
'Android Developer' 카테고리의 다른 글
RadarChartView 직접 만들기 : 1. CustomView Overview (0) | 2022.08.18 |
---|---|
Android Zoom Video SDK : 3. Essential Guides (2) (0) | 2022.08.10 |
Android Zoom Video SDK : 1. Overview (0) | 2022.07.28 |
Android Zoom Meeting SDK : 2. Integrate SDK (0) | 2022.06.30 |
Android Zoom Meeting SDK : 1. 데모앱 살펴보기 (0) | 2022.06.24 |