实现音视频互动
本文介绍如何集成声网实时互动 SDK,通过少量代码从 0 开始实现一个简单的实时互动 App,适用于互动直播和视频通话场景。
首先,你需要了解以下有关音视频实时互动的基础概念:
- 声网实时互动 SDK:由声网开发的、帮助开发者在 App 中实现实时音视频互动的 SDK。
- 频道:用于传输数据的通道,在同一个频道内的用户可以进行实时互动。
- 主播:可以在频道内发布音视频,同时也可以订阅其他主播发布的音视频。
- 观众:可以在频道内订阅音视频,不具备发布音视频权限。
更多概念详见关键概念。
下图展示在 App 中实现音视频互动的基本工作流程:
- 所有用户调用
joinChannel
方法加入频道,并根据需要设置用户角色:- 互动直播:如果用户需要在频道中发流,则设为主播;如果用户只需要收流,则设为观众。
- 视频通话:将所有的用户角色都为主播。
- 加入频道后,不同角色的用户具备不同的行为:
- 所有用户默认都可以接收频道中的音视频流。
- 主播可以在频道内发布音视频流。
- 观众如果需要发流,可在频道内调用
setClientRole
方法修改用户角色,使其具备发流权限。
前提条件
在实现功能以前,请按照以下要求准备开发环境:
- Android Studio 4.1 以上版本。
- Android API 级别 16 或以上。
- 两台运行 Android 4.1 或以上版本的移动设备。
- 可以访问互联网的计算机。如果你的网络环境部署了防火墙,参考应对防火墙限制以正常使用声网服务。
- 一个有效的声网账号以及声网项目。请参考开通服务从声网控制台获得 和临时 。
临时 Token 的有效期是 24 小时。Token 过期会导致加入频道失败。
创建项目
本小节介绍如何创建项目并为项目添加体验实时互动所需的权限。
-
(可选) 创建新项目。详见 Create a project。
-
打开 Android Studio,选择 New Project。
-
选择 Phone and Tablet > Empty Views Activity,点击 Next。
-
设置项目名称和存储路径,选择语言为 Java,点击 Finish 创建 Android 项目。
注意创建项目后,Android Studio 会自动开始同步 gradle,稍等片刻至同步成功后再进行下一步操作。
-
-
添加网络及设备权限。
打开
/app/src/main/AndroidManifest.xml
文件,在</application>
后面添加如下权限: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"/>
<!-- 对于 Android 12.0 及以上且集成 v4.1.0 以下 SDK 的设备,还需要添加以下权限 -->
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT"/>
<!-- 对于 Android 12.0 及以上设备,还需要添加以下权限 -->
<uses-permission android:name="android.permission.READ_PHONE_STATE"/>
<uses-permission android:name="android.permission.BLUETOOTH_SCAN"/> -
防止代码混淆。
打开
/app/proguard-rules.pro
文件,添加如下行以防止声网 SDK 的代码被混淆:Java-keep class io.agora.**{*;}
-dontwarn io.agora.**
集成 SDK
你可以选用以下任一方式集成声网实时互动 SDK。
- 通过 Maven Central 集成
- 手动集成
-
打开项目根目录下的
settings.gradle
文件,添加 Maven Central 依赖 (如果已有可忽略):Groovyrepositories {
...
mavenCentral()
...
}注意如果你的 Android 项目设置了 dependencyResolutionManagement,添加 Maven Central 依赖的方式可能存在差异。
-
打开
/app/build.gradle
文件,在dependencies
中添加声网 RTC SDK 的依赖。你可以从发版说明中查询 SDK 的最新版本,并将x.y.z
替换为具体的版本号。Groovy...
dependencies {
...
// x.y.z 替换为具体的 SDK 版本号,如:4.0.0 或 4.1.0-1
// 集成 Full SDK
implementation 'io.agora.rtc:full-sdk:x.y.z'
// 集成 Lite SDK
implementation 'io.agora.rtc:lite-sdk:4.4.1'
}
-
在下载页面下载最新版本的 Android 实时互动 SDK,并解压。
-
打开解压文件,将以下文件或子文件夹复制到你的项目路径中。
文件或子文件夹 项目路径 agora-rtc-sdk.jar
文件/app/libs/
arm64-v8a
文件夹/app/src/main/jniLibs/
armeabi-v7a
文件夹/app/src/main/jniLibs/
x86
文件夹/app/src/main/jniLibs/
x86_64
文件夹/app/src/main/jniLibs/
high_level_api
中的include
文件夹/app/src/main/jniLibs/
-
在 Android Studio 的左侧导航栏上选择
Project Files/app/libs/agora-rtc-sdk.jar
文件,右键单击,在下拉菜单中选择add as a library
。
创建用户界面
根据实时音视频互动的场景需要,为你的项目创建两个视图框,分别用于展示本地视频和远端视频。如下图所示:
复制以下代码到 /app/src/main/res/layout/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">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Start Video Call!"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<FrameLayout
android:id="@+id/local_video_view_container"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@android:color/white" />
<FrameLayout
android:id="@+id/remote_video_view_container"
android:layout_width="160dp"
android:layout_height="160dp"
android:layout_alignParentEnd="true"
android:layout_alignParentRight="true"
android:layout_alignParentTop="true"
android:layout_marginEnd="16dp"
android:layout_marginRight="16dp"
android:layout_marginTop="16dp"
android:background="@android:color/darker_gray"
tools:ignore="MissingConstraints" />
</androidx.constraintlayout.widget.ConstraintLayout>
实现步骤
本小节介绍如何实现一个实时音视频互动 App。你可以先复制完整的示例代码到你的项目中,快速体验实时音视频互动的基础功能,再按照实现步骤了解核心 API 调用。
下图展示了使用声网 RTC SDK 实现音视频互动的基本流程:
下面列出了一段实现实时互动基本流程的完整代码以供参考。复制以下代码到 /app/src/main/java/com/example/<projectname>/MainActivity.java
文件中替换 package com.example.<projectname>
后的全部内容,即可快速体验实时互动基础功能。
在 appId
、token
和 channelName
字段中传入你在控制台获取到的 App ID、临时 Token,以及生成临时 Token 时填入的频道名。
import android.Manifest;
import android.content.pm.PackageManager;
import android.os.Bundle;
import android.view.SurfaceView;
import android.widget.FrameLayout;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.appcompat.app.AppCompatActivity;
import androidx.core.app.ActivityCompat;
import androidx.core.content.ContextCompat;
import io.agora.rtc2.ChannelMediaOptions;
import io.agora.rtc2.Constants;
import io.agora.rtc2.IRtcEngineEventHandler;
import io.agora.rtc2.RtcEngine;
import io.agora.rtc2.RtcEngineConfig;
import io.agora.rtc2.video.VideoCanvas;
public class MainActivity extends AppCompatActivity {
// 填写声网控制台中获取的 App ID
private String appId = "<#Your App ID#>";
// 填写频道名
private String channelName = "<#Your channel name#>";
// 填写声网控制台中生成的临时 Token
private String token = "<#Your Token#>";
private RtcEngine mRtcEngine;
private final IRtcEngineEventHandler mRtcEventHandler = new IRtcEngineEventHandler() {
// 成功加入频道回调
@Override
public void onJoinChannelSuccess(String channel, int uid, int elapsed) {
super.onJoinChannelSuccess(channel, uid, elapsed);
runOnUiThread(() -> {
Toast.makeText(MainActivity.this, "Join channel success", Toast.LENGTH_SHORT).show();
});
}
// 远端用户或主播加入当前频道回调
@Override
public void onUserJoined(int uid, int elapsed) {
runOnUiThread(() -> {
// 当远端用户加入频道后,显示指定 uid 的远端视频流
setupRemoteVideo(uid);
});
}
// 远端用户或主播离开当前频道回调
@Override
public void onUserOffline(int uid, int reason) {
super.onUserOffline(uid, reason);
runOnUiThread(() -> {
Toast.makeText(MainActivity.this, "User offline: " + uid, Toast.LENGTH_SHORT).show();
});
}
};
private void initializeAndJoinChannel() {
try {
// 创建 RtcEngineConfig 对象,并进行配置
RtcEngineConfig config = new RtcEngineConfig();
config.mContext = getBaseContext();
config.mAppId = appId;
config.mEventHandler = mRtcEventHandler;
// 创建并初始化 RtcEngine
mRtcEngine = RtcEngine.create(config);
} catch (Exception e) {
throw new RuntimeException("Check the error.");
}
// 启用视频模块
mRtcEngine.enableVideo();
// 创建一个 SurfaceView 对象,并将其作为 FrameLayout 的子对象
FrameLayout container = findViewById(R.id.local_video_view_container);
SurfaceView surfaceView = new SurfaceView (getBaseContext());
container.addView(surfaceView);
// 将 SurfaceView 对象传入声网实时互动 SDK,设置本地视图
mRtcEngine.setupLocalVideo(new VideoCanvas(surfaceView, VideoCanvas.RENDER_MODE_FIT, 0));
// 开启本地预览
mRtcEngine.startPreview();
// 创建 ChannelMediaOptions 对象,并进行配置
ChannelMediaOptions options = new ChannelMediaOptions();
// 设置用户角色为 BROADCASTER (主播) 或 AUDIENCE (观众)
options.clientRoleType = Constants.CLIENT_ROLE_BROADCASTER;
// 设置频道场景为 BROADCASTING (直播场景)
options.channelProfile = Constants.CHANNEL_PROFILE_LIVE_BROADCASTING;
// 发布麦克风采集的音频
options.publishMicrophoneTrack = true;
// 发布摄像头采集的视频
options.publishCameraTrack = true;
// 自动订阅所有音频流
options.autoSubscribeAudio = true;
// 自动订阅所有视频流
options.autoSubscribeVideo = true;
// 使用临时 Token 和频道名加入频道,uid 为 0 表示引擎内部随机生成用户名
// 成功后会触发 onJoinChannelSuccess 回调
mRtcEngine.joinChannel(token, channelName, 0, options);
}
private void setupRemoteVideo(int uid) {
FrameLayout container = findViewById(R.id.remote_video_view_container);
SurfaceView surfaceView = new SurfaceView (getBaseContext());
surfaceView.setZOrderMediaOverlay(true);
container.addView(surfaceView);
// 将 SurfaceView 对象传入声网实时互动 SDK,设置远端视图
mRtcEngine.setupRemoteVideo(new VideoCanvas(surfaceView, VideoCanvas.RENDER_MODE_FIT, uid));
}
private static final int PERMISSION_REQ_ID = 22;
// 获取体验实时音视频互动所需的录音、摄像头等权限
private String[] getRequiredPermissions(){
// 判断 targetSDKVersion 31 及以上时所需的权限
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.S) {
return new String[]{
Manifest.permission.RECORD_AUDIO, // 录音权限
Manifest.permission.CAMERA, // 摄像头权限
Manifest.permission.READ_PHONE_STATE, // 读取电话状态权限
Manifest.permission.BLUETOOTH_CONNECT // 蓝牙连接权限
};
} else {
return new String[]{
Manifest.permission.RECORD_AUDIO,
Manifest.permission.CAMERA
};
}
}
private boolean checkPermissions() {
for (String permission : getRequiredPermissions()) {
int permissionCheck = ContextCompat.checkSelfPermission(this, permission);
if (permissionCheck != PackageManager.PERMISSION_GRANTED) {
return false;
}
}
return true;
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
// 如果已经授权,则初始化 RtcEngine 并加入频道
if (checkPermissions()) {
initializeAndJoinChannel();
} else {
ActivityCompat.requestPermissions(this, getRequiredPermissions(), PERMISSION_REQ_ID);
}
}
// 系统权限申请回调
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
if (checkPermissions()) {
initializeAndJoinChannel();
}
}
@Override
protected void onDestroy() {
super.onDestroy();
if (mRtcEngine != null) {
// 停止本地视频预览
mRtcEngine.stopPreview();
// 离开频道
mRtcEngine.leaveChannel();
mRtcEngine = null;
// 销毁引擎
RtcEngine.destroy();
}
}
}
处理权限请求
本小节介绍如何导入 Android 相关的类并获取 Android 设备的摄像头、录音等权限。
-
导入 Android 相关的类
Javaimport android.Manifest;
import android.content.pm.PackageManager;
import android.os.Bundle;
import android.view.SurfaceView;
import android.widget.FrameLayout;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.appcompat.app.AppCompatActivity;
import androidx.core.app.ActivityCompat;
import androidx.core.content.ContextCompat; -
获取 Android 权限
启动应用程序时,检查是否已在 App 中授予了实现实时互动所需的权限。
Javaprivate static final int PERMISSION_REQ_ID = 22;
// 获取体验实时音视频互动所需的录音、摄像头等权限
private String[] getRequiredPermissions(){
// 判断 targetSDKVersion 31 及以上时所需的权限
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.S) {
return new String[]{
Manifest.permission.RECORD_AUDIO, // 录音权限
Manifest.permission.CAMERA, // 摄像头权限
Manifest.permission.READ_PHONE_STATE, // 读取电话状态权限
Manifest.permission.BLUETOOTH_CONNECT // 蓝牙连接权限
};
} else {
return new String[]{
Manifest.permission.RECORD_AUDIO,
Manifest.permission.CAMERA
};
}
}
private boolean checkPermissions() {
for (String permission : getRequiredPermissions()) {
int permissionCheck = ContextCompat.checkSelfPermission(this, permission);
if (permissionCheck != PackageManager.PERMISSION_GRANTED) {
return false;
}
}
return true;
}
导入声网相关的类
导入声网 RTC SDK 相关的类和接口:
import io.agora.rtc2.ChannelMediaOptions;
import io.agora.rtc2.Constants;
import io.agora.rtc2.IRtcEngineEventHandler;
import io.agora.rtc2.RtcEngine;
import io.agora.rtc2.RtcEngineConfig;
import io.agora.rtc2.video.VideoCanvas;
定义 App ID 和 Token
传入从声网控制台获取的 App ID、临时 Token,以及生成临时 Token 时填入的频道名,用于后续初始化引擎和加入频道。
// 填写声网控制台中获取的 App ID
private String appId = "<#Your App ID#>";
// 填写频道名
private String channelName = "<#Your channel name#>";
// 填写声网控制台中生成的临时 Token
private String token = "<#Your Token#>";
初始化引擎
调用 create
[2/2] 方法初始化 RtcEngine
。
在初始化 SDK 前,需确保终端用户已经充分了解并同意相关的隐私政策。
private RtcEngine mRtcEngine;
private final IRtcEngineEventHandler mRtcEventHandler = new IRtcEngineEventHandler() {
...
};
// 创建 RtcEngineConfig 对象,并进行配置
RtcEngineConfig config = new RtcEngineConfig();
config.mContext = getBaseContext();
config.mAppId = appId;
config.mEventHandler = mRtcEventHandler;
// 创建并初始化 RtcEngine
mRtcEngine = RtcEngine.create(config);
启用视频模块
按照以下步骤启用视频模块:
- 调用
enableVideo
方法,启用视频模块。 - 调用
setupLocalVideo
方法初始化本地视图,同时设置本地的视频显示属性。 - 调用
startPreview
方法,开启本地视频预览。
// 启用视频模块
mRtcEngine.enableVideo();
// 创建一个 SurfaceView 对象,并将其作为 FrameLayout 的子对象
FrameLayout container = findViewById(R.id.local_video_view_container);
SurfaceView surfaceView = new SurfaceView (getBaseContext());
container.addView(surfaceView);
// 将 SurfaceView 对象传入声网实时互动 SDK,设置本地视图
mRtcEngine.setupLocalVideo(new VideoCanvas(surfaceView, VideoCanvas.RENDER_MODE_FIT, 0));
// 开启本地预览
mRtcEngine.startPreview();
加入频道并发布音视频流
调用 joinChannel
[2/2] 加入频道。在 ChannelMediaOptions
中进行如下配置:
- 设置频道场景为
BROADCASTING
(直播场景) 并设置用户角色设置为BROADCASTER
(主播) 或AUDIENCE
(观众)。 - 将
publishMicrophoneTrack
和publishCameraTrack
设置为true
,发布麦克风采集的音频和摄像头采集的视频。 - 将
autoSubscribeAudio
和autoSubscribeVideo
设置为true
,自动订阅所有音视频流。
// 创建 ChannelMediaOptions 对象,并进行配置
ChannelMediaOptions options = new ChannelMediaOptions();
// 设置用户角色为 BROADCASTER (主播) 或 AUDIENCE (观众)
options.clientRoleType = Constants.CLIENT_ROLE_BROADCASTER;
// 设置频道场景为 BROADCASTING (直播场景)
options.channelProfile = Constants.CHANNEL_PROFILE_LIVE_BROADCASTING;
// 发布麦克风采集的音频
options.publishMicrophoneTrack = true;
// 发布摄像头采集的视频
options.publishCameraTrack = true;
// 自动订阅所有音频流
options.autoSubscribeAudio = true;
// 自动订阅所有视频流
options.autoSubscribeVideo = true;
// 使用临时 Token 和频道名加入频道,uid 为 0 表示引擎内部随机生成用户名
// 成功后会触发 onJoinChannelSuccess 回调
mRtcEngine.joinChannel(token, channelName, 0, options);
设置远端视图
调用 setupRemoteVideo
方法初始化远端用户视图,同时设置远端用户的视图在本地显示属性。你可以通过 onUserJoined
回调获取远端用户的 uid
。
private void setupRemoteVideo(int uid) {
FrameLayout container = findViewById(R.id.remote_video_view_container);
SurfaceView surfaceView = new SurfaceView (getBaseContext());
surfaceView.setZOrderMediaOverlay(true);
container.addView(surfaceView);
// 将 SurfaceView 对象传入声网实时互动 SDK,设置远端视图
mRtcEngine.setupRemoteVideo(new VideoCanvas(surfaceView, VideoCanvas.RENDER_MODE_FIT, uid));
}
实现常用回调
根据使用场景,定义必要的回调。以下示例代码展示如何实现 onJoinChannelSuccess
、 onUserJoined
和 onUserOffline
回调。
// 成功加入频道回调
@Override
public void onJoinChannelSuccess(String channel, int uid, int elapsed) {
super.onJoinChannelSuccess(channel, uid, elapsed);
runOnUiThread(() -> {
Toast.makeText(MainActivity.this, "Join channel success", Toast.LENGTH_SHORT).show();
});
}
// 远端用户或主播加入当前频道回调
@Override
public void onUserJoined(int uid, int elapsed) {
runOnUiThread(() -> {
// 当远端用户加入频道后,显示指定 uid 的远端视频流
setupRemoteVideo(uid);
});
}
// 远端用户或主播离开当前频道回调
@Override
public void onUserOffline(int uid, int reason) {
super.onUserOffline(uid, reason);
runOnUiThread(() -> {
Toast.makeText(MainActivity.this, "User offline: " + uid, Toast.LENGTH_SHORT).show();
});
}
开始音视频互动
在 onCreate
中调用一系列方法加载界面布局、检查 App 是否获取实时互动所需权限,并加入频道开始音视频互动。
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
// 如果已经授权,则初始化 RtcEngine 并加入频道
if (checkPermissions()) {
initializeAndJoinChannel();
} else {
ActivityCompat.requestPermissions(this, getRequiredPermissions(), PERMISSION_REQ_ID);
}
}
结束音视频互动
按照以下步骤结束音视频互动:
-
调用
stopPreview
停止视频预览。 -
调用
leaveChannel
离开当前频道,释放所有会话相关的资源。 -
调用
destroy
销毁引擎,并释放声网 SDK 中使用的所有资源。警告调用
destroy
后,你将无法再使用 SDK 的所有方法和回调。如需再次使用实时音视频互动功能,你必须重新创建一个新的引擎。详见初始化引擎。Java@Override
protected void onDestroy() {
super.onDestroy();
if (mRtcEngine != null) {
// 停止本地视频预览
mRtcEngine.stopPreview();
// 离开频道
mRtcEngine.leaveChannel();
mRtcEngine = null;
// 销毁引擎
RtcEngine.destroy();
}
}
调试 App
按照以下步骤测试直播 App:
-
开启 Android 设备的开发者选项,打开 USB 调试,通过 USB 连接线将 Android 设备接入电脑,并在 Android 设备选项中勾选你的 Android 设备。
-
在 Android Studio 中,点击 (Sync Project with Gradle Files) 进行 Gradle 同步。
-
待同步成功后,点击 (Run 'app') 开始编译。片刻后,App 便会安装到你的 Android 设备上。
-
启动 App,授予录音和摄像头权限,如果你将用户角色设置为主播,便会在本地视图中看到自己。
-
使用第二台 Android 设备,重复以上步骤,在该设备上安装 App、打开 App 加入频道,观察测试结果:
- 如果两台设备均作为主播加入频道,则可以看到对方并且听到对方的声音。
- 如果两台设备分别作为主播和观众加入,则主播可以在本地视频窗口看到自己;观众可以在远端视频窗口看到主播、并听到主播的声音。
后续步骤
在完成音视频互动后,你可以阅读以下文档进一步了解:
- 本文的示例使用了临时 Token 加入频道。在测试或生产环境中,为保证通信安全,声网推荐从服务器中获取 Token,详情请参考使用 Token 鉴权。
- 如果你想要实现极速直播场景,可以在实时音视频互动的基础上,通过修改观众端的延时级别为低延时 (
AUDIENCE_LATENCY_LEVEL_LOW_LATENCY
) 实现。详见实现极速直播。
参考信息
示例项目
声网提供了开源的实时音视频互动示例项目供你参考,你可以前往下载或查看其中的源代码。
常见问题
- 直播场景下,如何监听远端观众角色用户加入/离开频道的事件?
- 如何处理视频黑屏问题?
- 为什么我无法打开摄像头?
- 如何处理频道相关常见问题?
- 如何设置日志文件?
- 为什么部分 Android 版本应用锁屏或切后台后采集音视频无效?
- 为什么 SDK 中使用的是动态库而不是静态库?