老鐵,一起來開Party(二)—— 聲網Agora SDK實踐

沉默的範大叔發表於2019-03-04

GitHub地址

github.com/uncleleonfa…

主介面(MainActivity)

在主介面,我們需要檢查先Camera和Audio許可權,以適配Andriod6.0及以上版本。

private static final int PERMISSION_REQ_ID_RECORD_AUDIO = 0;
private static final int PERMISSION_REQ_ID_CAMERA = 1;

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);
    //檢查Audio許可權
    if (checkSelfPermission(Manifest.permission.RECORD_AUDIO, PERMISSION_REQ_ID_RECORD_AUDIO)) {
        //檢查Camera許可權
        checkSelfPermission(Manifest.permission.CAMERA, PERMISSION_REQ_ID_CAMERA);
    }
}

public boolean checkSelfPermission(String permission, int requestCode) {
    if (ContextCompat.checkSelfPermission(this, permission) != PackageManager.PERMISSION_GRANTED) {
        ActivityCompat.requestPermissions(this, new String[]{permission}, requestCode);
        return false;
    }
    return true;
}複製程式碼

頻道介面 (ChannelActivity)

點選開PA!,進入頻道選擇介面

建立頻道列表

這裡使用RecyclerView建立頻道列表。

/**
 * 初始化頻道列表
 */
private void initRecyclerView() {
    mRecyclerView = (RecyclerView) findViewById(R.id.recycler_view);
    mRecyclerView.setHasFixedSize(true);
    mRecyclerView.setLayoutManager(new LinearLayoutManager(this));
    mRecyclerView.setAdapter(new ChannelAdapter(this, mockChannelList()));
}複製程式碼

前置攝像頭預覽

頻道介面背景為前置攝像頭預覽,這個可以使用Android SDK自己實現。但Agora SDK提供了相關API可以直接實現前置攝像頭預覽的功能。具體實現如下:

1. 初始化RtcEngine

RtcEngine是Agora SDK的核心類,叔用一個管理類AgoraManager進行了簡單的封裝,提供操作RtcEngine的核心功能。
初始化如下:

/**
 * 初始化RtcEngine
 */
public void init(Context context) {
    //建立RtcEngine物件, mRtcEventHandler為RtcEngine的回撥
    mRtcEngine = RtcEngine.create(context, context.getString(R.string.private_app_id), mRtcEventHandler);
    //開啟視訊功能
    mRtcEngine.enableVideo();
    //視訊配置,設定為360P
    mRtcEngine.setVideoProfile(Constants.VIDEO_PROFILE_360P, false);
    mRtcEngine.setChannelProfile(Constants.CHANNEL_PROFILE_COMMUNICATION);//設定為通訊模式(預設)
    //mRtcEngine.setChannelProfile(Constants.CHANNEL_PROFILE_LIVE_BROADCASTING);設定為直播模式
    //mRtcEngine.setChannelProfile(Constants.CHANNEL_PROFILE_GAME);設定為遊戲模式
}


/**
 * 在Application類中初始化RtcEngine,注意在AndroidManifest.xml中配置下Application
 */
public class LaoTieApplication extends Application {

    @Override
    public void onCreate() {
        super.onCreate();
        AgoraManager.getInstance().init(getApplicationContext());
    }
}複製程式碼

2. 設定本地視訊

/**
 * 設定本地視訊,即前置攝像頭預覽
 */
public AgoraManager setupLocalVideo(Context context) {
    //建立一個SurfaceView用作視訊預覽
    SurfaceView surfaceView = RtcEngine.CreateRendererView(context);
    //將SurfaceView儲存起來在SparseArray中,後續會將其加入介面。key為視訊的使用者id,這裡是本地視訊, 預設id是0
    mSurfaceViews.put(mLocalUid, surfaceView);
    //設定本地視訊,渲染模式選擇VideoCanvas.RENDER_MODE_HIDDEN,如果選其他模式會出現視訊不會填充滿整個SurfaceView的情況,
    //具體渲染模式的區別是什麼,官方也沒有詳細的說明
    mRtcEngine.setupLocalVideo(new VideoCanvas(surfaceView, VideoCanvas.RENDER_MODE_HIDDEN, mLocalUid));
    return this;//返回AgoraManager以作鏈式呼叫
}複製程式碼

