如何基於 Agora Android SDK 在應用中實現視訊通話?

聲網Agora發表於2022-02-28

在很多產品,實時視訊通話已經不是新鮮的功能了,例如視訊會議、社交應用、線上教育,甚至也可能出現在一些元宇宙的場景中。

本文將教你如何通過聲網Agora 視訊 SDK 在 Android 端實現一個視訊通話應用。聲網 SDK 每個月會提供 10000 分鐘的免費使用額度,可實現各類實時音視訊場景。話不多說,我們開始動手實操。

通過開源 Demo,體驗視訊通話

可能有些人,還不瞭解我們要實現的功能最後是怎樣的。所以我們在 GitHub 上提供一個開源的基礎視訊通話示例專案,在開始開發之前你可以通過該示例專案體驗音視訊通話效果。

Github:https://github.com/AgoraIO/API-Examples/blob/master/Android/APIExample/app/src/main/java/io/agora/api/example/examples/basic/JoinChannelVideo.java

在這裡插入圖片描述

視訊通話的技術原理

我們在這裡要實現的是一對一的視訊通話。你可以理解為是兩個使用者通過加入同一個頻道,實現的音視訊的互通。而這個頻道的資料,會通過聲網的 Agora SD-RTN 實時網路來進行低延時傳輸的。

那麼 App 整合 Agora SDK 後,視訊通話的基本工作流程如下圖所示:

在這裡插入圖片描述

如圖所示,實現視訊通話的步驟如下:

  1. 獲取 Token:當 app 客戶端加入頻道時,你需要使用 Token 驗證使用者身份。在測試或生產環境中,從 app 伺服器中獲取 Token。
  2. 加入頻道:呼叫 joinChannel 建立並加入頻道。使用同一頻道名稱的 app 客戶端預設加入同一頻道。頻道可理解為專用於傳輸實時音視訊資料的通道。
  3. 在頻道內釋出和訂閱音視訊流:加入頻道後,app 客戶端均可以在頻道內釋出和訂閱音視訊。

App 客戶端加入頻道需要以下資訊:

  • App ID:Agora 隨機生成的字串,用於識別你的 app,可從 Agora 控制檯獲取,詳細方法可見這篇教程
  • 使用者 ID:使用者的唯一標識。你需要自行設定使用者 ID,並確保它在頻道內是唯一的。
  • Token:在測試或生產環境中,app 客戶端從你的伺服器中獲取 Token。在本文介紹的流程中,你可以從 Agora 控制檯獲取臨時 Token。臨時 Token 的有效期為 24 小時。
  • 頻道名稱:用於標識視訊通話頻道的字串。

開發環境

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

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

以下是本文的開發環境和測試環境:

開發環境

  • 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)

如果你此前還未接觸過聲網 Agora SDK,那麼你還需要做以下準備工作:

  • 註冊一個聲網賬號,進入後臺建立 AppID、獲取 Token,詳細方法可參考這篇教程

https://sso2.agora.io/cn/signup?

專案設定

1.實現視訊通話之前,參考如下步驟設定你的專案:

如需建立新專案,在 Android Studio裡,依次選擇 Phone and Tablet > Empty Activity,建立 Android 專案

建立專案後,Android Studio會自動開始同步 gradle。請確保同步成功再進行下一步操作。

2.整合SDK, 本文推薦使用gradle方式整合Agora SDK:

a. 在 /Gradle Scripts/build.gradle(Project: ) 檔案中新增如下程式碼,以新增 mavenCentral 依賴:

buildscript {
     repositories {
         ...
         mavenCentral()
     }
     ...
}
 
  allprojects {
     repositories {
         ...
         mavenCentral()
     }
}

b. 在 /Gradle Scripts/build.gradle(Module: .app) 檔案中新增如下程式碼,將 Agora 視訊 SDK 整合到你的 Android 專案中:

...
dependencies {
 ...
 // x.y.z,請填寫具體的 SDK 版本號,如:3.5.0。
 // 通過發版說明獲取最新版本號。
 implementation 'io.agora.rtc:full-sdk:x.y.z'
}

3.許可權設定, 在 /app/Manifests/AndroidManifest.xml 檔案中的 `` 後面新增如下網路和裝置許可權:

<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.RECORD_AUDIO" />
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.BLUETOOTH" />

客戶端實現

本節介紹如何使用 Agora 視訊 SDK 在你的 app 裡實現視訊通話的幾個小貼士:

1.獲取必要許可權

啟動應用程式時,檢查是否已在 app 中授予了實現視訊通話所需的許可權。

