本系列教程将分为三期,分享基于 Agora SDK 在各系统平台应用中实现一对一视频通话、多人互动直播,以及结合跨平台技术进行开发。本期推送在 Android、iOS、Windows、Web、macOS 上实现多人视频互动直播。
本篇基于上一篇「Android 端一对一视频通话」教程,讲分享如何使用 Agora SDK 开发多人互动直播。
本文是采用瀑布流结合动态聊天窗实现分屏显示,这样比较方便的能够适应UI的变化。所谓瀑布流,就是目前比较流行的一种列表布局,会在界面上呈现参差不齐的多栏布局。我们先实现一个瀑布流:
瀑布流的实现方式很多,本文采用结合 GridLayoutManager的RecyclerView 来实现。我们首先自定义一个 RecyclerView,命名为 GridVideoViewContainer。核心代码如下:
int count = uids.size();
if(count <= 2) {
// 只有本地视频或聊天室内只有另外一个人
this.setLayoutManager(newLinearLayoutManager(activity.getApplicationContext(), orientation, false));
} elseif(count > 2) {
// 多人聊天室
int itemSpanCount = getNearestSqrt(count);
this.setLayoutManager(newGridLayoutManager(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 = newDisplayMetrics();
windowManager.getDefaultDisplay().getMetrics(outMetrics);
int count = uids.size();
intDividerX= 1;
intDividerY= 1;
if(count == 2) {
DividerY= 2;
} elseif(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 的代码如下:
我们可以看到,layout 中有关尺寸的属性都 是wrap_content,这就使得 item 大小随视频宽高比变化成为可能。
把分屏的布局写好之后,我们就可以在每一个 item 上播放聊天视频了。
在 Agora SDK 中一个远程视频的显示只和该用户的 UID 有关,所以使用的数据源只需要简单定义为包含 UID 和对应的 SurfaceView 即可,就像这样:
privatefinalHashMap mUidsList = newHashMap<>();
每当有人加入了我们的聊天频道,都会触发onFirstRemoteVideoDecoded(int uid, int width, int height, int elapsed)
方法,第一个 uid 就是他们的 UID;接下来我们要为每个 item 新建一个 SurfaceView 并为其创建渲染视图,最后将它们加入刚才创建好的mUidsList里并调用setupRemoteVideo( VideoCanvas remote )
方法播放这个聊天视频。这个过程的完整代码如下:
@Override
publicvoid onFirstRemoteVideoDecoded(int uid, int width, int height, int elapsed) {
doRenderRemoteUi(uid);
}
privatevoid doRenderRemoteUi(finalint uid) {
runOnUiThread(newRunnable() {
@Override
publicvoid 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(newVideoCanvas(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, newFrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT));
一般 Android 工程师看见 holderView 就明白这是 ViewHolder 的 layout 的根 layout 了,而 user 是哪儿来的,详见文末的代码,文中不做赘述。
这样在多人聊天的时候我们就能使用分屏的方式播放用户聊天视频了,如果想放大某一个用户的视频该怎么办呢?
当用户双击某一个 item 的时候,他希望对应的视频能够全屏显示,而其他的视频则变成小窗口,那么我们先定义一个双击事件接口:
publicinterfaceVideoViewEventListener{
void onItemDoubleClick(View v, Object item);
}
具体实现方式如下:
mGridVideoViewContainer.setItemEventHandler(newVideoViewEventListener() {
@Override
publicvoid 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();
}
}
});
将被选中的视频全屏播放的方法很容易理解,我们只看生成小窗列表的方法:
privatevoid switchToSmallVideoView(int bigBgUid) {
HashMap slice = newHashMap<>(1);
slice.put(bigBgUid, mUidsList.get(bigBgUid));
Iterator 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。
更多产品信息、开发教程以及相关技术活动,请点击「阅读原文」获取。
如开发中遇到问题,可访问 RTC 开发者社区发帖提问