3. 新增SurfaceView到佈局

@Override
protected void onResume() {
    super.onResume();
    //先清空容器
    mFrameLayout.removeAllViews();
    //設定本地前置攝像頭預覽並啟動
    AgoraManager.getInstance().setupLocalVideo(getApplicationContext()).startPreview();
    //將本地攝像頭預覽的SurfaceView新增到容器中
    mFrameLayout.addView(AgoraManager.getInstance().getLocalSurfaceView());
}複製程式碼

4. 停止預覽

/**
 * 停止預覽
 */
@Override
protected void onPause() {
    super.onPause();
    AgoraManager.getInstance().stopPreview();
}複製程式碼

聊天室 (PartyRoomActivity)

點選頻道列表中的選項,跳轉到聊天室介面。聊天室介面顯示規則是:1個人是全屏,2個人是2分屏,3-4個人是4分屏,5-6個人是6分屏, 4分屏和6分屏模式下,雙擊一個小窗,窗會變大,其餘小窗在底部排列。最多支援六人同時聊天。基於這種需求,叔決定寫一個自定義控制元件PartyRoomLayout來完成。PartyRoomLayout直接繼承ViewGroup,根據不同的顯示模式來完成孩子的測量和佈局。

1人全屏

1人全屏其實就是前置攝像頭預覽效果。

前置攝像頭預覽

//設定前置攝像頭預覽並開啟
AgoraManager.getInstance()
        .setupLocalVideo(getApplicationContext())
        .startPreview();
//將攝像頭預覽的SurfaceView加入PartyRoomLayout
mPartyRoomLayout.addView(AgoraManager.getInstance().getLocalSurfaceView());複製程式碼

PartyRoomLayout處理1人全屏

/**
 * 測量一個孩子的情況,孩子的寬高和父容器即PartyRoomLayout一樣
 */
private void measureOneChild(int widthMeasureSpec, int heightMeasureSpec) {
    View child = getChildAt(0);
    child.measure(widthMeasureSpec, heightMeasureSpec);
}

/**
 * 佈局一個孩子的情況
 */
private void layoutOneChild() {
    View child = getChildAt(0);
    child.layout(0, 0, child.getMeasuredWidth(), child.getMeasuredHeight());
}複製程式碼

加入頻道

從頻道列表跳轉過來後,需要加入到使用者所選的頻道。

//更新頻道的TextView
mChannel = (TextView) findViewById(R.id.channel);
String channel = getIntent().getStringExtra(“Channel”);
mChannel.setText(channel);

//在AgoraManager中封裝了加入頻道的API
AgoraManager.getInstance()
            .setupLocalVideo(getApplicationContext())
            .joinChannel(channel)//加入頻道
            .startPreview();複製程式碼

結束通話

當使用者點選結束通話按鈕可以退出頻道

mEndCall = (ImageButton) findViewById(R.id.end_call);
mEndCall.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View v) {
        //AgoraManager裡面封裝了結束通話的API, 退出頻道
        AgoraManager.getInstance().leaveChannel();
        finish();
    }
});複製程式碼

二分屏

事件監聽器

IRtcEngineEventHandler類裡面封裝了Agora SDK裡面的很多事件回撥,在AgoraManager中我們建立了IRtcEngineEventHandler的一個物件mRtcEventHandler,並在建立RtcEngine時傳入。

