用Agora SDK创建一个使用Kotlin的一对一视频通话Android应用
在开发一个包含视频通话功能的安卓应用时,你可能最终会有很多模板代码。
Agora SDK是一个平台,它允许开发者用相对较少的代码创建丰富的应用内体验,如嵌入式语音和视频聊天、实时录音、实时流媒体和实时消息。
前提条件
要继续学习本教程,你需要具备以下条件。
- 在你的机器上安装[Android Studio]。
- 对开发和运行Android应用程序有扎实的了解。
- [Kotlin]编程语言的基础知识。
- 一个Agora账户。
- 有Android
ViewBinding的经验。
目标
在本教程结束时,你将能够。
- 理解什么是Agora视频通话SDK。
- 创建并获得Agora SDK的
access key。 - 在一个一对一的视频通话应用程序中实施SDK。
什么是Agora视频通话SDK?
Agora视频通话SDK是一个平台,允许开发者创建丰富的应用内体验,如嵌入式语音和视频聊天、实时录音、实时流媒体和实时消息。
Agora的视频通话API增强了社交应用程序的新功能,如AR面部面具和分享屏幕时的声音效果、白板以及其他可能有利于商业和教育应用程序的功能。
在本教程中,我们将使用SDK在一个安卓应用中添加视频通话功能。
在Agora仪表板上创建一个项目
打开Agora开发者控制台,创建一个新项目,如下图所示。

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

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

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

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

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

设置该项目
在你的应用级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_ID 和TEMP_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()
}
应用程序演示
在两个不同的设备上安装和运行该应用程序,并确保它们连接到互联网。你应该期望它能像下面的截图所示那样工作。



结论
在本教程中,我们已经了解了什么是Agora视频SDK,如何从Agora控制台获得访问令牌,以及如何用Agora SDK创建一个视频通话应用程序。
请继续应用这些技能来创建更高级的应用程序。