onCreate 函式中呼叫如下程式碼:

private static final int PERMISSION_REQ_ID = 22;
 
private static final String[] REQUESTED_PERMISSIONS = {
     Manifest.permission.RECORD_AUDIO,
     Manifest.permission.CAMERA
};
 
private boolean checkSelfPermission(String permission, int requestCode) {
    if (ContextCompat.checkSelfPermission(this, permission) !=
            PackageManager.PERMISSION_GRANTED) {
        ActivityCompat.requestPermissions(this, REQUESTED_PERMISSIONS, requestCode);
        return false;
    }
    return true;
}

2.實現視訊通話邏輯

下圖展示視訊通話的 API 呼叫時序:

在這裡插入圖片描述

a. 初始化引擎

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

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

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

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

engine = RtcEngine.create(context.getApplicationContext(), getString(R.string.agora_app_id), iRtcEngineEventHandler);
...
private final IRtcEngineEventHandler iRtcEngineEventHandler = new IRtcEngineEventHandler()
{
    /**Reports a warning during SDK runtime.
     * Warning code: https://docs.agora.io/en/Voice/API%20Reference/java/classio_1_1agora_1_1rtc_1_1_i_rtc_engine_event_handler_1_1_warn_code.html*/
    @Override
    public void onWarning(int warn)
    {
        Log.w(TAG, String.format("onWarning code %d message %s", warn, RtcEngine.getErrorDescription(warn)));
    }
 
    /**Reports an error during SDK runtime.
     * Error code: https://docs.agora.io/en/Voice/API%20Reference/java/classio_1_1agora_1_1rtc_1_1_i_rtc_engine_event_handler_1_1_error_code.html*/
    @Override
    public void onError(int err)
    {
        Log.e(TAG, String.format("onError code %d message %s", err, RtcEngine.getErrorDescription(err)));
        showAlert(String.format("onError code %d message %s", err, RtcEngine.getErrorDescription(err)));
    }
 
    /**Occurs when a user leaves the channel.
     * @param stats With this callback, the application retrieves the channel information,
     *              such as the call duration and statistics.*/
    @Override
    public void onLeaveChannel(RtcStats stats)
    {
        super.onLeaveChannel(stats);
        Log.i(TAG, String.format("local user %d leaveChannel!", myUid));
        showLongToast(String.format("local user %d leaveChannel!", myUid));
    }
 
    /**Occurs when the local user joins a specified channel.
     * The channel name assignment is based on channelName specified in the joinChannel method.
     * If the uid is not specified when joinChannel is called, the server automatically assigns a uid.
     * @param channel Channel name
     * @param uid User ID
     * @param elapsed Time elapsed (ms) from the user calling joinChannel until this callback is triggered*/
    @Override
    public void onJoinChannelSuccess(String channel, int uid, int elapsed)
    {
        Log.i(TAG, String.format("onJoinChannelSuccess channel %s uid %d", channel, uid));
        showLongToast(String.format("onJoinChannelSuccess channel %s uid %d", channel, uid));
        myUid = uid;
        joined = true;
        handler.post(new Runnable()
        {
            @Override
            public void run()
            {
                join.setEnabled(true);
                join.setText(getString(R.string.leave));
            }
        });
    }
 
    @Override
    public void onRemoteAudioStats(io.agora.rtc.IRtcEngineEventHandler.RemoteAudioStats remoteAudioStats) {
        statisticsInfo.setRemoteAudioStats(remoteAudioStats);
        updateRemoteStats();
    }
 
    @Override
    public void onLocalAudioStats(io.agora.rtc.IRtcEngineEventHandler.LocalAudioStats localAudioStats) {
        statisticsInfo.setLocalAudioStats(localAudioStats);
        updateLocalStats();
    }
 
    @Override
    public void onRemoteVideoStats(io.agora.rtc.IRtcEngineEventHandler.RemoteVideoStats remoteVideoStats) {
        statisticsInfo.setRemoteVideoStats(remoteVideoStats);
        updateRemoteStats();
    }
 
    @Override
    public void onLocalVideoStats(io.agora.rtc.IRtcEngineEventHandler.LocalVideoStats localVideoStats) {
        statisticsInfo.setLocalVideoStats(localVideoStats);
        updateLocalStats();
    }
 
    @Override
    public void onRtcStats(io.agora.rtc.IRtcEngineEventHandler.RtcStats rtcStats) {
        statisticsInfo.setRtcStats(rtcStats);
    }
};

b. 設定本地視訊引數

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

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

