Android多人視頻聊天應用的開發(三)多人聊天
在上一篇《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() { @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使用總結
聲網Agora提供了高質量的視頻通信SDK,不僅覆蓋了主流的操作系統,集成效率也比較高,而且還支持包括聊天,會議,直播等功能在內的多個模式的視頻通話。SDK中API設計基本能夠滿足大部分的開發需要,而且隱藏了底層開發,只需要提供SurfaceView和UID即可播放視頻,這樣對于App層的開發者來說十分友好。非常適合有視頻聊天開發需求的開發者。在視頻領域創業大爆發的今天,建議更多的想要從事該領域的開發者可以嘗試下。
當然聲網Agora也并非盡善盡美,有些地方還有改進的空間,我有兩個建議,以往廠商可以考慮下:
1、Agora支持Kotlin,目前Kotlin現在已經成為Google官方建議的Android開發語言,支持Kotlin有助于跟上時代的步伐。但Agora并沒有把Kotlin的demo放在官網上。
2、希望可以提供帶基礎UI的SDK,就像UMeng的分享SDK一樣或者AMap的導航SDK,一套帶簡單UI的SDK不僅能直接應用到項目中,還允許開發者進一步定制,這樣更容易收到App層的開發者技術選型時的青睞,他們的集成效率也會更高。