Android端實現多人音視訊聊天應用(二):多人視訊通話

聲網Agora發表於2018-04-18

本文源自RTC 開發者社群,資深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() {
       @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,不僅覆蓋了主流的作業系統,整合效率也比較高,而且還支援包括聊天,會議,直播等功能在內的多個模式的視訊通話。SDK 中 API 設計基本能夠滿足大部分的開發需要,而且隱藏了底層開發,只需要提供 SurfaceView 和 UID 即可播放視訊,這樣對於 App 層的開發者來說十分友好。非常適合有視訊聊天開發需求的開發者。在視訊領域創業大爆發的今天,建議更多的想要從事該領域的開發者可以嘗試下。

如果參考本文時遇到開發問題,歡迎訪問聲網 Agora問答版塊,發帖與聲網工程師交流。

相關文章