0
Help us understand the problem. What are the problem?

posted at

【Zoom Video SDK】初心者による導入メモ - Android/Kotlin編

この記事は【Zoom Video SDK】初心者による導入メモ - iOS/Swift編に続くAndroid/Kotlin編です。
Swift編ではZoom Video SDKが初見だったので初心者すぎて詰まりまくりでしたが、現在はSwiftUI版をもくもく実装中です。複数人のグループ会話を実装する上で、iOSだとシミュレータでカメラが使えなくてテストが捗らないので、Androidのシミュレータで端末数を確保しようと思い、今回はAndroid(Kotlin)でZoom Video SDKを入門したいと思います。

完成イメージ

前回と同様に自分と相手の二人でビデオ会話できるシンプルなアプリです。
image.png

ハマりどころは

基本的には公式ドキュメントに沿って進めていけばOKでした。行間が省かれてる箇所があり初心者には戸惑うこともありましたが前回のiOS版での経験値を活かし? 空気を読んで読み替えることで割とサクサク進めることができました。

ということでやっていきたいと思います。

使用環境

  • MacBook Air (M1, 2020)
  • macOS Monterey 12.4
  • Android Studio Chipmunk | 2021.2.2 Patch 1
  • zoom-video-sdk-android-1.3.1

Android Studioのプロジェクト設定

  1. ライブラリーの追加
    ダウンロードしたSDKのSample-Libs/mobilertc-android配下にある mobilertcをAndroid Studioのプロジェクトの直下にコピーします。mobilertcにはmobilertc.aarが含まれています。
     
    ライブラリーをコピーしたら、setting.gradle(Project Settings)の末尾にinclude ':mobilertc'を追加します。
setting.gradle
pluginManagement {
    repositories {
        gradlePluginPortal()
        google()
        mavenCentral()
    }
}
dependencyResolutionManagement {
    repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
    repositories {
        google()
        mavenCentral()
    }
}
rootProject.name = "ZoomVideo1"
include ':app'
include ':mobilertc'

そして、build.gradle(Module)のdependenciesにimplementation project(':mobilertc')追加します。

build.gradle(Module)
...

dependencies {

    implementation 'androidx.core:core-ktx:1.7.0'
    implementation 'androidx.appcompat:appcompat:1.4.1'
    implementation 'com.google.android.material:material:1.6.0'
    implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
    testImplementation 'junit:junit:4.13.2'
    androidTestImplementation 'androidx.test.ext:junit:1.1.3'
    androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
    implementation project(':mobilertc')

}

  
2. パーミンションを追加
カメラとマイクのパーミンションを設定します。SDKドキュメントには具体的な設定値の記載がありませんでしたが、これで動作しました。

AndroidManifest.xml
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    package="com.example.zoomvideo1">

    <!-- ここから -->
    <uses-permission android:name="android.permission.CAMERA" />
    <uses-permission android:name="android.permission.RECORD_AUDIO" />
    <!-- ここまで -->

    <application
      ...

  
3. JWTトークンの取得
これが少々めんどうですが前回のJWTトークンの生成の記事を参考にトークンを作成します。

これで準備完了です。

コードの実装ポイント

レイアウトファイルにViewを設置

activity_main.xmlにビデオ映像を表示するためのViewを置きます。自分の映像と相手の映像の2つ定義しています。

activity_main.xml
    <us.zoom.sdk.ZoomVideoSDKVideoView
        android:id="@+id/remoteVideoView"/>
    <us.zoom.sdk.ZoomVideoSDKVideoView
        android:id="@+id/localVideoView">

パーミンションの確認

hasPermissions()でパーミンションを確認して、パーミンションが無ければrequestPermissions()でパーミンションを要求します。

