Android端實現多人音視訊聊天應用(一)

聲網Agora發表於2018-04-10

作者:聲網使用者,資深Android工程師吳東洋。 本系列文章分享了基於Agora SDK 2.1實現多人視訊通話的實踐經驗。 轉載已經過原作者許可。原文地址

自從2016年,鼓吹“網際網路寒冬”的論調甚囂塵上,2017年亦有愈演愈烈之勢。但連麥直播、線上抓娃娃、直播問答、遠端狼人殺等型別的專案卻異軍突起,成了投資人的風口,創業者的藍海和使用者的必裝App,而這些方向的專案都有一個共同的特點——都依賴視訊通話和全互動直播技術。

聲網Agora.io的SDK讓App和網站都可以實現高質量的音訊通話、視訊通話、全互動直播。我試著通過該SDK實現一個多人視訊通話應用。本文先分享整合與一對一視訊通話的部分。

環境

聲網Agora.io SDK的相容性良好,對硬體裝置和軟體系統的要求不高,開發環境和測試環境滿足以下條件即可:

  • Android SDK API Level >= 16
  • Android Studio 2.0 或以上版本
  • 支援語音和視訊功能的真機
  • App 要求 Android 4.1 或以上裝置

以下是我試用聲網Agora.io SDK的開發環境和測試環境:

  • 開發環境
  • Windows 10 家庭中文版
  • Java Version SE 8
  • Android Studio 3.2 Canary 4

測試環境

  • Samsung Nexus (Android 4.4.2 API 19)
  • Mi Note 3 (Android 7.1.1 API 25)

整合

步驟一:首先點此下載完整的SDK和官方demo

步驟二:既然我們要把聲網Agora.io整合到自己的專案裡,所以不必執行sample,我們自己新建一個HelloAgora專案,注意一定要支援C++哦。

步驟三:把libs資料夾裡的arm64-v8a、、armeabi-v7a以及x86資料夾複製貼上到app module的libs裡。如果有NDK開發的必要,則把libs->include資料夾裡的兩個.h標頭檔案複製貼上到合適位置。

步驟四:首先在app module的build.gradle檔案的android程式碼塊中新增如下程式碼:

sourceSets {
    main {
        jniLibs.srcDirs = ['../../../libs']
    }
}
複製程式碼

然後在app module的build.gradle檔案的android->defaultConfig程式碼塊中新增如下程式碼:

ndk {
    abiFilters "armeabi-v7a", "x86" 
}
複製程式碼

接下來在app module的build.gradle檔案的dependencies程式碼塊中新增如下程式碼:

compile 'io.agora.rtc:full-sdk:2.0.0'

複製程式碼

如果用複製貼上jar的方式,那麼此處新增如下程式碼:

compile fileTree(dir: '../../../libs', include: ['*.jar'])

複製程式碼

如果有自定義NDK的必要,可以繼續在app module的build.gradle檔案的android程式碼塊中新增如下程式碼:

externalNativeBuild {
    ndkBuild {
        path 'src/main/cpp/Android.mk'
    }
}
複製程式碼

然後在app module的build.gradle檔案的android->defaultConfig程式碼塊中新增如下程式碼:

externalNativeBuild {
    ndkBuild {
        arguments "NDK_APPLICATION_MK:=src/main/cpp/Application.mk"
    }
}
複製程式碼

最後sync一下,聲網Agora.io的SDK就整合到專案中來了。

許可權

SDK整合完畢後,為了保證SDK能正常執行,我們需要在AndroidManisfest.xml 檔案中宣告以下許可權:

<!--允許程式連線網路-->
<uses-permission android:name="android.permission.INTERNET" />
<!--允許程式錄制音訊-->
<uses-permission android:name="android.permission.RECORD_AUDIO" />
<!--允許程式使用照相裝置-->
<uses-permission android:name="android.permission.CAMERA" />
<!--允許程式修改全域性音訊設定-->
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />
<!--允許程式獲取網路狀態-->
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<!--允許對儲存空間進行讀寫-->
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<!--允許程式連線到已配對的藍芽裝置-->
<uses-permission android:name="android.permission.BLUETOOTH" />

複製程式碼

這些許可權都是Android開發過程中的常見許可權,有經驗的程式設計師都會感覺眼熟,WRITE_EXTERNAL_STORAGE等敏感許可權適配Android 6.0以後版本的問題並非本文關注重點,在此不做贅述。

混淆程式碼

整合SDK並宣告瞭許可權後,就該考慮混淆的問題了,我們需要在project的proguard-rules.pro檔案裡新增以下程式碼:

-keep class io.agora.**{*;}

複製程式碼

