如何用Agora SDK用Kotlin创建一个一对一视频通话的安卓应用

296 阅读6分钟

用Agora SDK创建一个使用Kotlin的一对一视频通话Android应用

在开发一个包含视频通话功能的安卓应用时,你可能最终会有很多模板代码。

Agora SDK是一个平台,它允许开发者用相对较少的代码创建丰富的应用内体验,如嵌入式语音和视频聊天、实时录音、实时流媒体和实时消息。

前提条件

要继续学习本教程,你需要具备以下条件。

  • 在你的机器上安装[Android Studio]。
  • 对开发和运行Android应用程序有扎实的了解。
  • [Kotlin]编程语言的基础知识。
  • 一个Agora账户。
  • 有AndroidViewBinding 的经验。

目标

在本教程结束时,你将能够。

  • 理解什么是Agora视频通话SDK。
  • 创建并获得Agora SDK的access key
  • 在一个一对一的视频通话应用程序中实施SDK。

什么是Agora视频通话SDK?

Agora视频通话SDK是一个平台,允许开发者创建丰富的应用内体验,如嵌入式语音和视频聊天、实时录音、实时流媒体和实时消息。

Agora的视频通话API增强了社交应用程序的新功能,如AR面部面具和分享屏幕时的声音效果、白板以及其他可能有利于商业和教育应用程序的功能。

在本教程中,我们将使用SDK在一个安卓应用中添加视频通话功能。

在Agora仪表板上创建一个项目

打开Agora开发者控制台,创建一个新项目,如下图所示。

New Agora App

选择一个适合你的应用的用例,如教育、社交、娱乐等。一旦你创建了这个项目,你就可以在控制台中看到它。点击编辑按钮,生成一个临时令牌,你将在你的应用程序中使用。

Edit Agora App

滚动到页面底部,选择为音频/视频呼叫生成临时令牌。

Token Page

输入通道名称并点击生成临时令牌。

Generate Token

注意APP ID,Channel Name, 和你的Temp Token 。在接下来的步骤中,它们将被要求。

Generated Token

创建一个安卓项目

打开你的Android Studio,创建一个空的项目,给它一个你选择的名字。

Android App

设置该项目

在你的应用级build.gradle 文件中,添加以下依赖关系。

dependencies{
    ...
    
    implementation 'io.agora.rtc:full-sdk:3.1.3'
}

在你的Manifest 文件中,添加以下权限。

  • INTERNET
  • read_phone_state
  • RECORD_AUDIO
  • MODIFY_AUDIO_SETTINGS和
  • 相机

为了防止你的proguard-rules.pro 中的代码混淆,添加以下代码。

-keep class io.agora.**{*;}

在你的res 目录中,打开values "strings 并包括你的APP_IDTEMP_TOKEN

<resources>
    ...
    
    <string name="app_id">APP_ID</string>
    <string name="agora_token">TEMP_TOKEN</string>
</resources>

确保你的agora_token 指向你从Agora控制台获得的令牌。

设计用户界面

在这一步,我们将创建一个简单的布局,其中有一个FrameLayout 来显示你的视频,有一个RelativeLayout 来显示另一个人的视频。我们还将有一些ImageViews (作为按钮使用),当静音麦克风,启动或结束通话,以及切换摄像头时。

<?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:id="@+id/activity_main_chat_view"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <RelativeLayout
        android:id="@+id/remoteVideoView"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:background="@color/remoteBackground">

        <ImageView
            android:layout_width="70dp"
            android:layout_height="70dp"
            android:layout_centerInParent="true"
            android:scaleType="centerCrop"
            android:src="@drawable/icon_agora_largest"
            tools:ignore="ContentDescription" />
    </RelativeLayout>

    <FrameLayout
        android:id="@+id/localVideoView"
        android:layout_width="100dp"
        android:layout_height="150dp"
        android:layout_alignParentEnd="true"
        android:layout_marginTop="24dp"
        android:layout_marginEnd="24dp"
        android:background="@color/localBackground"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintTop_toTopOf="parent">

        <ImageView
            android:layout_width="40dp"
            android:layout_height="40dp"
            android:layout_gravity="center"
            android:scaleType="centerCrop"
            android:src="@drawable/icon_agora_large"
            tools:ignore="ContentDescription" />
    </FrameLayout>

    <RelativeLayout
        android:id="@+id/controls"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_alignParentBottom="true"
        android:layout_marginBottom="24dp"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent">

        <ImageView
            android:id="@+id/buttonCall"
            android:layout_width="50dp"
            android:layout_height="50dp"
            android:layout_centerHorizontal="true"
            android:layout_centerVertical="true"
            android:scaleType="centerCrop"
            android:src="@drawable/btn_endcall"
            tools:ignore="ContentDescription" />

        <ImageView
            android:id="@+id/buttonMute"
            android:layout_width="50dp"
            android:layout_height="50dp"
            android:layout_centerVertical="true"
            android:layout_marginEnd="30dp"
            android:layout_toStartOf="@id/buttonCall"
            android:scaleType="centerCrop"
            android:src="@drawable/btn_unmute"
            tools:ignore="ContentDescription" />

        <ImageView
            android:id="@+id/buttonSwitchCamera"
            android:layout_width="50dp"
            android:layout_height="50dp"
            android:layout_centerVertical="true"
            android:layout_marginStart="30dp"
            android:layout_toEndOf="@id/buttonCall"
            android:scaleType="centerCrop"
            android:src="@drawable/btn_switch_camera"
            tools:ignore="ContentDescription" />

    </RelativeLayout>
