基於 Agora SDK 實現 Android 端的多人視頻互動

本系列教程將分爲三期,分享基於 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 的代碼如下:

<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 即可,就像這樣:

privatefinalHashMap<Integer, SurfaceView> 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<Integer, SurfaceView> slice = newHashMap<>(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。

更多產品信息、開發教程以及相關技術活動,請點擊「閱讀原文」獲取。

如開發中遇到問題,可訪問 RTC 開發者社區發帖提問

發佈了100 篇原創文章 · 獲贊 76 · 訪問量 18萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章