private IRtcEngineEventHandler mRtcEventHandler = new IRtcEngineEventHandler() {

    /**
     * 當獲取使用者uid的遠端視訊的回撥
     */
    @Override
    public void onFirstRemoteVideoDecoded(int uid, int width, int height, int elapsed) {
        if (mOnPartyListener != null) {
            mOnPartyListener.onGetRemoteVideo(uid);
        }
    }

    /**
     * 加入頻道成功的回撥
     */
    @Override
    public void onJoinChannelSuccess(String channel, int uid, int elapsed) {
        if (mOnPartyListener != null) {
            mOnPartyListener.onJoinChannelSuccess(channel, uid);
        }
    }

    /**
     * 退出頻道
     */
    @Override
    public void onLeaveChannel(RtcStats stats) {
        if (mOnPartyListener != null) {
            mOnPartyListener.onLeaveChannelSuccess();
        }
    }

    /**
     * 使用者uid離線時的回撥
     */
    @Override
    public void onUserOffline(int uid, int reason) {
        if (mOnPartyListener != null) {
            mOnPartyListener.onUserOffline(uid);
        }
    }
};複製程式碼

同時,我們也提供了一個介面,暴露給AgoraManager外部。

public interface OnPartyListener {

    void onJoinChannelSuccess(String channel, int uid);

    void onGetRemoteVideo(int uid);

    void onLeaveChannelSuccess();

    void onUserOffline(int uid);
}複製程式碼

在PartyRoomActivity中監聽事件

AgoraManager.getInstance()
        .setupLocalVideo(getApplicationContext())
        .setOnPartyListener(mOnPartyListener)//設定監聽
        .joinChannel(channel)
        .startPreview();複製程式碼

設定遠端使用者視訊

private AgoraManager.OnPartyListener mOnPartyListener = new AgoraManager.OnPartyListener() {

    /**
     * 獲取遠端使用者視訊的回撥
     */
    @Override
    public void onGetRemoteVideo(final int uid) {
        //操作UI,需要切換到主執行緒
        runOnUiThread(new Runnable() {
            @Override
            public void run() {
                //設定遠端使用者的視訊
                AgoraManager.getInstance().setupRemoteVideo(PartyRoomActivity.this, uid);
                //將遠端使用者視訊的SurfaceView新增到PartyRoomLayout中,這會觸發PartyRoomLayout重新走一遍繪製流程
                mPartyRoomLayout.addView(AgoraManager.getInstance().getSurfaceView(uid));
            }
        });
    }

};複製程式碼

測量佈局二分屏

當第一次回撥onGetRemoteVideo時,說明現在有兩個使用者了,所以在PartyRoomLayout中需要對二分屏模式進行處理

/**
 * 二分屏時的測量
 */
private void measureTwoChild(int widthMeasureSpec, int heightMeasureSpec) {
    for (int i = 0; i < getChildCount(); i++) {
        View child = getChildAt(i);
        int size = MeasureSpec.getSize(heightMeasureSpec);
        //孩子高度為父容器高度的一半
        int childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(size / 2, MeasureSpec.EXACTLY);
        child.measure(widthMeasureSpec, childHeightMeasureSpec);
    }
}

/**
 * 二分屏模式的佈局
 */
private void layoutTwoChild() {
    int left = 0;
    int top = 0;
    int right = getMeasuredWidth();
    int bottom = getChildAt(0).getMeasuredHeight();
    for (int i = 0; i < getChildCount(); i++) {
        View child = getChildAt(i);
        child.layout(left, top, right, bottom);
        top += child.getMeasuredHeight();
        bottom += child.getMeasuredHeight();
    }
}複製程式碼

使用者離線時的處理

當有使用者離線時,我們需要移除該使用者視訊對應的SurfaceView

private AgoraManager.OnPartyListener mOnPartyListener = new AgoraManager.OnPartyListener() {

    @Override
    public void onUserOffline(final int uid) {
        runOnUiThread(new Runnable() {
            @Override
            public void run() {
                //從PartyRoomLayout移除遠端視訊的SurfaceView
                mPartyRoomLayout.removeView(AgoraManager.getInstance().getSurfaceView(uid));
                //清除快取的SurfaceView
                AgoraManager.getInstance().removeSurfaceView(uid);
            }
        });
    }
};複製程式碼

四分屏和六分屏

當有3個或者4個老鐵開趴,介面顯示成四分屏, 當有5個或者6個老鐵開趴,介面切分成六分屏


由於之前已經處理了新進使用者就會建立SurfaceView加入PartyRoomLayout的邏輯,所以這裡只需要處理四六分屏時的測量和佈局

四六分屏測量

