Android 开发实践:实现17人视频l聊天应用

自从2016年,鼓吹“互联网寒冬”的论调甚嚣尘上,2017年亦有愈演愈烈之势。但连麦直播、在线抓娃娃、直播问答、远程狼人杀等类型的项目却异军突起,成了投资人的风口,创业者的蓝海和用户的必装App,而这些方向的项目都有一个共同的特点——都依赖视频通话和全互动直播技术。

目前有很多第三方平台提供实时音视频通讯服务,让Android、iOS开发者们不用去考虑网络延时、设备兼容等方面的问题。我们这次将试着通过声网的SDK实现一个多人视频通话应用。

环境

声网Agora.io SDK的兼容性良好,对硬件设备和软件系统的要求不高,开发环境和测试环境满足以下条件即可:

  • Android SDK API Level >= 16
  • Android Studio 2.0 或以上版本
  • 支持语音和视频功能的真机
  • App 要求 Android 4.1 或以上设备

以下是我试用声网Agora.io SDK的开发环境和测试环境:

  • 开发环境
  • 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)

集成

步骤一:首先点此下载完整的SDK和官方demo

步骤二:既然我们要把声网Agora.io集成到自己的项目里,所以不必运行sample,我们自己新建一个HelloAgora项目,注意一定要支持C++哦。

步骤三:把libs文件夹里的arm64-v8a、、armeabi-v7a以及x86文件夹复制粘贴到app module的libs里。如果有NDK开发的必要,则把libs->include文件夹里的两个.h头文件复制粘贴到合适位置。

步骤四:首先在app module的build.gradle文件的android代码块中添加如下代码:

sourceSets {
 main {
 jniLibs.srcDirs = ['../../../libs']
 }
}

然后在app module的build.gradle文件的android->defaultConfig代码块中添加如下代码:

ndk {
 abiFilters "armeabi-v7a", "x86" 
}

接下来在app module的build.gradle文件的dependencies代码块中添加如下代码:

compile 'io.agora.rtc:full-sdk:2.0.0'

如果用复制粘贴jar的方式,那么此处添加如下代码:

compile fileTree(dir: '../../../libs', include: ['*.jar'])

如果有自定义NDK的必要,可以继续在app module的build.gradle文件的android代码块中添加如下代码:

externalNativeBuild {
 ndkBuild {
 path 'src/main/cpp/Android.mk'
 }
}

然后在app module的build.gradle文件的android->defaultConfig代码块中添加如下代码:

externalNativeBuild {
 ndkBuild {
 arguments "NDK_APPLICATION_MK:=src/main/cpp/Application.mk"
 }
}

最后sync一下,声网Agora.io的SDK就集成到项目中来了。

权限

SDK集成完毕后,为了保证SDK能正常运行,我们需要在AndroidManisfest.xml 文件中声明以下权限:

<!--允许程序连接网络-->
<uses-permission android:name="android.permission.INTERNET" />
<!--允许程序录制音频-->
<uses-permission android:name="android.permission.RECORD_AUDIO" />
<!--允许程序使用照相设备-->
<uses-permission android:name="android.permission.CAMERA" />
<!--允许程序修改全局音频设置-->
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />
<!--允许程序获取网络状态-->
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<!--允许对存储空间进行读写-->
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<!--允许程序连接到已配对的蓝牙设备-->
<uses-permission android:name="android.permission.BLUETOOTH" />

这些权限都是Android开发过程中的常见权限,有经验的程序员都会感觉眼熟,WRITE_EXTERNAL_STORAGE等敏感权限适配Android 6.0以后版本的问题并非本文关注重点,在此不做赘述。

混淆代码

集成SDK并声明了权限后,就该考虑混淆的问题了,我们需要在project的proguard-rules.pro文件里添加以下代码:

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