MainActivity.kt
    private fun hasPermissions(context: Context) =
        permissions.all {
            ContextCompat.checkSelfPermission(context, it) == PackageManager.PERMISSION_GRANTED
        }

    private fun requestPermissions(activity: MainActivity) {
        ActivityCompat.requestPermissions(activity, permissions, PERMISSIONS_REQUEST_CODE)
    }

    override fun onRequestPermissionsResult(
        requestCode: Int,
        permissions: Array<out String>,
        grantResults: IntArray
    ) {
        super.onRequestPermissionsResult(requestCode, permissions, grantResults)
        if (requestCode == PERMISSIONS_REQUEST_CODE) {
            var grantedCount = 0
            for (result in grantResults) {
                if (result == PackageManager.PERMISSION_GRANTED) {
                    grantedCount++
                }
            }
            if (grantResults.size == grantedCount) {
                // ここに成功時の処理を書く
                initZoomSDK()
            } else {
                Toast.makeText(applicationContext, "アクセスを許可してください", Toast.LENGTH_LONG)
                    .show()
            }
        }
    }

    companion object {
        const val PERMISSIONS_REQUEST_CODE = 1

        val permissions = arrayOf(
            Manifest.permission.CAMERA,
            Manifest.permission.RECORD_AUDIO
        )
    }

SDKの初期化とコールバックの実装

SDKの初期化メソッドを呼び出し部とコールバックを実装します。コールバック数が多くて手打ちだと大変なので、Android Studioのコードアシスタントでまとめて作成します。その後、公式ドキュメントから必要なスニペットをコピペします。
自分の環境ではスニペットをペーストするとダブルコートが全角になってしまったので半角に修正する必要がありました。

    private fun initZoomSDK() {
        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, params)
        if (initResult == ZoomVideoSDKErrors.Errors_Success) {
            // You have successfully initialized the SDK
            Log.d(TAG, "initialize: successfully")

        } else {
            // Something went wrong, see error code documentation
            Log.d(TAG, "initialize: failed")
        }

        val listener = object : ZoomVideoSDKDelegate {
            override fun onSessionJoin() {
                Log.d(TAG, "onSessionJoin")

                val session = ZoomVideoSDK.getInstance().session
                session.mySelf.videoCanvas.subscribe(
                    localVideoView,
                    ZoomVideoSDKVideoAspect.ZoomVideoSDKVideoAspect_Original
                )

                joinButton.setText("Leave")
            }
            
            // (略)
        }
    }

セッションへの参加

セッション名やJWTトークンを設定し、joinSession()を呼び出します。トークン作成時のセッション名・ユーザー名・パスワードが一致していないとonErrorが発生するので注意深く設定します。

    private fun join(name: String) {
        val audioOptions = ZoomVideoSDKAudioOption().apply {
            connect = true // Auto connect to audio upon joining
            mute = false // Auto mute audio upon joining
        }
        val videoOptions = ZoomVideoSDKVideoOption().apply {
            localVideoOn = true // Turn on local/self video upon joining
        }
        val params = ZoomVideoSDKSessionContext().apply {
            sessionName = "Session1"
            userName = name
            sessionPassword = "123"
            token = "" // TODO: Pass in your JWT. In a production app, ensure that you do not hard code JWT or any confidential credentials.
            audioOption = audioOptions
            videoOption = videoOptions
        }

        val session = ZoomVideoSDK.getInstance().joinSession(params)
        if (session != null) {
            Log.d(TAG, "joinSession")
            Log.d(TAG, "  sessionName: $session.sessionName")
            Log.d(TAG, "  sessionID: $session.sessionID")

            val videoHelper = ZoomVideoSDK.getInstance().videoHelper
            videoHelper.startVideo()
        }
    }

    private fun leave() {
        val shouldEndSession = true
        ZoomVideoSDK.getInstance().leaveSession(shouldEndSession)
    }

ビデオの開始

セッションが開始されたらsubscribe()でキャンバスに関連付けます。これでビデオ映像が表示されます。

          override fun onSessionJoin() {
                Log.d(TAG, "onSessionJoin")

                val session = ZoomVideoSDK.getInstance().session
                session.mySelf.videoCanvas.subscribe(
                    localVideoView,
                    ZoomVideoSDKVideoAspect.ZoomVideoSDKVideoAspect_Original
                )

                joinButton.setText("Leave")
            }

ビデオの終了

セッションから抜けるとキャンバスを解放します。

            override fun onSessionLeave() {
                Log.d(TAG, "onSessionLeave")

                val session = ZoomVideoSDK.getInstance().session
                session.mySelf.videoCanvas.unSubscribe(localVideoView) // todo: unSubscribeしてもViewがクリアされない

                leave()
                joinButton.setText("Join")
            }

unSubscribe()してもViewに以前の映像がゴミとして残ってしまいました。iOSの場合は自動的に背景色でクリアされたのですが、Androidの場合はクリアしてくれませんでした...。解決方法をご存知の方、教えていただけると助かります。