private void measureMoreChildSplit(int widthMeasureSpec, int heightMeasureSpec) {
    //列數為兩列,計算行數
    int row = getChildCount() / 2;
    if (getChildCount() % 2 != 0) {
        row = row + 1;
    }
    //根據行數平分高度
    int childHeight = MeasureSpec.getSize(heightMeasureSpec) / row;
    //寬度為父容器PartyRoomLayout的寬度一般,即屏寬的一半
    int childWidth = MeasureSpec.getSize(widthMeasureSpec) / 2;
    for (int i = 0; i < getChildCount(); i++) {
        View child = getChildAt(i);
        int childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(childHeight, MeasureSpec.EXACTLY);
        int childWidthMeasureSpec = MeasureSpec.makeMeasureSpec(childWidth, MeasureSpec.EXACTLY);
        child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
    }
}複製程式碼

四六分屏佈局

private void layoutMoreChildSplit() {
    int left = 0;
    int top = 0;
    for (int i = 0; i < getChildCount(); i++) {
        View child = getChildAt(i);
        int right = left + child.getMeasuredWidth();
        int bottom = top + child.getMeasuredHeight();
        child.layout(left, top, right, bottom);
        if ( (i + 1 )% 2 == 0) {//滿足換行條件,更新left和top,佈局下一行
            left = 0;
            top += child.getMeasuredHeight();
        } else {
            //不滿足換行條件,更新left值,繼續佈局一行中的下一個孩子
            left += child.getMeasuredWidth();
        }
    }
}複製程式碼

雙擊上下分屏佈局


在四六分屏模式下,雙擊一個小窗,窗會變大,其餘小窗在底部排列, 成上下分屏模式。實現思路就是監聽PartyRoomLayout的觸控時間,當是雙擊時,則重新佈局。

觸控事件處理

/**
 *  攔截所有的事件
 */
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
    return true;
}

/**
 * 讓GestureDetector處理觸控事件
 */
@Override
public boolean onTouchEvent(MotionEvent event) {
    mGestureDetector.onTouchEvent(event);
    return true;
}



//四六分屏模式
private static int DISPLAY_MODE_SPLIT = 0;
//上下分屏模式
private static int DISPLAY_MODE_TOP_BOTTOM = 1;
//顯示模式的變數,預設是四六分屏
private int mDisplayMode = DISPLAY_MODE_SPLIT;
//上下分屏時上面View的下標
private int mTopViewIndex = -1;

private GestureDetector.SimpleOnGestureListener mOnGestureListener = new GestureDetector.SimpleOnGestureListener() {

    @Override
    public boolean onDoubleTap(MotionEvent e) {
        handleDoubleTap(e);//處理雙擊事件
        return true;
    }

    private void handleDoubleTap(MotionEvent e) {
        //遍歷所有的孩子
        for (int i = 0; i < getChildCount(); i++) {
            View view = getChildAt(i);
            //獲取孩子view的矩形
            Rect rect = new Rect(view.getLeft(), view.getTop(), view.getRight(), view.getBottom());
            if (rect.contains((int)e.getX(), (int)e.getY())) {//找到雙擊位置的孩子是誰
                if (mTopViewIndex == i) {//如果點選的位置就是上面的view, 則切換成四六分屏模式
                    mDisplayMode = DISPLAY_MODE_SPLIT;
                    mTopViewIndex = -1;//重置上面view的下標
                } else {
                    //切換成上下分屏模式,
                    mTopViewIndex = i;//儲存雙擊位置的下標,即上面View的下標
                    mDisplayMode = DISPLAY_MODE_TOP_BOTTOM;
                }
                requestLayout();//請求重新佈局
                break;
            }
        }
    }
};複製程式碼

上下分屏測量

處理完雙擊事件後,切換顯示模式,請求重新佈局,這時候又會觸發測量和佈局。

/**
 * 上下分屏模式的測量
 */
