作者:声网用户,资深Android工程师吴东洋
本系列文章分享了基于Agora SDK 2.1实现多人视频通话的实践经验。

在上一篇《Android 多人视频聊天应用的开发(一)一对一聊天》中我们学习了如何使用声网Agora SDK 进行一对一的聊天,本篇主要讨论如何使用 Agora SDK 进行多人聊天。主要需要实现以下功能:
1. 上一篇已经实现过的聊天功能
2. 随着加入人数和他们的手机摄像头分辨率的变化,显示不同的UI,即所谓的“分屏”
3. 点击分屏中的小窗,可以放大显示该聊天窗

分屏

根据前期技术调研,分屏显示最好的方式是采用瀑布流结合动态聊天窗实现,这样比较方便的能够适应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() {@Overridepublic 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() {@Overridepublic 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 层的开发者来说十分友好。非常适合有视频聊天开发需求的开发者。在视频领域创业大爆发的今天,建议更多的想要从事该领域的开发者可以尝试下。

Android端实现多人音视频聊天应用(二):多人视频通话相关推荐

  1. 在多人音视频聊天中插入现场直播

    如何在聊天中插入现场直播呢? 今天我就教给大家怎样在我们的聊天中插入现场直播.(本文聊天以多人音视频为例) 首先,我们要知道现场直播是什么呢? 它是通过流媒体技术来实现实时在线播放 什么是流媒体呢? ...

  2. Android端实现多人音视频聊天应用(一)

    本文转载于资深Android开发者"东风玖哥"的博客. 本系列文章分享了基于Agora SDK 2.1实现多人视频通话的实践经验. 转载已经过原作者许可.原文地址 自从2016年, ...

  3. WebRTC多人音视频聊天架构及实战

    三种模式 简单介绍一下基于 WebRTC 的多人通信的几种架构模式. 1.Mesh 架构 我们之前写过几个 1 v 1 的栗子,它们的连接模式如下: 这是典型的端到端对等连接,所以当我们要实现多人视频 ...

  4. android端采用FFmpeg进行音视频合成与分离

    上一篇文章谈到音频剪切.混音.拼接与转码,也详细介绍cMake配置与涉及FFmpeg文件的导入: android端采用FFmpeg进行音频混合与拼接剪切 .现在接着探讨音视频的合成与分离. 1.音频提 ...

  5. 使用mediasoup-demo搭建多人音视频聊天室

    一. 环境搭建 安装 nodejs,npm:sudo apt install nodejs(注意版本 >= nodejs 10.0,>= npm 6.4.1) 安装 gulp:npm in ...

  6. VUE实现Web端多人语音视频聊天

    1 多人语音聊天功能介绍 本文展示了如何使用 ZEGO Express SDK 构造多人音视频通话场景,即实现多对多实时音视频聊天互动.用户可在房间内与其余用户进行实时音视频通话,互相推拉流.该场景可 ...

  7. iOS WebRTC多人音视频建立的流程

    前言 本文主要以"代码是最好的注释"为基点,介绍在处理iOS端多人音视频的建立流程. 在看本篇前建议先了解一下多人音视频通讯现在的常用架构,参考<WebRTC多人音视频聊天架 ...

  8. 多人实时音视频聊天架构探讨

    前言 移动互联网发展迅猛,目前实时音视频技术已被广泛地应用在了实时在线教育.智能家居.在线直播.安防监控等领域.这之中,诸如多人视频会议.在线实时视频教育等场景,跟传统的一对一实时音视频聊天,在技术架 ...

  9. 浅析即时通讯音视频开发多人实时音视频聊天架构

    移动互联网发展迅猛,目前实时音视频技术已被广泛地应用在了实时在线教育.智能家居.在线直播.安防监控等领域.这之中,诸如多人视频会议.在线实时视频教育等场景,跟传统的一对一实时音视频聊天,在技术架构的实 ...

最新文章

  1. Dart 2.2 发布,谷歌推出的结构化编程语言
  2. VB:如何选定文件或文件夹
  3. C和C++的关键字区别
  4. jQuery的事件绑定和解绑
  5. 理论基础 —— 查找 —— 二分查找
  6. 可以进行单元测试么_大量实例助攻,让你的单元测试更高效
  7. C#对象赋值出现的诡异问题,或许你也遇到过,有待你的解决
  8. Android:学习路线总结,绝对干货
  9. WebStorm功能特点以及使用指南
  10. java this和super_Java中超详细this与super的概念和用法_Java_软件编程
  11. 四川传媒学院计算机应用技术分数,四川传媒学院历年各专业录取文化最低控制分数线一览表...
  12. 渐开线曲线方程c语言,proe常用曲线方程大全
  13. idea2017永久性破解
  14. 电脑上计算机软件一直自动弹出,拨号连接自动弹出,详细教您如何解决电脑总是自动弹出...
  15. Java String、StringBuffer和StringBuilder的区别
  16. AllegroPCB里面设置好颜色,光绘文件artwork和其他板子相关参数怎么重复应用到其他板子里面
  17. 新知实验室 腾讯云实时音视频产品体验
  18. 2D Conforming Triangulations
  19. 实数编码 matlab ga,实数编码的遗传算法与MATLAB
  20. 互联网+时代,智慧停车如何改变城市“停车难”现状?

热门文章

  1. 打开网上邻居,点击“查看工作组计算机”,出现:“Workgroup无法访问,您...
  2. 心理、意识和其他状态
  3. 李俊计算机哈佛大学,李俊教授个人主页
  4. 【已解决】因为计算机丢失D3DCOMPILER_47.dll 的解决方法
  5. SQL1084C Shared memory segments cannot be allocat
  6. Android音视频——基础介绍
  7. 转-果壳网python工程师招聘启事
  8. 用STM32F105双CAN激活众泰汽车中控屏投射
  9. 考试末名的英国教授获诺贝尔奖
  10. Linux x8664汇编,xorl%eax,g86生成的x86_64汇编代码中的%eax