本系列教程将分为三期,分享基于 Agora SDK 在各系统平台应用中实现一对一视频通话、多人互动直播,以及结合跨平台技术进行开发。本期推送在 Android、iOS、Windows、Web、macOS 上实现多人视频互动直播。
本篇基于上一篇「Android 端一对一视频通话」教程,讲分享如何使用 Agora SDK 开发多人互动直播。
分屏
本文是采用瀑布流结合动态聊天窗实现分屏显示,这样比较方便的能够适应 UI 的变化。所谓瀑布流,就是目前比较流行的一种列表布局,会在界面上呈现参差不齐的多栏布局。我们先实现一个瀑布流:
瀑布流的实现方式很多,本文采用结合 GridLayoutManager 的 RecyclerView 来实现。我们首先自定义一个 RecyclerView,命名为 GridVideoViewContainer。核心代码如下:
int
count = uids.size();
if
(count <=
2
) {
// 只有本地视频或聊天室内只有另外一个人
this
.setLayoutManager(
new
LinearLayoutManager
(activity.getApplicationContext(), orientation,
false
));
}
elseif
(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
;
}
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 的代码如下:
<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 即可,就像这样:
privatefinal
HashMap
<
Integer
,
SurfaceView
> mUidsList =
new
HashMap
<>();
复制代码
每当有人加入了我们的聊天频道,都会触发 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(
new
Runnable
() {
@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(
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 的时候,他希望对应的视频能够全屏显示,而其他的视频则变成小窗口,那么我们先定义一个双击事件接口:
publicinterface
VideoViewEventListener
{
void
onItemDoubleClick(
View
v,
Object
item);
}
复制代码
具体实现方式如下:
mGridVideoViewContainer.setItemEventHandler(
new
VideoViewEventListener
() {
@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
<
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 公众号。
原文链接:https://mp.weixin.qq.com/s/lO5pf71goWTRFhm8mo3jKw
评论