經過以上過程後,我們已經完成了聲網Agora.io SDK的快速整合,邁出了走向連麥直播、線上抓娃娃、直播問答、遠端狼人殺等風口的第一步。在接下來的文章裡,我將繼續分享APP ID鑑權、Token鑑權、一對一視訊聊天、建立群聊room、分屏、視窗切換和前後攝像頭切換等內容。

鑑權

APP ID鑑權

所謂APP ID,就是在 Agora建立每個專案都有的一個唯一標識。App ID 可以明確你的專案及組織身份,並在 joinChannel 方法中作為引數,連線到 Agora 實時網路中,實現實時通訊或直播功能。不同的App ID在Agora實時網路中的通話是完全隔離的;Agora 提供的頻道資訊、計費、管理服務也都是基於 App ID。

申請APP ID的操作很簡便,只要在Agora官網https://dashboard.agora.io/projects右側欄目的“專案”中點選“新增新專案”,只需輸入專案名就可生成APP ID,全過程如下圖所示:

Android端實現多人音視訊聊天應用(一)

找到,把“<#YOUR APP ID#>”替換為圖中的馬賽克里的字串。

<string name="agora_app_id"><#YOUR APP ID#></string>
複製程式碼

以上就是APP ID鑑權的全過程。

儘管App ID鑑權在最大程度上方便了開發者使用 Agora 的服務。但App ID 鑑權的安全性不佳,一旦有別有用心的人非法獲取了你的 App ID,他就可以在 Agora 提供的SDK中使用你的App ID。如果你的專案對安全性要求高,或者增加使用者許可權設定的話,建議採用Token鑑權。

Token鑑權

在通訊和直播場景中存在著多個角色,而每種角色又對應著一些預設許可權。比如在直播場景中,主播可以釋出流、訂閱流、邀請嘉賓;觀眾可以訂閱流、申請連麥;管理員則可以踢人或禁言。

Token鑑權的步驟比APP ID鑑權稍微複雜一些,在上文專案列表中檢視 App ID 的地方,啟用該專案的 App Certificate:

首先,點選啟用專案右上方的 編輯 按鈕。

Android端實現多人音視訊聊天應用(一)

將你的 App Certificate 儲存在伺服器端,且對任何客戶端均不可見。當專案的 App Certificate 被啟用後,你必須使用 Token。例如: 在啟用 App Certificate 前,你可以使用 App ID 加入頻道。但啟用了 App Certificate 後,你就必須使用 Token 加入頻道。後臺如何用App Certificate生成Token本文不做贅述。

初始化Agora

RtcEngine 類包含應用程式呼叫的主要方法,呼叫 RtcEngine 的介面最好在同一個執行緒進行,不建議在不同的執行緒同時呼叫。

目前 Agora Native SDK 只支援一個 RtcEngine 例項,每個應用程式僅建立一個 RtcEngine 物件 。 RtcEngine 類的所有介面函式,如無特殊說明,都是非同步呼叫,對介面的呼叫建議在同一個執行緒進行。所有返回值為 int 型的 API,如無特殊說明,返回值 0 為呼叫成功,返回值小於 0 為呼叫失敗。

IRtcEngineEventHandler介面類用於SDK嚮應用程式傳送回撥事件通知,應用程式通過繼承該介面類的方法獲取 SDK 的事件通知。

介面類的所有方法都有預設(空)實現,應用程式可以根據需要只繼承關心的事件。在回撥方法中,應用程式不應該做耗時或者呼叫可能會引起阻塞的 API(如 SendMessage),否則可能影響 SDK 的執行。

private RtcEngine mRtcEngine;

/**
 * Tutorial Step 1
 * 初始化Agora,建立 RtcEngine 物件
 */
private void initializeAgoraEngine() {
    try {
        mRtcEngine = RtcEngine.create(getBaseContext(), getString(R.string.agora_app_id), mRtcEventHandler);
    } catch (Exception e) {
        Log.e(LOG_TAG, Log.getStackTraceString(e));

        throw new RuntimeException("Agora初始化失敗了,檢查一下是哪兒出錯了\n" + Log.getStackTraceString(e));
    }
}

private final IRtcEngineEventHandler mRtcEventHandler = new IRtcEngineEventHandler() {
    @Override
    public void onFirstRemoteVideoDecoded(final int uid, int width, int height, int elapsed) {
        runOnUiThread(new Runnable() {
            @Override
            public void run() {
                //設定遠端視訊顯示屬性
                setupRemoteVideo(uid);
            }
        });
    }

    @Override
    public void onUserOffline(int uid, int reason) {
        runOnUiThread(new Runnable() {
            @Override
            public void run() {
                //其他使用者離開當前頻道回撥
                onRemoteUserLeft();
            }
        });
    }

    @Override
    public void onUserMuteVideo(final int uid, final boolean muted) {
        runOnUiThread(new Runnable() {
            @Override
            public void run() {
                //其他使用者已停發/已重發視訊流回撥
                onRemoteUserVideoMuted(uid, muted);
            }
        });
    }
};

 