</androidx.constraintlayout.widget.ConstraintLayout>

记住要从资源管理器中添加所需的图标。

创建应用程序逻辑

声明

在你的MainActivity.kt 文件中,添加以下声明。

private val PERMISSION_REQUEST_ID = 7

// Ask for Android device permissions at runtime.
private val ALL_REQUESTED_PERMISSIONS = arrayOf(
    Manifest.permission.RECORD_AUDIO,
    Manifest.permission.CAMERA,
    Manifest.permission.READ_PHONE_STATE
)

private var mEndCall = false
private var mMuted = false
private var remoteView: SurfaceView? = null
private var localView: SurfaceView? = null
private lateinit var rtcEngine: RtcEngine

初始化RtcEngine对象

创建这个方法将初始化Agora的RtcEngine。RtcEngine是Agora SDK的核心类。

private fun initRtcEngine() {
    try {
        rtcEngine = RtcEngine.create(baseContext, getString(R.string.app_id), mRtcEventHandler)
    } catch (e: Exception) {
        Log.d(TAG, "initRtcEngine: $e")
    }
}

设置视频配置

private fun setupVideoConfig() {

    rtcEngine.enableVideo()
    // Set the video encoding profile.
    rtcEngine.setVideoEncoderConfiguration(
        VideoEncoderConfiguration(
            VideoEncoderConfiguration.VD_640x360,
            VideoEncoderConfiguration.FRAME_RATE.FRAME_RATE_FPS_15,
            VideoEncoderConfiguration.STANDARD_BITRATE,
            VideoEncoderConfiguration.ORIENTATION_MODE.ORIENTATION_MODE_FIXED_PORTRAIT
        )
    )
}

设置本地和远程视频

在这一步,我们将设置本地视频和当前用户将观看的远程视频。

private fun setupLocalVideoView() {
    localView = RtcEngine.CreateRendererView(baseContext)
    localView!!.setZOrderMediaOverlay(true)
    binding.localVideoView.addView(localView)
    rtcEngine.setupLocalVideo(VideoCanvas(localView, VideoCanvas.RENDER_MODE_HIDDEN, 0))
}

private fun setupRemoteVideoView(uid: Int) {
    if (binding.remoteVideoView.childCount > 1) {
        return
    }
    remoteView = RtcEngine.CreateRendererView(baseContext)
    binding.remoteVideoView.addView(remoteView)
    rtcEngine.setupRemoteVideo(VideoCanvas(remoteView, VideoCanvas.RENDER_MODE_FILL, uid))
}

加入一个频道

设置好本地视频后,当前用户需要加入一个频道,以开始接收远程视频流。

private fun joinChannel() {
    val token = getString(R.string.agora_token)
    // Join a channel with a token.
    rtcEngine.joinChannel(token, "ChannelOne", "Extra Optional Data", 0)
}

请确保频道名称与你在创建临时令牌时输入的名称相似。

离开一个频道

private fun leaveChannel() {
    rtcEngine.leaveChannel()
}

初始化Agora引擎并加入一个频道

创建这个函数,它将结合我们刚刚创建的三个函数。

这些是加入一个频道和启动一个呼叫时的常规步骤。

private fun initAndJoinChannel() {
    initRtcEngine()
    setupVideoConfig()
    setupLocalVideoView()
    joinChannel()
}