经过以上过程后,我们已经完成了声网Agora.io SDK的快速集成,迈出了走向连麦直播、在线抓娃娃、直播问答、远程狼人杀等风口的第一步。在接下来的文章里,我将继续分享APP ID鉴权、Token鉴权、一对一视频聊天、创建群聊room、分屏、窗口切换和前后摄像头切换等内容。

鉴权

APP ID鉴权

所谓APP ID,就是在 Agora创建每个项目都有的一个唯一标识。App ID 可以明确你的项目及组织身份,并在 joinChannel 方法中作为参数,连接到 Agora 实时网络中,实现实时通信或直播功能。不同的App ID在Agora实时网络中的通话是完全隔离的;Agora 提供的频道信息、计费、管理服务也都是基于 App ID。

申请APP ID的操作很简便,只要在Agora官网https://dashboard.agora.io/projects右侧栏目的“项目”中点击“添加新项目”,只需输入项目名就可生成APP ID,全过程如下图所示:

Android 开发实践:实现17人视频l聊天应用

找到,把“<#YOUR APP ID#>”替换为图中的马赛克里的字符串。

<string name="agora_app_id"><#YOUR APP ID#></string>

以上就是APP ID鉴权的全过程。

尽管App ID鉴权在最大程度上方便了开发者使用 Agora 的服务。但App ID 鉴权的安全性不佳,一旦有别有用心的人非法获取了你的 App ID,他就可以在 Agora 提供的SDK中使用你的App ID。如果你的项目对安全性要求高,或者增加用户权限设置的话,建议采用Token鉴权。

Token鉴权

在通信和直播场景中存在着多个角色,而每种角色又对应着一些默认权限。比如在直播场景中,主播可以发布流、订阅流、邀请嘉宾;观众可以订阅流、申请连麦;管理员则可以踢人或禁言。

Token鉴权的步骤比APP ID鉴权稍微复杂一些,在上文项目列表中查看 App ID 的地方,启用该项目的 App Certificate:

首先,点击激活项目右上方的 编辑 按钮。

Android 开发实践:实现17人视频l聊天应用

将你的 App Certificate 保存在服务器端,且对任何客户端均不可见。当项目的 App Certificate 被启用后,你必须使用 Token。例如: 在启用 App Certificate 前,你可以使用 App ID 加入频道。但启用了 App Certificate 后,你就必须使用 Token 加入频道。后台如何用App Certificate生成Token本文不做赘述。

初始化Agora

RtcEngine 类包含应用程序调用的主要方法,调用 RtcEngine 的接口最好在同一个线程进行,不建议在不同的线程同时调用。

目前 Agora Native SDK 只支持一个 RtcEngine 实例,每个应用程序仅创建一个 RtcEngine 对象 。 RtcEngine 类的所有接口函数,如无特殊说明,都是异步调用,对接口的调用建议在同一个线程进行。所有返回值为 int 型的 API,如无特殊说明,返回值 0 为调用成功,返回值小于 0 为调用失败。

IRtcEngineEventHandler接口类用于SDK向应用程序发送回调事件通知,应用程序通过继承该接口类的方法获取 SDK 的事件通知。

接口类的所有方法都有缺省(空)实现,应用程序可以根据需要只继承关心的事件。在回调方法中,应用程序不应该做耗时或者调用可能会引起阻塞的 API(如 SendMessage),否则可能影响 SDK 的运行。

private RtcEngine mRtcEngine;
/**
 * Tutorial Step 1
 * 初始化Agora,创建 RtcEngine 对象
 */