サンプルコード

今回のサンプルコード全文を掲載します。

activity_main.xml

activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <EditText
        android:id="@+id/userNameEditText"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:layout_marginStart="10dp"
        android:layout_marginTop="10dp"
        android:layout_marginEnd="10dp"
        android:ems="10"
        android:inputType="textPersonName"
        android:minHeight="48dp"
        android:text="user123"
        app:layout_constraintEnd_toStartOf="@+id/joinButton"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

    <Button
        android:id="@+id/joinButton"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginTop="10dp"
        android:layout_marginEnd="10dp"
        android:text="Join"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintHorizontal_bias="0.5"
        app:layout_constraintStart_toEndOf="@+id/userNameEditText"
        app:layout_constraintTop_toTopOf="parent" />

    <us.zoom.sdk.ZoomVideoSDKVideoView
        android:id="@+id/remoteVideoView"
        android:layout_width="0dp"
        android:layout_height="0dp"
        android:layout_marginStart="10dp"
        android:layout_marginTop="20dp"
        android:layout_marginEnd="10dp"
        android:layout_marginBottom="10dp"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/joinButton" />

    <us.zoom.sdk.ZoomVideoSDKVideoView
        android:id="@+id/localVideoView"
        android:layout_width="123dp"
        android:layout_height="142dp"
        android:layout_marginStart="10dp"
        android:layout_marginTop="10dp"
        android:background="#C33939"
        android:backgroundTint="#CA4545"
        android:backgroundTintMode="add"
        app:layout_constraintStart_toStartOf="@+id/remoteVideoView"
        app:layout_constraintTop_toTopOf="@+id/remoteVideoView">
    </us.zoom.sdk.ZoomVideoSDKVideoView>
</androidx.constraintlayout.widget.ConstraintLayout>

MainActivity.kt

MainActivity.kt
package com.example.zoomvideo1

import android.Manifest
import android.annotation.SuppressLint
import android.content.Context
import android.content.pm.PackageManager
import android.os.Bundle
import android.util.Log
import android.widget.Button
import android.widget.EditText
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat
import us.zoom.sdk.*

class MainActivity : AppCompatActivity() {
    private val TAG = "MyDebug#MainActivity"
    private lateinit var joinButton: Button
    private lateinit var localVideoView: ZoomVideoSDKVideoView
    private lateinit var remoteVideoView: ZoomVideoSDKVideoView

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        joinButton = findViewById(R.id.joinButton)
        localVideoView = findViewById(R.id.localVideoView)
        remoteVideoView = findViewById(R.id.remoteVideoView)

        if (!hasPermissions(this)) {
            requestPermissions(this)
        } else {
            initZoomSDK()
        }