权限(Permissions

声明这个方法,它将帮助我们确定所需的权限是否已经被用户授予。

private fun checkSelfPermission(permission: String, requestCode: Int): Boolean {
    if (ContextCompat.checkSelfPermission(this, permission) != PackageManager.PERMISSION_GRANTED) {

        ActivityCompat.requestPermissions(this, ALL_REQUESTED_PERMISSIONS, requestCode)
        return false
    }
    return true
}

onCreate 方法中检查所有权限是否被授予,然后调用initAgoraEngineAndJoinChannel 函数。

if (checkSelfPermission(ALL_REQUESTED_PERMISSIONS[0], PERMISSION_REQUEST_ID) &&
    checkSelfPermission(ALL_REQUESTED_PERMISSIONS[1], PERMISSION_REQUEST_ID
    ) && checkSelfPermission(ALL_REQUESTED_PERMISSIONS[2], PERMISSION_REQUEST_ID)) {
    initAgoraEngineAndJoinChannel()
}

另外,别忘了覆盖检查权限请求结果的onRequestPermissionsResult

override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<out String>, grantResults: IntArray) {
    super.onRequestPermissionsResult(requestCode, permissions, grantResults)

    if (requestCode == PERMISSION_REQUEST_ID) {
        if (
            grantResults[0] != PackageManager.PERMISSION_GRANTED ||
            grantResults[1] != PackageManager.PERMISSION_GRANTED ||
            grantResults[2] != PackageManager.PERMISSION_GRANTED
        ) {

            Toast.makeText(applicationContext, "Permissions needed", Toast.LENGTH_LONG).show()
            finish()
            return
        }
        // Here we continue only if all permissions are granted.
        initAgoraEngineAndJoinChannel()
    }
}

删除远程视图和本地视频

在这一步,我们将删除当前用户正在观看的远程视频和本地视频。

private fun removeRemoteVideo() {
    if (remoteView != null) {
        binding.remoteVideoView.removeView(remoteView)
    }
    remoteView = null
}

private fun removeLocalVideo() {
    if (localView != null) {
        binding.localVideoView.removeView(localView)
    }
    localView = null
}

当一个远程用户离开频道时,我们需要通过调用removeRemoteVideo 方法来移除远程视图。

private fun onRemoteUserLeft() {
    removeRemoteVideo()
}

处理RtcEngine事件

接下来,我们需要处理RtcEngine 的一些事件,比如当有人成功加入一个频道时,当第一个远程视频被解码时,当用户离线时。

创建一个RtcEventHandler 对象并实现以下必要的方法。

private val mRtcEventHandler = object : IRtcEngineEventHandler() {
    override fun onJoinChannelSuccess(channel: String?, uid: Int, elapsed: Int) {
        runOnUiThread {
            Toast.makeText(applicationContext, "Joined Channel Successfully", Toast.LENGTH_SHORT).show()
        }
    }

    override fun onFirstRemoteVideoDecoded(uid: Int, width: Int, height: Int, elapsed: Int) {
        runOnUiThread {
            setupRemoteVideo(uid)
        }
    }

    override fun onUserOffline(uid: Int, reason: Int) {
        runOnUiThread {
            onRemoteUserLeft()
        }
    }
}

开始和结束通话

为了开始通话,我们需要设置一个本地视频视图并加入一个频道。

private fun startCall() {
    setupLocalVideo()
    joinChannel()
}

要结束通话,我们需要删除本地和远程视频并离开频道。

private fun endCall() {
    removeLocalVideo()
    removeRemoteVideo()
    leaveChannel()
}

onCreate 方法里面,我们需要实现点击,如当点击以下Views

  • 呼叫按钮
  • 静音按钮
  • 切换摄像机按钮

添加以下实现。

binding.buttonCall.setOnClickListener {
    if (mEndCall) {
        startCall()
        mEndCall = false
        binding.buttonCall.setImageResource(R.drawable.btn_endcall)
        binding.buttonMute.visibility = VISIBLE
        binding.buttonSwitchCamera.visibility = VISIBLE
    } else {
        endCall()
        mEndCall = true
        binding.buttonCall.setImageResource(R.drawable.btn_startcall)
        binding.buttonMute.visibility = INVISIBLE
        binding.buttonSwitchCamera.visibility = INVISIBLE
    }
}

binding.buttonSwitchCamera.setOnClickListener {
    rtcEngine.switchCamera()
}

binding.buttonMute.setOnClickListener {
    mMuted = !mMuted
    rtcEngine.muteLocalAudioStream(mMuted)

    val res: Int = if (mMuted) {
        R.drawable.btn_mute
    } else {
        R.drawable.btn_unmute
    }

    binding.buttonMute.setImageResource(res)
}

销毁一切

我们还需要在应用程序关闭和不再使用时释放资源。覆盖onDestroy 和以下代码。

override fun onDestroy() {
    super.onDestroy()
    if (!mEndCall) {
        leaveChannel()
    }
    RtcEngine.destroy()
}

应用程序演示

在两个不同的设备上安装和运行该应用程序,并确保它们连接到互联网。你应该期望它能像下面的截图所示那样工作。

Screen 1

Screen 2

Screen 3

结论

在本教程中,我们已经了解了什么是Agora视频SDK,如何从Agora控制台获得访问令牌,以及如何用Agora SDK创建一个视频通话应用程序。

请继续应用这些技能来创建更高级的应用程序。