private void measureMoreChildTopBottom(int widthMeasureSpec, int heightMeasureSpec) {
    for (int i = 0; i < getChildCount(); i++) {
        if (i == mTopViewIndex) {
            //測量上面View
            measureTopChild(widthMeasureSpec, heightMeasureSpec);
        } else {
            //測量下面View
            measureBottomChild(i, widthMeasureSpec, heightMeasureSpec);
        }
    }
}

/**
 *  上下分屏模式時上面View的測量
 */
private void measureTopChild(int widthMeasureSpec, int heightMeasureSpec) {
    int size = MeasureSpec.getSize(heightMeasureSpec);
    //高度為PartyRoomLayout的一半
    int childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(size / 2, MeasureSpec.EXACTLY);
    getChildAt(mTopViewIndex).measure(widthMeasureSpec, childHeightMeasureSpec);
}

/**
 * 上下分屏模式時底部View的測量
 */
private void measureBottomChild(int i, int widthMeasureSpec, int heightMeasureSpec) {
    //除去頂部孩子後還剩的孩子個數
    int childCountExcludeTop = getChildCount() - 1;
    //當底部孩子個數小於等於3時
    if (childCountExcludeTop <= 3) {
        //平分孩子寬度
        int childWidth = MeasureSpec.getSize(widthMeasureSpec) / childCountExcludeTop;
        int size = MeasureSpec.getSize(heightMeasureSpec);
        //高度為PartyRoomLayout的一半
        int childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(size / 2, MeasureSpec.EXACTLY);
        int childWidthMeasureSpec = MeasureSpec.makeMeasureSpec(childWidth, MeasureSpec.EXACTLY);
        getChildAt(i).measure(childWidthMeasureSpec, childHeightMeasureSpec);
    } else if (childCountExcludeTop == 4) {//當底部孩子個數為4個時
        int childWidth = MeasureSpec.getSize(widthMeasureSpec) / 2;//寬度為PartyRoomLayout的一半
        int childHeight = MeasureSpec.getSize(heightMeasureSpec) / 4;//高度為PartyRoomLayout的1/4
        int childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(childHeight, MeasureSpec.EXACTLY);
        int childWidthMeasureSpec = MeasureSpec.makeMeasureSpec(childWidth, MeasureSpec.EXACTLY);
        getChildAt(i).measure(childWidthMeasureSpec, childHeightMeasureSpec);
    } else {//當底部孩子大於4個時
        //計算行的個數
        int row = childCountExcludeTop / 3;
        if (row  % 3 != 0) {
            row ++;
        }
        //孩子的寬度為PartyRoomLayout寬度的1/3
        int childWidth = MeasureSpec.getSize(widthMeasureSpec) / 3;
        //底部孩子平分PartyRoomLayout一半的高度
        int childHeight = (MeasureSpec.getSize(heightMeasureSpec) / 2) / row;
        int childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(childHeight, MeasureSpec.EXACTLY);
        int childWidthMeasureSpec = MeasureSpec.makeMeasureSpec(childWidth, MeasureSpec.EXACTLY);
        getChildAt(i).measure(childWidthMeasureSpec, childHeightMeasureSpec);
    }
}複製程式碼

上下分屏佈局

private void layoutMoreChildTopBottom() {
    //佈局上面View
    View topView = getChildAt(mTopViewIndex);
    topView.layout(0, 0, topView.getMeasuredWidth(), topView.getMeasuredHeight());
    int left = 0;
    int top = topView.getMeasuredHeight();
    for (int i = 0; i < getChildCount(); i++) {
        //上面已經佈局過上面的View, 這裡就跳過
        if (i == mTopViewIndex) {
            continue;
        }
        View view = getChildAt(i);
        int right = left + view.getMeasuredWidth();
        int bottom = top + view.getMeasuredHeight();
        //佈局下面的一個View
        view.layout(left, top, right, bottom);
        left = left + view.getMeasuredWidth();
        if (left >= getWidth()) {//滿足換行條件則換行
            left = 0;
            top += view.getMeasuredHeight();
        }
    }
}複製程式碼

老鐵,一起來開Party(一) —— 聲網Agoria SDK整合
老鐵,一起來開Party(三) —— 聲網Agoria SDK趟坑記

Agora交流群
Agora交流群

相關文章