private void onRemoteUserLeft() {
    FrameLayout container = (FrameLayout) findViewById(R.id.remote_video_view_container);
    container.removeAllViews();

    //文案可隨意定製
    View tipMsg = findViewById(R.id.quick_tips_when_use_agora_sdk);
    tipMsg.setVisibility(View.VISIBLE);
}

 

private void onRemoteUserVideoMuted(int uid, boolean muted) {
    FrameLayout container = (FrameLayout) findViewById(R.id.remote_video_view_container);

    SurfaceView surfaceView = (SurfaceView) container.getChildAt(0);

    Object tag = surfaceView.getTag();
    if (tag != null && (Integer) tag == uid) {
        surfaceView.setVisibility(muted ? View.GONE : View.VISIBLE);
    }
}
複製程式碼

開啟視訊模式

enableVideo()方法用於開啟視訊模式。可以在加入頻道前或者通話中呼叫,在加入頻道前呼叫,則自動開啟視訊模式,在通話中呼叫則由音訊模式切換為視訊模式。呼叫 disableVideo() 方法可關閉視訊模式。

setVideoProfile()方法設定視訊編碼屬性(Profile)。每個屬性對應一套視訊引數,如解析度、幀率、位元速率等。 當裝置的攝像頭不支援指定的解析度時,SDK 會自動選擇一個合適的攝像頭解析度,但是編碼解析度仍然用 setVideoProfile() 指定的。

該方法僅設定編碼器編出的碼流屬性,可能跟最終顯示的屬性不一致,例如編碼碼流解析度為 640x480,碼流的旋轉屬性為 90 度,則顯示出來的解析度為豎屏模式。

/**
 * Tutorial Step 2
 * 開啟視訊模式,並設定本地視訊屬性
 */
private void setupVideoProfile() {
    //開啟視訊模式
    mRtcEngine.enableVideo();
    //設定本地視訊屬性
    mRtcEngine.setVideoProfile(Constants.VIDEO_PROFILE_360P, false);
}
複製程式碼

設定本地視訊顯示屬性

setupLocalVideo( VideoCanvas local )方法用於設定本地視訊顯示資訊。應用程式通過呼叫此介面繫結本地視訊流的顯示視窗(view),並設定視訊顯示模式。 在應用程式開發中,通常在初始化後呼叫該方法進行本地視訊設定,然後再加入頻道。退出頻道後,繫結仍然有效,如果需要解除繫結,可以呼叫 setupLocalVideo(null) 。

/**
 * Tutorial Step 3
 * 設定本地視訊顯示屬性
 */
private void setupLocalVideo() {
    FrameLayout container = (FrameLayout) findViewById(R.id.local_video_view_container);
    SurfaceView surfaceView = RtcEngine.CreateRendererView(getBaseContext());
    surfaceView.setZOrderMediaOverlay(true);
    container.addView(surfaceView);
    mRtcEngine.setupLocalVideo(new VideoCanvas(surfaceView, VideoCanvas.RENDER_MODE_ADAPTIVE, 0));
}

複製程式碼

加入一個頻道

joinChannel(String token,String channelName,String optionalInfo,int optionalUid )方法讓使用者加入通話頻道,在同一個頻道內的使用者可以互相通話,多個使用者加入同一個頻道,可以群聊。 使用不同 App ID 的應用程式是不能互通的。如果已在通話中,使用者必須呼叫 leaveChannel() 退出當前通話,才能進入下一個頻道。

/**
 * Tutorial Step 4
 * 加入一個頻道
 */
private void joinChannel() {
    //如果不指定UID,Agroa將自動生成並分配一個UID
    mRtcEngine.joinChannel(null, "demoChannel1", "Extra Optional Data", 0);
}
複製程式碼

設定遠端視訊顯示屬性

setupRemoteVideo( VideoCanvas remote)方法用於繫結遠端使用者和顯示檢視,即設定 uid 指定的使用者用哪個檢視顯示。呼叫該介面時需要指定遠端視訊的 uid,一般可以在進頻道前提前設定好。

如果應用程式不能事先知道對方的 uid,可以在 APP 收到 onUserJoined 事件時設定。如果啟用了視訊錄製功能,視訊錄製服務會做為一個啞客戶端加入頻道,因此其他客戶端也會收到它的 onUserJoined 事件,APP 不應給它繫結檢視(因為它不會傳送視訊流),如果 APP 不能識別啞客戶端,可以在 onFirstRemoteVideoDecoded 事件時再繫結檢視。解除某個使用者的繫結檢視可以把 view 設定為空。退出頻道後,SDK 會把遠端使用者的繫結關係清除掉。