private void initializeAgoraEngine() {
 try {
 mRtcEngine = RtcEngine.create(getBaseContext(), getString(R.string.agora_app_id), mRtcEventHandler);
 } catch (Exception e) {
 Log.e(LOG_TAG, Log.getStackTraceString(e));
 throw new RuntimeException("Agora初始化失败了,检查一下是哪儿出错了
" + Log.getStackTraceString(e));
 }
}
private final IRtcEngineEventHandler mRtcEventHandler = new IRtcEngineEventHandler() {
 @Override
 public void onFirstRemoteVideoDecoded(final int uid, int width, int height, int elapsed) {
 runOnUiThread(new Runnable() {
 @Override
 public void run() {
 //设置远端视频显示属性
 setupRemoteVideo(uid);
 }
 });
 }
 @Override
 public void onUserOffline(int uid, int reason) {
 runOnUiThread(new Runnable() {
 @Override
 public void run() {
 //其他用户离开当前频道回调
 onRemoteUserLeft();
 }
 });
 }
 @Override
 public void onUserMuteVideo(final int uid, final boolean muted) {
 runOnUiThread(new Runnable() {
 @Override
 public void run() {
 //其他用户已停发/已重发视频流回调
 onRemoteUserVideoMuted(uid, muted);
 }
 });
 }
};
private void onRemoteUserLeft() {
 FrameLayout container = (FrameLayout) findViewById(R.id.remote_video_view_container);
 container.removeAllViews();
 //文案可随意定制
 View tipMsg = findViewById(R.id.quick_tips_when_use_agora_sdk);
 tipMsg.setVisibility(View.VISIBLE);
}
private void onRemoteUserVideoMuted(int uid, boolean muted) {
 FrameLayout container = (FrameLayout) findViewById(R.id.remote_video_view_container);
 SurfaceView surfaceView = (SurfaceView) container.getChildAt(0);
 Object tag = surfaceView.getTag();
 if (tag != null && (Integer) tag == uid) {
 surfaceView.setVisibility(muted ? View.GONE : View.VISIBLE);
 }
}

打开视频模式

enableVideo()方法用于打开视频模式。可以在加入频道前或者通话中调用,在加入频道前调用,则自动开启视频模式,在通话中调用则由音频模式切换为视频模式。调用 disableVideo() 方法可关闭视频模式。

setVideoProfile()方法设置视频编码属性(Profile)。每个属性对应一套视频参数,如分辨率、帧率、码率等。 当设备的摄像头不支持指定的分辨率时,SDK 会自动选择一个合适的摄像头分辨率,但是编码分辨率仍然用 setVideoProfile() 指定的。

该方法仅设置编码器编出的码流属性,可能跟最终显示的属性不一致,例如编码码流分辨率为 640x480,码流的旋转属性为 90 度,则显示出来的分辨率为竖屏模式。

/**
 * Tutorial Step 2
 * 打开视频模式,并设置本地视频属性
 */
private void setupVideoProfile() {
 //打开视频模式
 mRtcEngine.enableVideo();
 //设置本地视频属性
 mRtcEngine.setVideoProfile(Constants.VIDEO_PROFILE_360P, false);
}

设置本地视频显示属性

setupLocalVideo( VideoCanvas local )方法用于设置本地视频显示信息。应用程序通过调用此接口绑定本地视频流的显示视窗(view),并设置视频显示模式。 在应用程序开发中,通常在初始化后调用该方法进行本地视频设置,然后再加入频道。退出频道后,绑定仍然有效,如果需要解除绑定,可以调用 setupLocalVideo(null) 。

/**
 * Tutorial Step 3
 * 设置本地视频显示属性
 */
private void setupLocalVideo() {
 FrameLayout container = (FrameLayout) findViewById(R.id.local_video_view_container);
 SurfaceView surfaceView = RtcEngine.CreateRendererView(getBaseContext());
 surfaceView.setZOrderMediaOverlay(true);
 container.addView(surfaceView);
 mRtcEngine.setupLocalVideo(new VideoCanvas(surfaceView, VideoCanvas.RENDER_MODE_ADAPTIVE, 0));
}

加入一个频道

joinChannel(String token,String channelName,String optionalInfo,int optionalUid )方法让用户加入通话频道,在同一个频道内的用户可以互相通话,多个用户加入同一个频道,可以群聊。 使用不同 App ID 的应用程序是不能互通的。如果已在通话中,用户必须调用 leaveChannel() 退出当前通话,才能进入下一个频道。

/**
 * Tutorial Step 4
 * 加入一个频道
 */
private void joinChannel() {
 //如果不指定UID,Agroa将自动生成并分配一个UID
 mRtcEngine.joinChannel(null, "demoChannel1", "Extra Optional Data", 0);
}

设置远端视频显示属性

setupRemoteVideo( VideoCanvas remote)方法用于绑定远程用户和显示视图,即设定 uid 指定的用户用哪个视图显示。调用该接口时需要指定远程视频的 uid,一般可以在进频道前提前设置好。

如果应用程序不能事先知道对方的 uid,可以在 APP 收到 onUserJoined 事件时设置。如果启用了视频录制功能,视频录制服务会做为一个哑客户端加入频道,因此其他客户端也会收到它的 onUserJoined 事件,APP 不应给它绑定视图(因为它不会发送视频流),如果 APP 不能识别哑客户端,可以在 onFirstRemoteVideoDecoded 事件时再绑定视图。解除某个用户的绑定视图可以把 view 设置为空。退出频道后,SDK 会把远程用户的绑定关系清除掉。

/**
 * Tutorial Step 5
 * 设置远端视频显示属性
 */
private void setupRemoteVideo(int uid) {
 FrameLayout container = (FrameLayout) findViewById(R.id.remote_video_view_container);
 if (container.getChildCount() >= 1) {
 return;
 }
 SurfaceView surfaceView = RtcEngine.CreateRendererView(getBaseContext());
 container.addView(surfaceView);
 mRtcEngine.setupRemoteVideo(new VideoCanvas(surfaceView, VideoCanvas.RENDER_MODE_ADAPTIVE, uid));
 surfaceView.setTag(uid);
 //文案可随意定制
 View tipMsg = findViewById(R.id.quick_tips_when_use_agora_sdk);
 tipMsg.setVisibility(View.GONE);
}

离开当前频道

leaveChannel()方法用于离开频道,即挂断或退出通话。

当调用 joinChannel() API 方法后,必须调用 leaveChannel() 结束通话,否则无法开始下一次通话。 不管当前是否在通话中,都可以调用 leaveChannel(),没有副作用。该方法会把会话相关的所有资源释放掉。该方法是异步操作,调用返回时并没有真正退出频道。在真正退出频道后,SDK 会触发 onLeaveChannel 回调。

/**
 * Tutorial Step 6
 * 离开当前频道
 */
private void leaveChannel() {
 mRtcEngine.leaveChannel();
}
public void onEncCallClicked(View view) {
 finish();
}
@Override
protected void onDestroy() {
 super.onDestroy();
 leaveChannel();
 RtcEngine.destroy();
 mRtcEngine = null;
}

管理摄像头

switchCamera()方法用于在前置/后置摄像头间切换。除此以外Agora还提供了一下管理摄像头的方法:例如setCameraTorchOn(boolean isOn)设置是否打开闪光灯、setCameraAutoFocusFaceModeEnabled(boolean enabled)设置是否开启人脸对焦功能等等。

/**
 * Tutorial Step 7
 * 切换前置/后置摄像头
 */
public void onSwitchCameraClicked(View view) {
 mRtcEngine.switchCamera();
}

将自己静音

muteLocalAudioStream(boolean muted)方法用于静音/取消静音。该方法可以允许/禁止往网络发送本地音频流。但该方法并没有禁用麦克风,不影响录音状态。

/**
 * Tutorial Step 8
 * 将自己静音
 */
public void onLocalAudioMuteClicked(View view) {
 ImageView iv = (ImageView) view;
 if (iv.isSelected()) {
 iv.setSelected(false);
 iv.clearColorFilter();
 } else {
 iv.setSelected(true);
 iv.setColorFilter(getResources().getColor(R.color.colorPrimary), PorterDuff.Mode.MULTIPLY);
 }
 mRtcEngine.muteLocalAudioStream(iv.isSelected());
}

暂停本地视频流

muteLocalVideoStream(boolean muted)方法用于暂停发送本地视频流,但该方法并没有禁用摄像头,不影响本地视频流获取。

/**
 * Tutorial Step 9
 * 暂停本地视频流
 */
public void onLocalVideoMuteClicked(View view) {
 ImageView iv = (ImageView) view;
 if (iv.isSelected()) {
 iv.setSelected(false);
 iv.clearColorFilter();
 } else {
 iv.setSelected(true);
 iv.setColorFilter(getResources().getColor(R.color.colorPrimary), PorterDuff.Mode.MULTIPLY);
 }
 mRtcEngine.muteLocalVideoStream(iv.isSelected());
 FrameLayout container = (FrameLayout) findViewById(R.id.local_video_view_container);
 SurfaceView surfaceView = (SurfaceView) container.getChildAt(0);
 surfaceView.setZOrderMediaOverlay(!iv.isSelected());
 surfaceView.setVisibility(iv.isSelected() ? View.GONE : View.VISIBLE);
}

完成一对一视频通话

拿两部手机安装编译好的App,如果能看见两个自己,说明你成功了。至此我们已经实现了一对一的视频通话。接下来,我们来进一步实现多人视频通话功能,主要需要解决两个问题:

  1. 随着加入人数和他们的手机摄像头分辨率的变化,显示不同的UI,即所谓的“分屏”
  2. 点击分屏中的小窗,可以放大显示该聊天窗

分屏

根据前期技术调研,分屏显示最好的方式是采用瀑布流结合动态聊天窗实现,这样比较方便的能够适应UI的变化。所谓瀑布流,就是目前比较流行的一种列表布局,会在界面上呈现参差不齐的多栏布局。我们先实现一个瀑布流:

瀑布流的实现方式很多,本文采用结合 GridLayoutManager的RecyclerView 来实现。我们首先自定义一个 RecyclerView,命名为 GridVideoViewContainer。核心代码如下:

int count = uids.size();
if (count <= 2) { 
 // 只有本地视频或聊天室内只有另外一个人
 this.setLayoutManager(new LinearLayoutManager(activity.getApplicationContext(), orientation, false));
} else if (count > 2) {
 // 多人聊天室
 int itemSpanCount = getNearestSqrt(count);
 this.setLayoutManager(new GridLayoutManager(activity.getApplicationContext(), itemSpanCount, orientation, false));
}

根据上面的代码可以看出,在聊天室里只有自己的本地视频或者只有另外一个人的时候,采用 LinearLayoutManager,这样的布局其实与前文的一对一聊天类似;而在真正意义的多人聊天室里,则采用 GridLayoutManager 实现瀑布流,其中 itemSpanCount 就是瀑布流的列数。

有了一个可用的瀑布流之后,下面我们就可以实现动态聊天窗了:

动态聊天窗的要点在于 item 的大小由视频的宽高比决定,因此 Adapter 及其对应的 layout 就该注意不要写死尺寸。在 Adapter 里控制 item 具体尺寸的代码如下:

if (force || mItemWidth == 0 || mItemHeight == 0) {
 WindowManager windowManager = (WindowManager) mContext.getSystemService(Context.WINDOW_SERVICE);
 DisplayMetrics outMetrics = new DisplayMetrics();
 windowManager.getDefaultDisplay().getMetrics(outMetrics);
 int count = uids.size();
 int DividerX = 1;
 int DividerY = 1;
 if (count == 2) {
 DividerY = 2;
 } else if (count >= 3) {
 DividerX = getNearestSqrt(count);
 DividerY = (int) Math.ceil(count * 1.f / DividerX);
 }
 int width = outMetrics.widthPixels;
 int height = outMetrics.heightPixels;
 if (width > height) {
 mItemWidth = width / DividerY;
 mItemHeight = height / DividerX;
 } else {
 mItemWidth = width / DividerX;
 mItemHeight = height / DividerY;
 }
}

以上代码根据视频的数量确定了列数和行数,然后根据列数和屏幕宽度确定了视频的宽度,接着根据视频的宽高比和视频宽度确定了视频高度。同时也考虑了手机的横竖屏情况(就是if (width > height)这行代码)。

该 Adapter 对应的 layout 的代码如下:

<RelativeLayout
 xmlns:android="http://schemas.android.com/apk/res/android"
 android:id="@+id/user_control_mask"
 android:layout_width="match_parent"
 android:layout_height="match_parent"
 android:orientation="vertical">
 <ImageView
 android:id="@+id/default_avatar"
 android:layout_width="wrap_content"
 android:layout_height="wrap_content"
 android:layout_centerInParent="true"
 android:visibility="gone"
 android:src="@drawable/icon_default_avatar"
 android:contentDescription="DEFAULT_AVATAR" />
 <ImageView
 android:id="@+id/indicator"
 android:layout_width="wrap_content"
 android:layout_height="wrap_content"
 android:layout_centerHorizontal="true"
 android:layout_alignParentBottom="true"
 android:layout_marginBottom="@dimen/video_indicator_bottom_margin"
 android:contentDescription="VIDEO_INDICATOR" />
 <LinearLayout
 android:id="@+id/video_info_container"
 android:layout_width="wrap_content"
 android:layout_height="wrap_content"
 android:layout_alignParentTop="true"
 android:layout_marginTop="24dp"
 android:layout_marginStart="15dp"
 android:layout_marginLeft="15dp"
 android:visibility="gone"
 android:orientation="vertical">
 <TextView
 android:id="@+id/video_info_metadata"
 android:layout_width="wrap_content"
 android:layout_height="wrap_content"
 android:singleLine="true"
 style="@style/NotificationUIText" />
 </LinearLayout>
</RelativeLayout>

我们可以看到,layout 中有关尺寸的属性都 是wrap_content,这就使得 item 大小随视频宽高比变化成为可能。

把分屏的布局写好之后,我们就可以在每一个 item 上播放聊天视频了。

播放聊天视频

在 Agora SDK 中一个远程视频的显示只和该用户的 UID 有关,所以使用的数据源只需要简单定义为包含 UID 和对应的 SurfaceView 即可,就像这样:

private final HashMap<Integer, SurfaceView> mUidsList = new HashMap<>();
 ```
每当有人加入了我们的聊天频道,都会触发`onFirstRemoteVideoDecoded(int uid, int width, int height, int elapsed)`方法,第一个 uid 就是他们的 UID;接下来我们要为每个 item 新建一个 SurfaceView 并为其创建渲染视图,最后将它们加入刚才创建好的mUidsList里并调用`setupRemoteVideo( VideoCanvas remote )`方法播放这个聊天视频。这个过程的完整代码如下:
 ```
@Override
public void onFirstRemoteVideoDecoded(int uid, int width, int height, int elapsed) {
 doRenderRemoteUi(uid);
}
private void doRenderRemoteUi(final int uid) {
 runOnUiThread(new Runnable() {
 @Override
 public void run() {
 if (isFinishing()) {
 return;
 }
 if (mUidsList.containsKey(uid)) {
 return;
 }
 SurfaceView surfaceV = RtcEngine.CreateRendererView(getApplicationContext());
 mUidsList.put(uid, surfaceV);
 boolean useDefaultLayout = mLayoutType == LAYOUT_TYPE_DEFAULT;
 surfaceV.setZOrderOnTop(true);
 surfaceV.setZOrderMediaOverlay(true);
 rtcEngine().setupRemoteVideo(new VideoCanvas(surfaceV, VideoCanvas.RENDER_MODE_HIDDEN, uid));
 if (useDefaultLayout) {
 log.debug("doRenderRemoteUi LAYOUT_TYPE_DEFAULT " + (uid & 0xFFFFFFFFL));
 switchToDefaultVideoView();
 } else {
 int bigBgUid = mSmallVideoViewAdapter == null ? uid : mSmallVideoViewAdapter.getExceptedUid();
 log.debug("doRenderRemoteUi LAYOUT_TYPE_SMALL " + (uid & 0xFFFFFFFFL) + " " + (bigBgUid & 0xFFFFFFFFL));
 switchToSmallVideoView(bigBgUid);
 }
 }
 });
}

以上代码与前文中播放一对一视频的代码如出一撤,但是细心的读者可能已经发现我们并没有将生成的 SurfaceView 放在界面里,这正是与一对一视频的不同之处:我们要在一个抽象的 VideoViewAdapter 类里将 SurfaceView 放出来,关键代码如下:

SurfaceView target = user.mView;
VideoViewAdapterUtil.stripView(target);
holderView.addView(target, 0, new FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT));