// Create render view by RtcEngine
SurfaceView surfaceView = RtcEngine.CreateRendererView(context);
if(fl_local.getChildCount() > 0)
{
    fl_local.removeAllViews();
}
// Add to the local container
fl_local.addView(surfaceView, new FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT));
// Setup local video to render your local camera preview
engine.setupLocalVideo(new VideoCanvas(surfaceView, RENDER_MODE_HIDDEN, 0));
// Enable video module
engine.enableVideo();

c . 加入一個頻道

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

ChannelMediaOptions option = new ChannelMediaOptions();
option.autoSubscribeAudio = true;
option.autoSubscribeVideo = true;
int res = engine.joinChannel(accessToken, channelId, "Extra Optional Data", 0, option);
if (res != 0)
{
    // Usually happens with invalid parameters
    // Error code description can be found at:
    // en: https://docs.agora.io/en/Voice/API%20Reference/java/classio_1_1agora_1_1rtc_1_1_i_rtc_engine_event_handler_1_1_error_code.html
    // cn: https://docs.agora.io/cn/Voice/API%20Reference/java/classio_1_1agora_1_1rtc_1_1_i_rtc_engine_event_handler_1_1_error_code.html
    showAlert(RtcEngine.getErrorDescription(Math.abs(res)));
    return;
}

d. 離開當前頻道

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

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

e. 管理攝像頭

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

f. 當遠端使用者加入頻道時,更新遠端使用者介面。

private final IRtcEngineEventHandler iRtcEngineEventHandler = new IRtcEngineEventHandler()
{
    ...
    /**Occurs when a remote user (Communication)/host (Live Broadcast) joins the channel.
     * @param uid ID of the user whose audio state changes.
     * @param elapsed Time delay (ms) from the local user calling joinChannel/setClientRole
     *                until this callback is triggered.*/
    @Override
    public void onUserJoined(int uid, int elapsed)
    {
        super.onUserJoined(uid, elapsed);
        Log.i(TAG, "onUserJoined->" + uid);
        showLongToast(String.format("user %d joined!", uid));
        /**Check if the context is correct*/
        Context context = getContext();
        if (context == null) {
            return;
        }
        if(remoteViews.containsKey(uid)){
            return;
        }
        else{
            handler.post(() ->
            {
                /**Display remote video stream*/
                SurfaceView surfaceView = null;
                // Create render view by RtcEngine
                surfaceView = RtcEngine.CreateRendererView(context);
                surfaceView.setZOrderMediaOverlay(true);
                ViewGroup view = getAvailableView();
                remoteViews.put(uid, view);
                // Add to the remote container
                view.addView(surfaceView, new FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT));
                // Setup remote video to render
                engine.setupRemoteVideo(new VideoCanvas(surfaceView, RENDER_MODE_HIDDEN, uid));
            });
        }
    }
 
    /**Occurs when a remote user (Communication)/host (Live Broadcast) leaves the channel.
     * @param uid ID of the user whose audio state changes.
     * @param reason Reason why the user goes offline:
     *   USER_OFFLINE_QUIT(0): The user left the current channel.
     *   USER_OFFLINE_DROPPED(1): The SDK timed out and the user dropped offline because no data
     *              packet was received within a certain period of time. If a user quits the
     *               call and the message is not passed to the SDK (due to an unreliable channel),
     *               the SDK assumes the user dropped offline.
     *   USER_OFFLINE_BECOME_AUDIENCE(2): (Live broadcast only.) The client role switched from
     *               the host to the audience.*/
    @Override
    public void onUserOffline(int uid, int reason)
    {
        Log.i(TAG, String.format("user %d offline! reason:%d", uid, reason));
        showLongToast(String.format("user %d offline! reason:%d", uid, reason));
        handler.post(new Runnable() {
            @Override
            public void run() {
                /**Clear render view
                 Note: The video will stay at its last frame, to completely remove it you will need to
                 remove the SurfaceView from its parent*/
                engine.setupRemoteVideo(new VideoCanvas(null, RENDER_MODE_HIDDEN, uid));
                remoteViews.get(uid).removeAllViews();
                remoteViews.remove(uid);
            }
        });
    }
    ...
};

完成,執行

拿兩部手機安裝編譯好的App,如果能看見兩個自己,說明你成功了。如果你在開發過程中遇到問題,可以訪問論壇提問與聲網工程師交流

https://rtcdeveloper.agora.io/

也可以訪問後臺獲取更進一步的技術支援

https://console.agora.io/

相關文章