/**
 * Tutorial Step 5
 * 設定遠端視訊顯示屬性
 */
private void setupRemoteVideo(int uid) {
    FrameLayout container = (FrameLayout) findViewById(R.id.remote_video_view_container);

    if (container.getChildCount() >= 1) {
        return;
    }

    SurfaceView surfaceView = RtcEngine.CreateRendererView(getBaseContext());
    container.addView(surfaceView);
    mRtcEngine.setupRemoteVideo(new VideoCanvas(surfaceView, VideoCanvas.RENDER_MODE_ADAPTIVE, uid));

    surfaceView.setTag(uid);
    //文案可隨意定製
    View tipMsg = findViewById(R.id.quick_tips_when_use_agora_sdk);
    tipMsg.setVisibility(View.GONE);
}

複製程式碼

離開當前頻道

leaveChannel()方法用於離開頻道,即結束通話或退出通話。

當呼叫 joinChannel() API 方法後,必須呼叫 leaveChannel() 結束通話,否則無法開始下一次通話。 不管當前是否在通話中,都可以呼叫 leaveChannel(),沒有副作用。該方法會把會話相關的所有資源釋放掉。該方法是非同步操作,呼叫返回時並沒有真正退出頻道。在真正退出頻道後,SDK 會觸發 onLeaveChannel 回撥。

/**
 * Tutorial Step 6
 * 離開當前頻道
 */
private void leaveChannel() {
    mRtcEngine.leaveChannel();
}

public void onEncCallClicked(View view) {
    finish();
}

@Override
protected void onDestroy() {
    super.onDestroy();

    leaveChannel();
    RtcEngine.destroy();
    mRtcEngine = null;
}

複製程式碼

管理攝像頭

switchCamera()方法用於在前置/後置攝像頭間切換。除此以外Agora還提供了一下管理攝像頭的方法:例如setCameraTorchOn(boolean isOn)設定是否開啟閃光燈、setCameraAutoFocusFaceModeEnabled(boolean enabled)設定是否開啟人臉對焦功能等等。

/**
 * Tutorial Step 7
 * 切換前置/後置攝像頭
 */
public void onSwitchCameraClicked(View view) {
    mRtcEngine.switchCamera();
}
複製程式碼

將自己靜音

muteLocalAudioStream(boolean muted)方法用於靜音/取消靜音。該方法可以允許/禁止往網路傳送本地音訊流。但該方法並沒有禁用麥克風,不影響錄音狀態。

/**
 * Tutorial Step 8
 * 將自己靜音
 */
public void onLocalAudioMuteClicked(View view) {
    ImageView iv = (ImageView) view;
    if (iv.isSelected()) {
        iv.setSelected(false);
        iv.clearColorFilter();
    } else {
        iv.setSelected(true);
        iv.setColorFilter(getResources().getColor(R.color.colorPrimary), PorterDuff.Mode.MULTIPLY);
    }

    mRtcEngine.muteLocalAudioStream(iv.isSelected());
}
複製程式碼

暫停本地視訊流

muteLocalVideoStream(boolean muted)方法用於暫停傳送本地視訊流,但該方法並沒有禁用攝像頭,不影響本地視訊流獲取。

/**
 * Tutorial Step 9
 * 暫停本地視訊流
 */
public void onLocalVideoMuteClicked(View view) {
    ImageView iv = (ImageView) view;
    if (iv.isSelected()) {
        iv.setSelected(false);
        iv.clearColorFilter();
    } else {
        iv.setSelected(true);
        iv.setColorFilter(getResources().getColor(R.color.colorPrimary), PorterDuff.Mode.MULTIPLY);
    }

    mRtcEngine.muteLocalVideoStream(iv.isSelected());

    FrameLayout container = (FrameLayout) findViewById(R.id.local_video_view_container);
    SurfaceView surfaceView = (SurfaceView) container.getChildAt(0);
    surfaceView.setZOrderMediaOverlay(!iv.isSelected());
    surfaceView.setVisibility(iv.isSelected() ? View.GONE : View.VISIBLE);
}
複製程式碼

執行效果

拿兩部手機安裝編譯好的App,如果能看見兩個自己,說明你成功了。

Android端實現多人音視訊聊天應用(一)

通過本文的學習,我們已經掌握了利用Agora SDK進行一對一聊天的技巧,接下來的文章中,我將繼續介紹如何實現多人聊天室。

相關文章