如何基于 Agora Android SDK 在应用中实现视频通话?
在很多产品,实时视频通话已经不是新鲜的功能了,例如视频会议、社交应用、在线教育,甚至也可能出现在一些元宇宙的场景中。
本文将教你如何通过声网Agora 视频 SDK 在 Android 端实现一个视频通话应用。声网 SDK 每个月会提供 10000 分钟的免费使用额度,可实现各类实时音视频场景。话不多说,我们开始动手实操。
通过开源 Demo,体验视频通话
可能有些人,还不了解我们要实现的功能最后是怎样的。所以我们在 GitHub 上提供一个开源的基础视频通话示例项目,在开始开发之前你可以通过该示例项目体验音视频通话效果。
视频通话的技术原理
我们在这里要实现的是一对一的视频通话。你可以理解为是两个用户通过加入同一个频道,实现的音视频的互通。而这个频道的数据,会通过声网的 Agora SD-RTN 实时网络来进行低延时传输的。
那么 App 集成 Agora SDK 后,视频通话的基本工作流程如下图所示:
如图所示,实现视频通话的步骤如下:
-
获取 Token:当 app 客户端加入频道时,你需要使用 Token 验证用户身份。在测试或生产环境中,从 app 服务器中获取 Token。
-
加入频道:调用
joinChannel
创建并加入频道。使用同一频道名称的 app 客户端默认加入同一频道。频道可理解为专用于传输实时音视频数据的通道。 -
在频道内发布和订阅音视频流:加入频道后,app 客户端均可以在频道内发布和订阅音视频。
App 客户端加入频道需要以下信息:
-
用户 ID:用户的唯一标识。你需要自行设置用户 ID,并确保它在频道内是唯一的。
-
Token:在测试或生产环境中,app 客户端从你的服务器中获取 Token。在本文介绍的流程中,你可以从 Agora 控制台获取临时 Token。临时 Token 的有效期为 24 小时。
-
频道名称:用于标识视频通话频道的字符串。
开发环境
声网Agora SDK 的兼容性良好,对硬件设备和软件系统的要求不高,开发环境和测试环境满足以下条件即可:
-
Android SDK API Level >= 16
-
Android Studio 2.0 或以上版本
-
支持语音和视频功能的真机
-
App 要求 Android 4.1 或以上设备
以下是本文的开发环境和测试环境:
开发环境
-
Windows 10 家庭中文版
-
Java Version SE 8
-
Android Studio 3.2 Canary 4
测试环境
-
Samsung Nexus (Android 4.4.2 API 19)
-
Mi Note 3 (Android 7.1.1 API 25)
如果你此前还未接触过声网 Agora SDK,那么你还需要做以下准备工作:
-
注册一个声网账号,进入后台创建 AppID、获取 Token,详细方法可参考这篇教程;
-
下载声网官方最新的 视频 SDK
项目设置
1.实现视频通话之前,参考如下步骤设置你的项目:
如需创建新项目,在 Android Studio里,依次选择 Phone and Tablet > Empty Activity,创建 Android 项目。
创建项目后,Android Studio会自动开始同步 gradle。请确保同步成功再进行下一步操作。
2.集成SDK, 本文推荐使用gradle方式集成Agora SDK:
a. 在 /Gradle Scripts/build.gradle(Project: )
文件中添加如下代码,以添加 mavenCentral 依赖:
buildscript {
repositories {
...
mavenCentral()
}
...
}
allprojects {
repositories {
...
mavenCentral()
}
}
b. 在 /Gradle Scripts/build.gradle(Module: .app)
文件中添加如下代码,将 Agora 视频 SDK 集成到你的 Android 项目中:
... dependencies { ... // x.y.z,请填写具体的 SDK 版本号,如:3.5.0。 // 通过发版说明获取最新版本号。 implementation 'io.agora.rtc:full-sdk:x.y.z' }
3.权限设置, 在 /app/Manifests/AndroidManifest.xml
文件中的 `` 后面添加如下网络和设备权限:
<uses-permission android:name="android.permission.INTERNET" /> <uses-permission android:name="android.permission.CAMERA" /> <uses-permission android:name="android.permission.RECORD_AUDIO" /> <uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" /> <uses-permission android:name="android.permission.ACCESS_WIFI_STATE" /> <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" /> <uses-permission android:name="android.permission.BLUETOOTH" />
客户端实现
本节介绍如何使用 Agora 视频 SDK 在你的 app 里实现视频通话的几个小贴士:
1.获取必要权限
启动应用程序时,检查是否已在 app 中授予了实现视频通话所需的权限。
在onCreate
函数中调用如下代码:
private static final int PERMISSION_REQ_ID = 22; private static final String[] REQUESTED_PERMISSIONS = { Manifest.permission.RECORD_AUDIO, Manifest.permission.CAMERA }; private boolean checkSelfPermission(String permission, int requestCode) { if (ContextCompat.checkSelfPermission(this, permission) != PackageManager.PERMISSION_GRANTED) { ActivityCompat.requestPermissions(this, REQUESTED_PERMISSIONS, requestCode); return false; } return true; }
2.实现视频通话逻辑
下图展示视频通话的 API 调用时序:
a. 初始化引擎
RtcEngine 类包含应用程序调用的主要方法,调用 RtcEngine 的接口最好在同一个线程进行,不建议在不同的线程同时调用。
目前 Agora Native SDK 只支持一个 RtcEngine 实例,每个应用程序仅创建一个 RtcEngine 对象 。 RtcEngine 类的所有接口函数,如无特殊说明,都是异步调用,对接口的调用建议在同一个线程进行。所有返回值为 int 型的 API,如无特殊说明,返回值 0 为调用成功,返回值小于 0 为调用失败。
IRtcEngineEventHandler接口类用于SDK向应用程序发送回调事件通知,应用程序通过继承该接口类的方法获取 SDK 的事件通知。
接口类的所有方法都有缺省(空)实现,应用程序可以根据需要只继承关心的事件。在回调方法中,应用程序不应该做耗时或者调用可能会引起阻塞的 API(如 SendMessage),否则可能影响 SDK 的运行。
engine = RtcEngine.create(context.getApplicationContext(), getString(R.string.agora_app_id), iRtcEngineEventHandler); ... private final IRtcEngineEventHandler iRtcEngineEventHandler = new IRtcEngineEventHandler() { /**Reports a warning during SDK runtime. * Warning code: https://docs.agora.io/en/Voice/API%20Reference/java/classio_1_1agora_1_1rtc_1_1_i_rtc_engine_event_handler_1_1_warn_code.html*/ @Override public void onWarning(int warn) { Log.w(TAG, String.format("onWarning code %d message %s", warn, RtcEngine.getErrorDescription(warn))); } /**Reports an error during SDK runtime. * Error code: https://docs.agora.io/en/Voice/API%20Reference/java/classio_1_1agora_1_1rtc_1_1_i_rtc_engine_event_handler_1_1_error_code.html*/ @Override public void onError(int err) { Log.e(TAG, String.format("onError code %d message %s", err, RtcEngine.getErrorDescription(err))); showAlert(String.format("onError code %d message %s", err, RtcEngine.getErrorDescription(err))); } /**Occurs when a user leaves the channel. * @param stats With this callback, the application retrieves the channel information, * such as the call duration and statistics.*/ @Override public void onLeaveChannel(RtcStats stats) { super.onLeaveChannel(stats); Log.i(TAG, String.format("local user %d leaveChannel!", myUid)); showLongToast(String.format("local user %d leaveChannel!", myUid)); } /**Occurs when the local user joins a specified channel. * The channel name assignment is based on channelName specified in the joinChannel method. * If the uid is not specified when joinChannel is called, the server automatically assigns a uid. * @param channel Channel name * @param uid User ID * @param elapsed Time elapsed (ms) from the user calling joinChannel until this callback is triggered*/ @Override public void onJoinChannelSuccess(String channel, int uid, int elapsed) { Log.i(TAG, String.format("onJoinChannelSuccess channel %s uid %d", channel, uid)); showLongToast(String.format("onJoinChannelSuccess channel %s uid %d", channel, uid)); myUid = uid; joined = true; handler.post(new Runnable() { @Override public void run() { join.setEnabled(true); join.setText(getString(R.string.leave)); } }); } @Override public void onRemoteAudioStats(io.agora.rtc.IRtcEngineEventHandler.RemoteAudioStats remoteAudioStats) { statisticsInfo.setRemoteAudioStats(remoteAudioStats); updateRemoteStats(); } @Override public void onLocalAudioStats(io.agora.rtc.IRtcEngineEventHandler.LocalAudioStats localAudioStats) { statisticsInfo.setLocalAudioStats(localAudioStats); updateLocalStats(); } @Override public void onRemoteVideoStats(io.agora.rtc.IRtcEngineEventHandler.RemoteVideoStats remoteVideoStats) { statisticsInfo.setRemoteVideoStats(remoteVideoStats); updateRemoteStats(); } @Override public void onLocalVideoStats(io.agora.rtc.IRtcEngineEventHandler.LocalVideoStats localVideoStats) { statisticsInfo.setLocalVideoStats(localVideoStats); updateLocalStats(); } @Override public void onRtcStats(io.agora.rtc.IRtcEngineEventHandler.RtcStats rtcStats) { statisticsInfo.setRtcStats(rtcStats); } };
b. 设置本地视频参数
enableVideo()方法用于打开视频模式。可以在加入频道前或者通话中调用,在加入频道前调用,则自动开启视频模式,在通话中调用则由音频模式切换为视频模式。调用 disableVideo() 方法可关闭视频模式。
setupLocalVideo( VideoCanvas local )方法用于设置本地视频显示信息。应用程序通过调用此接口绑定本地视频流的显示视窗(view),并设置视频显示模式。 在应用程序开发中,通常在初始化后调用该方法进行本地视频设置,然后再加入频道。退出频道后,绑定仍然有效,如果需要解除绑定,可以调用 setupLocalVideo(null) 。
// Create render view by RtcEngine SurfaceView surfaceView = RtcEngine.CreateRendererView(context); if(fl_local.getChildCount() > 0) { fl_local.removeAllViews(); } // Add to the local container fl_local.addView(surfaceView, new FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)); // Setup local video to render your local camera preview engine.setupLocalVideo(new VideoCanvas(surfaceView, RENDER_MODE_HIDDEN, 0)); // Enable video module engine.enableVideo();
c . 加入一个频道
joinChannel()方法让用户加入通话频道,在同一个频道内的用户可以互相通话,多个用户加入同一个频道,可以群聊。 使用不同 App ID 的应用程序是不能互通的。如果已在通话中,用户必须调用 leaveChannel() 退出当前通话,才能进入下一个频道。
ChannelMediaOptions option = new ChannelMediaOptions(); option.autoSubscribeAudio = true; option.autoSubscribeVideo = true; int res = engine.joinChannel(accessToken, channelId, "Extra Optional Data", 0, option); if (res != 0) { // Usually happens with invalid parameters // Error code description can be found at: // en: https://docs.agora.io/en/Voice/API%20Reference/java/classio_1_1agora_1_1rtc_1_1_i_rtc_engine_event_handler_1_1_error_code.html // cn: https://docs.agora.io/cn/Voice/API%20Reference/java/classio_1_1agora_1_1rtc_1_1_i_rtc_engine_event_handler_1_1_error_code.html showAlert(RtcEngine.getErrorDescription(Math.abs(res))); return; }
d. 离开当前频道
leaveChannel()方法用于离开频道,即挂断或退出通话。
当调用 joinChannel() API 方法后,必须调用 leaveChannel() 结束通话,否则无法开始下一次通话。 不管当前是否在通话中,都可以调用 leaveChannel(),没有副作用。该方法会把会话相关的所有资源释放掉。该方法是异步操作,调用返回时并没有真正退出频道。在真正退出频道后,SDK 会触发 onLeaveChannel 回调。
e. 管理摄像头
switchCamera()方法用于在前置/后置摄像头间切换。除此以外Agora还提供了一下管理摄像头的方法:例如setCameraTorchOn(boolean isOn)设置是否打开闪光灯、setCameraAutoFocusFaceModeEnabled(boolean enabled)设置是否开启人脸对焦功能等等。
f. 当远端用户加入频道时,更新远端用户界面。
private final IRtcEngineEventHandler iRtcEngineEventHandler = new IRtcEngineEventHandler() { ... /**Occurs when a remote user (Communication)/host (Live Broadcast) joins the channel. * @param uid ID of the user whose audio state changes. * @param elapsed Time delay (ms) from the local user calling joinChannel/setClientRole * until this callback is triggered.*/ @Override public void onUserJoined(int uid, int elapsed) { super.onUserJoined(uid, elapsed); Log.i(TAG, "onUserJoined->" + uid); showLongToast(String.format("user %d joined!", uid)); /**Check if the context is correct*/ Context context = getContext(); if (context == null) { return; } if(remoteViews.containsKey(uid)){ return; } else{ handler.post(() -> { /**Display remote video stream*/ SurfaceView surfaceView = null; // Create render view by RtcEngine surfaceView = RtcEngine.CreateRendererView(context); surfaceView.setZOrderMediaOverlay(true); ViewGroup view = getAvailableView(); remoteViews.put(uid, view); // Add to the remote container view.addView(surfaceView, new FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)); // Setup remote video to render engine.setupRemoteVideo(new VideoCanvas(surfaceView, RENDER_MODE_HIDDEN, uid)); }); } } /**Occurs when a remote user (Communication)/host (Live Broadcast) leaves the channel. * @param uid ID of the user whose audio state changes. * @param reason Reason why the user goes offline: * USER_OFFLINE_QUIT(0): The user left the current channel. * USER_OFFLINE_DROPPED(1): The SDK timed out and the user dropped offline because no data * packet was received within a certain period of time. If a user quits the * call and the message is not passed to the SDK (due to an unreliable channel), * the SDK assumes the user dropped offline. * USER_OFFLINE_BECOME_AUDIENCE(2): (Live broadcast only.) The client role switched from * the host to the audience.*/ @Override public void onUserOffline(int uid, int reason) { Log.i(TAG, String.format("user %d offline! reason:%d", uid, reason)); showLongToast(String.format("user %d offline! reason:%d", uid, reason)); handler.post(new Runnable() { @Override public void run() { /**Clear render view Note: The video will stay at its last frame, to completely remove it you will need to remove the SurfaceView from its parent*/ engine.setupRemoteVideo(new VideoCanvas(null, RENDER_MODE_HIDDEN, uid)); remoteViews.get(uid).removeAllViews(); remoteViews.remove(uid); } }); } ... };
完成,运行
拿两部手机安装编译好的App,如果能看见两个自己,说明你成功了。如果你在开发过程中遇到问题,可以访问论坛提问与声网工程师交流
https://rtcdeveloper.agora.io/
也可以访问后台获取更进一步的技术支持
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· 阿里巴巴 QwQ-32B真的超越了 DeepSeek R-1吗?
· 【译】Visual Studio 中新的强大生产力特性
· 10年+ .NET Coder 心语 ── 封装的思维:从隐藏、稳定开始理解其本质意义
· 【设计模式】告别冗长if-else语句:使用策略模式优化代码结构