        joinButton.setOnClickListener {
            if (ZoomVideoSDK.getInstance().isInSession) {
                leave()
            } else {
                val userNameEditText = findViewById<EditText>(R.id.userNameEditText)
                join(userNameEditText.text.toString())
            }
        }
    }

    private fun hasPermissions(context: Context) =
        permissions.all {
            ContextCompat.checkSelfPermission(context, it) == PackageManager.PERMISSION_GRANTED
        }

    private fun requestPermissions(activity: MainActivity) {
        ActivityCompat.requestPermissions(activity, permissions, PERMISSIONS_REQUEST_CODE)
    }

    override fun onRequestPermissionsResult(
        requestCode: Int,
        permissions: Array<out String>,
        grantResults: IntArray
    ) {
        super.onRequestPermissionsResult(requestCode, permissions, grantResults)
        if (requestCode == PERMISSIONS_REQUEST_CODE) {
            var grantedCount = 0
            for (result in grantResults) {
                if (result == PackageManager.PERMISSION_GRANTED) {
                    grantedCount++
                }
            }
            if (grantResults.size == grantedCount) {
                initZoomSDK()
            } else {
                Toast.makeText(applicationContext, "アクセスを許可してください", Toast.LENGTH_LONG)
                    .show()
            }
        }
    }

    companion object {
        const val PERMISSIONS_REQUEST_CODE = 1

        val permissions = arrayOf(
            Manifest.permission.CAMERA,
            Manifest.permission.RECORD_AUDIO
        )
    }

    private fun join(name: String) {
        val audioOptions = ZoomVideoSDKAudioOption().apply {
            connect = true // Auto connect to audio upon joining
            mute = false // Auto mute audio upon joining
        }
        val videoOptions = ZoomVideoSDKVideoOption().apply {
            localVideoOn = true // Turn on local/self video upon joining
        }
        val params = ZoomVideoSDKSessionContext().apply {
            sessionName = "Session1"
            userName = name
            sessionPassword = "123"
            token = "" // TODO: Pass in your JWT. In a production app, ensure that you do not hard code JWT or any confidential credentials.
            audioOption = audioOptions
            videoOption = videoOptions
        }

        val session = ZoomVideoSDK.getInstance().joinSession(params)
        if (session != null) {
            Log.d(TAG, "joinSession")
            Log.d(TAG, "  sessionName: $session.sessionName")
            Log.d(TAG, "  sessionID: $session.sessionID")

            val videoHelper = ZoomVideoSDK.getInstance().videoHelper
            videoHelper.startVideo()
            //videoHelper.switchCamera()
        }
    }

    private fun leave() {
        val shouldEndSession = true
        ZoomVideoSDK.getInstance().leaveSession(shouldEndSession)
    }

    private fun initZoomSDK() {
        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, params)
        if (initResult == ZoomVideoSDKErrors.Errors_Success) {
            // You have successfully initialized the SDK
            Log.d(TAG, "initialize: successfully")

        } else {
            // Something went wrong, see error code documentation
            Log.d(TAG, "initialize: failed")
        }

        val listener = object : ZoomVideoSDKDelegate {
            override fun onSessionJoin() {
                Log.d(TAG, "onSessionJoin")

                val session = ZoomVideoSDK.getInstance().session
                session.mySelf.videoCanvas.subscribe(
                    localVideoView,
                    ZoomVideoSDKVideoAspect.ZoomVideoSDKVideoAspect_Original
                )

                joinButton.setText("Leave")
            }

            override fun onSessionLeave() {
                Log.d(TAG, "onSessionLeave")

                val session = ZoomVideoSDK.getInstance().session
                session.mySelf.videoCanvas.unSubscribe(localVideoView) // todo: unSubscribeしてもViewがクリアされない

                leave()
                joinButton.setText("Join")
            }

            override fun onError(errorCode: Int) {
                when (errorCode) {
                    ZoomVideoSDKErrors.Errors_Session_Join_Failed -> { // 2003
                        // トークンの期限切れかも
                        Log.d(TAG, "onError: Errors_Session_Join_Failed($errorCode)")
                    }

                    else -> {
                        Log.d(TAG, "onError: $errorCode")
                    }
                }
            }

            @SuppressLint("WrongViewCast")
            override fun onUserJoin(
                userHelper: ZoomVideoSDKUserHelper?,
                userList: MutableList<ZoomVideoSDKUser>?
            ) {
                Log.d(TAG, "onUserJoin")
                userList?.forEach { user ->
                    Log.d(TAG, "  userName: ${user.userName}")

                    user.videoCanvas.subscribe(
                        remoteVideoView,
                        ZoomVideoSDKVideoAspect.ZoomVideoSDKVideoAspect_Original
                    )
                }
            }

            override fun onUserLeave(
                userHelper: ZoomVideoSDKUserHelper?,
                userList: MutableList<ZoomVideoSDKUser>?
            ) {
                Log.d(TAG, "onUserLeave")

                userList?.forEach { user ->
                    Log.d(TAG, "  id: " + user.userID)
                    Log.d(TAG, "  name: " + user.userName)

                    user.videoCanvas.unSubscribe(remoteVideoView) // todo: unSubscribeしてもViewがクリアされない
                }
            }

            override fun onUserVideoStatusChanged(
                videoHelper: ZoomVideoSDKVideoHelper?,
                userList: MutableList<ZoomVideoSDKUser>?
            ) {
                Log.d(TAG, "onUserVideoStatusChanged")
            }

            override fun onUserAudioStatusChanged(
                audioHelper: ZoomVideoSDKAudioHelper?,
                userList: MutableList<ZoomVideoSDKUser>?
            ) {
                Log.d(TAG, "onUserAudioStatusChanged")
            }

            override fun onUserShareStatusChanged(
                shareHelper: ZoomVideoSDKShareHelper?,
                userInfo: ZoomVideoSDKUser?,
                status: ZoomVideoSDKShareStatus?
            ) {
                Log.d(TAG, "onUserShareStatusChanged")
            }

            override fun onLiveStreamStatusChanged(
                liveStreamHelper: ZoomVideoSDKLiveStreamHelper?,
                status: ZoomVideoSDKLiveStreamStatus?
            ) {
                Log.d(TAG, "onLiveStreamStatusChanged")
            }

            override fun onChatNewMessageNotify(
                chatHelper: ZoomVideoSDKChatHelper?,
                messageItem: ZoomVideoSDKChatMessage?
            ) {
                Log.d(TAG, "onChatNewMessageNotify")
            }

            override fun onUserHostChanged(
                userHelper: ZoomVideoSDKUserHelper?,
                userInfo: ZoomVideoSDKUser?
            ) {
                Log.d(TAG, "onUserHostChanged")
            }

            override fun onUserManagerChanged(user: ZoomVideoSDKUser?) {
                Log.d(TAG, "onUserManagerChanged")
            }

            override fun onUserNameChanged(user: ZoomVideoSDKUser?) {
                Log.d(TAG, "onUserNameChanged")
            }

            override fun onUserActiveAudioChanged(
                audioHelper: ZoomVideoSDKAudioHelper?,
                list: MutableList<ZoomVideoSDKUser>?
            ) {
                Log.d(TAG, "onUserActiveAudioChanged")
            }

            override fun onSessionNeedPassword(handler: ZoomVideoSDKPasswordHandler?) {
                Log.d(TAG, "onSessionNeedPassword")
            }

            override fun onSessionPasswordWrong(handler: ZoomVideoSDKPasswordHandler?) {
                Log.d(TAG, "onSessionPasswordWrong")
            }

            override fun onMixedAudioRawDataReceived(rawData: ZoomVideoSDKAudioRawData?) {
                Log.d(TAG, "onMixedAudioRawDataReceived")
            }

            override fun onOneWayAudioRawDataReceived(
                rawData: ZoomVideoSDKAudioRawData?,
                user: ZoomVideoSDKUser?
            ) {
                Log.d(TAG, "onOneWayAudioRawDataReceived")
            }

            override fun onShareAudioRawDataReceived(rawData: ZoomVideoSDKAudioRawData?) {
                Log.d(TAG, "onShareAudioRawDataReceived")
            }

            override fun onCommandReceived(sender: ZoomVideoSDKUser?, strCmd: String?) {
                Log.d(TAG, "onCommandReceived")
            }

            override fun onCommandChannelConnectResult(isSuccess: Boolean) {
                Log.d(TAG, "onCommandChannelConnectResult")
            }

            override fun onCloudRecordingStatus(status: ZoomVideoSDKRecordingStatus?) {
                Log.d(TAG, "onCloudRecordingStatus")
            }

            override fun onHostAskUnmute() {
                Log.d(TAG, "onHostAskUnmute")
            }

            override fun onInviteByPhoneStatus(
                status: ZoomVideoSDKPhoneStatus?,
                reason: ZoomVideoSDKPhoneFailedReason?
            ) {
                Log.d(TAG, "onInviteByPhoneStatus")
            }

            override fun onMultiCameraStreamStatusChanged(
                status: ZoomVideoSDKMultiCameraStreamStatus?,
                user: ZoomVideoSDKUser?,
                videoPipe: ZoomVideoSDKRawDataPipe?
            ) {
                Log.d(TAG, "onMultiCameraStreamStatusChanged")
            }

            override fun onMultiCameraStreamStatusChanged(
                status: ZoomVideoSDKMultiCameraStreamStatus?,
                user: ZoomVideoSDKUser?,
                canvas: ZoomVideoSDKVideoCanvas?
            ) {
                Log.d(TAG, "onMultiCameraStreamStatusChanged")
            }
        }

        ZoomVideoSDK.getInstance().addListener(listener)
    }
}

まとめ

iOS用のSDKとAndroid用のSDKでほとんど仕様が同じでAPIもシンプルなため使いやすいなと思いました。MVVMモデルなどでUIとの分離もしやすそうなので、iOSとAndroidのそれぞれで効率良く開発できそうです。今後はAndroid Jetpack Composeによる実装も試してみたいなと思います。

参考サイト

Register as a new user and use Qiita more conveniently

  1. You can follow users and tags
  2. you can stock useful information
  3. You can make editorial suggestions for articles
What you can do with signing up
0
Help us understand the problem. What are the problem?