一般 Android 工程师看见 holderView 就明白这是 ViewHolder 的 layout 的根 layout 了,而 user 是哪儿来的,详见文末的代码,文中不做赘述。

这样在多人聊天的时候我们就能使用分屏的方式播放用户聊天视频了,如果想放大某一个用户的视频该怎么办呢?

全屏和小窗

当用户双击某一个 item 的时候,他希望对应的视频能够全屏显示,而其他的视频则变成小窗口,那么我们先定义一个双击事件接口:

public interface VideoViewEventListener {
 void onItemDoubleClick(View v, Object item);
}
具体实现方式如下:
mGridVideoViewContainer.setItemEventHandler(new VideoViewEventListener() {
 @Override
 public void onItemDoubleClick(View v, Object item) {
 log.debug("onItemDoubleClick " + v + " " + item + " " + mLayoutType);
 if (mUidsList.size() < 2) {
 return;
 }
 UserStatusData user = (UserStatusData) item;
 int uid = (user.mUid == 0) ? config().mUid : user.mUid;
 if (mLayoutType == LAYOUT_TYPE_DEFAULT && mUidsList.size() != 1) {
 switchToSmallVideoView(uid);
 } else {
 switchToDefaultVideoView();
 }
 }
});

将被选中的视频全屏播放的方法很容易理解,我们只看生成小窗列表的方法:

private void switchToSmallVideoView(int bigBgUid) {
 HashMap<Integer, SurfaceView> slice = new HashMap<>(1);
 slice.put(bigBgUid, mUidsList.get(bigBgUid));
 Iterator<SurfaceView> iterator = mUidsList.values().iterator();
 while (iterator.hasNext()) {
 SurfaceView s = iterator.next();
 s.setZOrderOnTop(true);
 s.setZOrderMediaOverlay(true);
 }
 mUidsList.get(bigBgUid).setZOrderOnTop(false);
 mUidsList.get(bigBgUid).setZOrderMediaOverlay(false);
 mGridVideoViewContainer.initViewContainer(this, bigBgUid, slice, mIsLandscape);
 bindToSmallVideoView(bigBgUid);
 mLayoutType = LAYOUT_TYPE_SMALL;
 requestRemoteStreamType(mUidsList.size());
}

小窗列表要注意移除全屏的那个 UID,此外一切都和正常瀑布流视图相同,包括双击小窗的item将其全屏播放。

到了这里我们就已经使用 Agora SDK 完成了一个有基本功能的简单多人聊天 demo,要产品化还有很多的东西要做,在这里先做一个简单的总结吧!

总结

声网Agora SDK已经仅覆盖了主流的操作系统,集成效率也比较高,而且还支持包括聊天,会议,直播等功能在内的多个模式的视频通话。SDK 中 API 设计基本能够满足大部分的开发需要,而且隐藏了底层开发,只需要提供 SurfaceView 和 UID 即可播放视频,这样对于 App 层的开发者来说十分友好。非常适合有视频聊天开发需求的开发者。

相关推荐