全部產品
Search
文件中心

ApsaraVideo Live:Android實現音視訊通話

更新時間:Jun 08, 2026

本文檔將介紹如何在您的Android專案中整合 ARTC SDK, 快速實現一個簡單的即時音視頻互動App,適用於互動直播和視訊通話等情境。

功能簡介

在開始之前,瞭解以下幾個關鍵概念會很有協助:

  • ARTC SDK:這是阿里雲的即時音視頻產品,協助開發人員快速實現即時音視頻互動的SDK。

  • GRTN:阿里雲全球即時傳輸網路,提供超低延時、高音質、安全可靠的音視頻通訊服務。

  • 頻道:相當於一個虛擬房間,所有加入同一頻道的使用者都可以進行即時音視頻互動。

  • 主播:可在頻道內發布音視頻流,並可訂閱其他主播發布的音視頻流。

  • 觀眾:可在頻道內訂閱音視頻流,不能發布音視頻流。

  1. 使用者需要調用setChannelProfile(設定頻道情境),後調用joinChannel加入頻道:

    • 視訊通話情境:所有使用者都是主播角色,可以進行推流和拉流。

    • 互動直播情境:需要調用setClientRole(設定角色),在頻道內推流的使用者佈建主播角色;如果使用者只需要拉流,不需要推流,則設定觀眾角色。

  2. 加入頻道後,不同角色的使用者有不同的推拉流行為:

    • 所有加入頻道內的使用者都可以接收頻道內的音視頻流。

    • 主播角色可以在頻道內推音視頻流。

    • 觀眾如果需要推流,需要調用setClientRole方法,將使用者角色切換成主播,便可以推流。

樣本專案

阿里雲ARTC SDK提供了開源的即時音視頻互動樣本專案供客戶參考,您可以前往下載或查看樣本源碼。

前提條件

在運行樣本專案之前,請確保開發環境滿足以下要求:

  • 開發工具:Android Studio 2020.3.1 及以上版本。

  • 測試裝置:Android 5.0(SDK API Level 21)及以上版本的測試裝置。

    說明

    推薦使用真機測試,類比機可能存在功能缺失。

  • 網路環境:需要穩定的網路連接。

  • 應用準備:擷取即時音視頻應用的AppID和AppKey,詳情請參見建立應用。

建立專案(可選)

本節將介紹如何建立專案並為專案添加體驗音視頻互動必須的許可權。如果已有專案可跳過。

  1. 開啟 Android Studio,選擇 New Project。

  2. 選擇 Phone and Tablet 並選擇一個初始模板,在此以 Empty Views Activity 為例。

  1. 設定項目資訊,包含專案名、包名、專案儲存路徑、開發語言(在此以 Java 為例)、構建配置語言(在此以 Groovy DSL 為例)。

  1. 點擊 Finish 完成建立,等待專案同步完成。

設定項目

步驟一:匯入SDK

Maven自動整合(推薦)

  1. 開啟專案根目錄下的settings.gradle檔案,在dependencyResolutionManagement/repositories欄位中添加 ARTC SDK 所需的Maven地址,如下所示:

dependencyResolutionManagement {
    repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
    repositories {
        google()
        mavenCentral()
        // 添加ARTC SDK所在的Maven地址
        maven { url 'https://maven.aliyun.com/repository/google' }
        maven { url 'https://maven.aliyun.com/repository/public' }
    }
}

注意,如果您使用的 Android Gradle Plugin 版本低於 7.1.0,可能無法在settings.gradle檔案中找到對應欄位,相關資訊請參考Android Gradle 外掛程式 7.1。此時,請採用如下方案作為替換:

Android Gradle Plugin 版本低於 7.1.0方案

開啟專案根目錄下的build.gradle檔案,在allprojects/repositories中添加對應 Maven倉庫地址,如下所示:

allprojects {
    repositories {
        ...
        // 添加ARTC SDK所在的Maven地址
        maven { url 'https://maven.aliyun.com/repository/google' }
        maven { url 'https://maven.aliyun.com/repository/public' }
    }
}
  1. 開啟app/build.gradle檔案,在dependencies中添加對 ARTC SDK 的依賴,你可以在SDK下載中擷取版本資訊,並將${latest_version}替換為具體版本號碼,最新版本7.11.0。

dependencies {
    // 引入即時音視頻SDK依賴
    // ${latest_version}替換為具體版本號碼
    implementation 'com.aliyun.aio:AliVCSDK_ARTC:${latest_version}'
    // 7.4.0及以下版本需添加keep
    // implementation 'com.aliyun.aio.keep:keep:1.0.1'
}

如果使用的 Android Gradle Plugin 為 8.1 以上版本,Android Studio 推薦將依賴庫資訊遷移到版本目錄,相關資訊請參考遷移依賴專案到版本目錄。

下載SDK手動整合

  1. 在SDK下載中下載所需版本的 ARTC SDK aar 檔案,最新版本7.11.0,例如AliVCSDK_ARTC-x.y.z.aar。

  2. 將下載的 aar 檔案拷貝到您的專案目錄下,例如app/libs,如果沒有該檔案夾則建立。

  3. 開啟專案根目錄下的settings.gradle檔案,在dependencyResolutionManagement/repositories下添加 aar 所在檔案夾,如下所示:

dependencyResolutionManagement {
    repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
    repositories {
        google()
        mavenCentral()
        // 添加ARTC SDK所在位置的相對目錄
        flatDir {
            dir 'app/libs'
        }
    }
}

注意,如果您使用的 Android Gradle Plugin 版本低於 7.1.0,可能無法在settings.gradle檔案中找到對應欄位,相關資訊請參考Android Gradle 外掛程式 7.1。此時,請採用如下方案作為替換:

開啟專案根目錄下的build.gradle檔案,在allprojects/repositories中添加如下欄位:

allprojects {
    repositories {
        ...
        // 添加ARTC SDK所在位置的相對目錄
        flatDir {
            dir 'app/libs'
        }
    }
}
  1. 開啟app/build.gradle檔案,在dependencies下添加 aar 檔案的依賴,如下所示:

//x.y.z替換為對應版本號碼
implementation(name:'AliVCSDK_ARTC', version: 'x.y.z', ext:'aar')
  1. 構建後即可在 External Libraries 下產生對應依賴。

    image

步驟二:設定項目支援的 CPU 架構

開啟app/build.gradle檔案,在defaultConfig中指定專案支援的 CPU 架構,如下所示。可選架構包括armeabi-v7a、arm64-v8a、x86、x86_64,根據實際需要進行配置。

android {
    defaultConfig {
        // ...其他預設配置
        // 支援 armeabi-v7a 和 arm64-v8a 架構
        ndk {
             abiFilters "armeabi-v7a", "arm64-v8a"
        }
    }
}	

步驟三:設定許可權

根據實際需求設定應用所需的許可權,具體流程如下:

進入app/src/main目錄,開啟AndroidManifest.xml檔案,添加要求的權限。

<uses-feature android:name="android.hardware.camera" android:required="false" /> 
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.RECORD_AUDIO" />
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<!-- Request legacy Bluetooth permissions on older devices. -->
<uses-permission
  android:name="android.permission.BLUETOOTH"
  android:maxSdkVersion="30" />
<uses-permission
  android:name="android.permission.BLUETOOTH_ADMIN"
  android:maxSdkVersion="30" />

<!-- Needed only if your app communicates with already-paired Bluetooth devices. -->
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
<uses-permission android:name="android.permission.CHANGE_NETWORK_STATE" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />
<uses-permission android:name="android.permission.READ_PHONE_STATE" />
<uses-permission android:name="android.permission.WRITE_SETTINGS"
  tools:ignore="ProtectedPermissions" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />

藍芽許可權動態申請說明

ARTC SDK 內部 AndroidManifest.xml 中內建了部分許可權的聲明(對於動態許可權內部沒有申請的邏輯),其中 BLUETOOTH_CONNECT 許可權為 Android 12 開始引入的動態許可權。匯入 ARTC SDK 後下面的許可權會自動引入到 APK 中,但是仍推薦根據您的業務情境自行申請許可權。

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

根據您的業務情境,如果不需要下面的某項許可權,可以在您的主專案 AndroidManifest.xml 檔案中聲明移除:

<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" tools:node="remove"/>

特別針對藍芽許可權:

情境一: 需要藍芽功能

應用targetSdk <; 31

當您的專案 targetSdk <; 31 時,藍芽功能由低版本許可權 BLUETOOTH 控制。可以在您的主應用中聲明:

<!-- 聲明藍芽許可權 -->
<uses-permission android:name="android.permission.BLUETOOTH" />
<!-- 覆蓋移除高版本API 31+ 許可權 -->
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" tools:node="remove" />
說明
  • SDK 中會內建BLUETOOTH_CONNECT許可權的聲明,而BLUETOOTH_CONNECT是Android 12(API 31)引入的許可權,需要在運行時通過requestPermissions動態申請。

  • 部分裝置上應用如果包含BLUETOOTH_CONNECT的聲明則要求動態申請該許可權,否則可能出現SecurityException異常,如果出現此類問題:

    • 方式一:通過 tools:node="remove"移除該許可權聲明。

    • 方式二:動態申請該許可權。

應用targetSdk >;= 31

當您的專案 targetSdk >;= 31 時,則需要考慮低版本和高版本的藍芽許可權。可在您的主應用AndroidManifest.xml中聲明許可權:

<!-- 聲明藍芽許可權並設定android:maxSdkVersion為30以相容更低版本裝置 -->
<uses-permission android:name="android.permission.BLUETOOTH"/>
<!-- 聲明高版本API 31+ 藍芽許可權 -->
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT"/>
<!-- 其他藍芽許可權 -->

同時,由於BLUETOOTH_CONNECT為動態許可權,需要在運行時主動申請許可權:

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
    String[] permissions = {
        android.Manifest.permission.BLUETOOTH_CONNECT
    };
    ActivityCompat.requestPermissions(activity, permissions, REQUEST_BLUETOOTH);
}
情境二: 完全不需要藍芽功能

為了避免因藍芽許可權引發崩潰或彈窗提示,推薦移除不必要的藍芽許可權。

操作步驟:在主專案的AndroidManifest.xml中通過tools:node="remove"覆蓋移除許可權:

<!-- 覆蓋移除ARTC SDK中內建的藍芽許可權聲明 -->
<uses-permission android:name="android.permission.BLUETOOTH" tools:node="remove" />
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" tools:node="remove" />

其他許可權動態申請說明

Android 6.0(API 23)之後危險許可權需要動態申請,除了在AndroidManifest.xml檔案中靜態申請外,還需要在代碼運行時請求許可權。

其中部分許可權需要動態申請, 需要動態申請的許可權包含:

  • Manifest.permission.CAMERA

  • Manifest.permission.WRITE_EXTERNAL_STORAGE

  • Manifest.permission.RECORD_AUDIO

  • Manifest.permission.READ_EXTERNAL_STORAGE

  • Manifest.permission.READ_PHONE_STATE

當Android系統版本>;=12(API_LEVEL>;=31) 時, 需要額外動態申請以下許可權:

  • Manifest.permission.BLUETOOTH_CONNECT

可能涉及到的部分許可權說明如下:

許可權名

許可權說明

申請原因

必要性

是否動態許可權

CAMERA

網路攝影機許可權。

訪問裝置網路攝影機以採集視頻流。

是

Android >;= 6.0

RECORD_AUDIO

麥克風許可權。

訪問裝置麥克風以採集音頻流。

是

Android >;= 6.0

INTERNET

網路許可權。

音視頻資料通過網路傳輸(如 WebRTC等協議)。

是

否

ACCESS_NETWORK_STATE

允許應用擷取網路狀態。

監控網路連接狀態以最佳化音視頻傳輸品質,例如斷網重連。

按需

否

ACCESS_WIFI_STATE

允許應用擷取 WiFi 狀態。

擷取當前 WiFi 串連資訊以最佳化網路效能。

按需

否

MODIFY_AUDIO_SETTINGS

允許應用修改音頻配置。

調整系統音量、切換音訊輸出裝置等。

按需

否

BLUETOOTH

藍芽許可權(基礎功能)

串連藍牙裝置(如藍芽耳機)。

按需

否

BLUETOOTH_CONNECT

藍芽串連許可權

與已配對的藍牙裝置進行通訊(如傳輸音頻流)。

按需

android >;= 12

READ_PHONE_STATE

允許應用訪問與裝置電話狀態相關的資訊

根據電話狀態啟停音頻。

按需

android >;= 6.0

READ_EXTERNAL_STORAGE

允許應用讀取外部儲存中的檔案。

播放本地音樂等。

按需

android >;= 6.0

WRITE_EXTERNAL_STORAGE

允許應用寫入外部儲存。

儲存音視頻檔案、日誌等。

按需

android >;= 6.0

步驟四:防止混淆代碼(可選)

在app/proguard-rules.pro檔案中,為 SDK 配置規則,防止 SDK 對外提供的介面被混淆導致無法正常調用。

-keep class com.aliyun.allinone.** {
*;
}

-keep class com.aliyun.rts.network.AliHttpTool {
*;
}

-keep class com.aliyun.common.AlivcBase {
*;
}

-keep class com.huawei.multimedia.alivc.** {
*;
}

-keep class com.alivc.rtc.** {
*;
}

-keep class com.alivc.component.** {
*;
}

-keep class org.webrtc.** {
*;
}

常見問題

ARTC SDK 是否會引入額外許可權?

匯入 ARTC SDK 後,會自動聲明以下 Android 許可權(具體列表可能因 SDK 版本而異,請以所整合的 .aar 檔案中 AndroidManifest.xml 的內容為準):

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

這些許可權主要用於支援網路狀態檢測、Wi-Fi 資訊擷取、藍芽串連及音頻路由等功能。

如何處理 SDK 許可權與現有代碼的相容性或衝突問題?

儘管 ARTC SDK 內部已對許可權進行了 Android 版本適配(例如針對 BLUETOOTH_CONNECT 在 Android 12+ (API 31)的運行時許可權要求),通常不會導致編譯錯誤,但仍可能在以下情境中引發問題:

  • 運行時行為異常:如Manifest 合并失敗;

  • Google Play 審核風險:若應用實際未使用某項功能卻聲明了相關許可權(如藍芽),可能違反最小許可權原則,導致上架被拒。

建議採取以下措施進行相容性處理:

  1. 評估許可權必要性
    首先評估你的應用是否確實需要 SDK 聲明的所有許可權。若某些功能(如藍芽)未被使用,可考慮移除對應許可權聲明,例如您的程式僅在 Android API 30 及以下裝置上運行(targetSdk 版本為 30 以下時,可以移除BLUETOOTH_CONNECT許可權的聲明)。

  2. 通過 Manifest 合并指令解決衝突
    若多個模組對同一許可權聲明了不同屬性(例如 maxSdkVersion 不一致),可在主 AndroidManifest.xml 中使用 tools 命名空間進行幹預:

    <manifest xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:tools="http://schemas.android.com/tools">
        
        <!-- 樣本:移除 SDK 中聲明但應用不需要的許可權 -->
        <uses-permission 
            android:name="android.permission.BLUETOOTH_CONNECT"
            tools:node="remove" />
            
        <!-- 樣本:覆蓋許可權屬性 -->
        <uses-permission
            android:name="android.permission.BLUETOOTH"
            android:maxSdkVersion="30"
            tools:replace="android:maxSdkVersion" />
    </manifest>
    

步驟五:建立使用者介面

根據即時音視頻互動情境需要,建立相應的使用者介面。我們提供了一個以視訊通話情境為例,建立兩個視圖,分別用於展示本地視頻和遠端視頻的範例程式碼,作為開發中的參考。

使用者介面程式碼範例

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/video_chat_main"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".VideoCall.VideoCallActivity"
    >
    <LinearLayout
        android:id="@+id/ll_channel_layout"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintBottom_toTopOf="@id/ll_video_layout"
        android:orientation="vertical">

        <LinearLayout
            android:id="@+id/ll_channel_desc"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:orientation="horizontal"
            android:layout_marginTop="12dp"
            android:layout_marginLeft="8dp"
            android:layout_marginRight="12dp"
        >
            <TextView
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:text="@string/video_chat_channel_desc"
                />

        </LinearLayout>
        <LinearLayout
            android:id="@+id/ll_channel_id"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:orientation="horizontal"
            android:layout_marginTop="12dp"
            android:layout_marginLeft="8dp"
            android:layout_marginRight="12dp"
            app:layout_constraintTop_toTopOf="parent"
            app:layout_constraintLeft_toLeftOf="parent"
            app:layout_constraintRight_toRightOf="parent"
            android:visibility="visible">
            <TextView
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_weight="0"
                android:text="ChannelID:"
                android:layout_marginTop="5dp"
                />
            <EditText
                android:id="@+id/channel_id_input"
                android:layout_width="0dp"
                android:layout_height="wrap_content"
                android:layout_weight="1"
                android:text=""
                android:padding="5dp"
                android:textSize="15sp"
                android:layout_marginLeft="10dp"
                android:layout_marginTop="5dp"
                android:layout_marginRight="10dp"
                android:background="@drawable/edittext_border"
                />
        </LinearLayout>
        <LinearLayout
            android:id="@+id/ll_bottom_bar"
            android:layout_width="match_parent"
            android:layout_height="48dp"
            android:layout_marginTop="20dp"
            android:orientation="horizontal"
            android:gravity="center_vertical"
            app:layout_constraintLeft_toLeftOf="parent"
            app:layout_constraintRight_toRightOf="parent"
            app:layout_constraintTop_toBottomOf="@id/ll_channel_desc"
            app:layout_constraintBottom_toBottomOf="parent">
            <TextView
                android:id="@+id/join_room_btn"
                android:layout_width="0dp"
                android:layout_height="wrap_content"
                android:layout_weight="1"
                android:text="@string/video_chat_join_room"
                android:layout_marginStart="20dp"
                android:layout_marginEnd="20dp"
                android:gravity="center"
                android:padding="10dp"
                android:background="@color/layout_base_blue"
                />

        </LinearLayout>
    </LinearLayout>
    <LinearLayout
        android:id="@+id/ll_video_layout"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="vertical"
        app:layout_constraintTop_toBottomOf="@id/ll_channel_layout"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        android:layout_marginTop="10dp"
        android:layout_marginBottom="10dp"
        android:layout_marginLeft="10dp"
        android:layout_marginRight="10dp"
        >

        <LinearLayout
            android:id="@+id/video_layout_1"
            android:layout_width="match_parent"
            android:layout_height="0dp"
            android:layout_weight="0.5"
            android:orientation="horizontal">

            <FrameLayout
                android:id="@+id/fl_local"
                android:layout_width="108dp"
                android:layout_weight="0.5"
                android:layout_height="192dp"
                />
            <FrameLayout
                android:id="@+id/fl_remote"
                android:layout_marginLeft="5dp"
                android:layout_width="108dp"
                android:layout_weight="0.5"
                android:layout_height="192dp"
                />

        </LinearLayout>

        <LinearLayout
            android:id="@+id/video_layout_2"
            android:layout_width="match_parent"
            android:layout_height="0dp"
            android:layout_weight="0.5"
            android:layout_marginTop="10dp"
            android:orientation="horizontal">

            <FrameLayout
                android:id="@+id/fl_remote2"
                android:layout_width="108dp"
                android:layout_weight="0.5"
                android:layout_height="192dp"
                />
            <FrameLayout
                android:id="@+id/fl_remote3"
                android:layout_marginLeft="5dp"
                android:layout_width="108dp"
                android:layout_weight="0.5"
                android:layout_height="192dp"
                />

        </LinearLayout>
    </LinearLayout>
</androidx.constraintlayout.widget.ConstraintLayout>

實現步驟

本節介紹如何使用阿里雲 ARTC SDK 快速實現一個基礎的即時音視頻互動應用。你可以先將完整範例程式碼複製到專案,快速體驗功能,再通過以下步驟瞭解核心 API 的調用。

下圖展示了實現音視頻互動的基本流程:

下面是一段實現音視訊通話基本流程的完整參考代碼:

基本流程程式碼範例

/**
 * 音視訊通話情境API調用樣本
 */
public class VideoCallActivity extends AppCompatActivity {

    private Handler handler;
    private EditText mChannelEditText;
    private TextView mJoinChannelTextView;
    private boolean hasJoined = false;
    private FrameLayout fl_local, fl_remote, fl_remote_2, fl_remote_3;

    private AliRtcEngine mAliRtcEngine = null;
    private AliRtcEngine.AliRtcVideoCanvas mLocalVideoCanvas = null;
    private Map<String, ViewGroup> remoteViews = new ConcurrentHashMap<String, ViewGroup>();

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        handler = new Handler(Looper.getMainLooper());
        EdgeToEdge.enable(this);
        setContentView(R.layout.activity_video_chat);
        ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.video_chat_main), (v, insets) -> {
            Insets systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars());
            v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom);
            return insets;
        });
        setTitle(getString(R.string.video_chat));
        getSupportActionBar().setDisplayHomeAsUpEnabled(true);

        fl_local = findViewById(R.id.fl_local);
        fl_remote = findViewById(R.id.fl_remote);
        fl_remote_2 = findViewById(R.id.fl_remote2);
        fl_remote_3 = findViewById(R.id.fl_remote3);

        mChannelEditText = findViewById(R.id.channel_id_input);
        mChannelEditText.setText(GlobalConfig.getInstance().gerRandomChannelId());
        mJoinChannelTextView = findViewById(R.id.join_room_btn);
        mJoinChannelTextView.setOnClickListener(v -> {
            if(hasJoined) {
                destroyRtcEngine();
                mJoinChannelTextView.setText(R.string.video_chat_join_room);
            } else {
                startRTCCall();
            }
        });
    }

    public static void startActionActivity(Activity activity) {
        Intent intent = new Intent(activity, VideoCallActivity.class);
        activity.startActivity(intent);
    }

    @Override
    public boolean onOptionsItemSelected(MenuItem item) {
        if (item.getItemId() == android.R.id.home) {
            // 點擊返回按鈕時的操作
            destroyRtcEngine();
            finish();
            return true;
        }
        return super.onOptionsItemSelected(item);
    }

    private FrameLayout getAvailableView() {
        if (fl_remote.getChildCount() == 0) {
            return fl_remote;
        } else if (fl_remote_2.getChildCount() == 0) {
            return fl_remote_2;
        } else if (fl_remote_3.getChildCount() == 0) {
            return fl_remote_3;
        } else {
            return null;
        }
    }

    private void handleJoinResult(int result, String channel, String userId) {
        handler.post(() -> {
            String  str = null;
            if(result == 0) {
                str = "User " + userId + " Join " + channel + " Success";
            } else {
                str = "User " + userId + " Join " + channel + " Failed!, error:" + result;
            }
            ToastHelper.showToast(this, str, Toast.LENGTH_SHORT);
            ((TextView)findViewById(R.id.join_room_btn)).setText(R.string.leave_channel);
        });
    }

    private void startRTCCall() {
        if(hasJoined) {
            return;
        }
        initAndSetupRtcEngine();
        startPreview();
        joinChannel();
    }

    private void initAndSetupRtcEngine() {

        //建立並初始化引擎
        if(mAliRtcEngine == null) {
            mAliRtcEngine = AliRtcEngine.getInstance(this);
        }
        mAliRtcEngine.setRtcEngineEventListener(mRtcEngineEventListener);
        mAliRtcEngine.setRtcEngineNotify(mRtcEngineNotify);

        // 設定頻道模式為互動模式,RTC下都使用AliRTCSdkInteractiveLive
        mAliRtcEngine.setChannelProfile(AliRtcEngine.AliRTCSdkChannelProfile.AliRTCSdkInteractiveLive);
        // 設定使用者角色,既需要推流也需要拉流使用AliRTCSdkInteractive, 只拉流不推流使用AliRTCSdkLive
        mAliRtcEngine.setClientRole(AliRtcEngine.AliRTCSdkClientRole.AliRTCSdkInteractive);
        //設定音頻Profile,預設使用高音質模式AliRtcEngineHighQualityMode及音樂模式AliRtcSceneMusicMode
        mAliRtcEngine.setAudioProfile(AliRtcEngine.AliRtcAudioProfile.AliRtcEngineHighQualityMode, AliRtcEngine.AliRtcAudioScenario.AliRtcSceneMusicMode);
        mAliRtcEngine.setCapturePipelineScaleMode(AliRtcEngine.AliRtcCapturePipelineScaleMode.AliRtcCapturePipelineScaleModePost);

        //設定視頻編碼參數
        AliRtcEngine.AliRtcVideoEncoderConfiguration aliRtcVideoEncoderConfiguration = new AliRtcEngine.AliRtcVideoEncoderConfiguration();
        aliRtcVideoEncoderConfiguration.dimensions = new AliRtcEngine.AliRtcVideoDimensions(
                720, 1280);
        aliRtcVideoEncoderConfiguration.frameRate = 20;
        aliRtcVideoEncoderConfiguration.bitrate = 1200;
        aliRtcVideoEncoderConfiguration.keyFrameInterval = 2000;
        aliRtcVideoEncoderConfiguration.orientationMode = AliRtcVideoEncoderOrientationModeAdaptive;
        mAliRtcEngine.setVideoEncoderConfiguration(aliRtcVideoEncoderConfiguration);

        //SDK預設會publish音頻,publishLocalAudioStream可以不調用
        mAliRtcEngine.publishLocalAudioStream(true);
        //如果是視訊通話,publishLocalVideoStream(true)可以不調用,SDK預設會publish視頻
        //如果是純語音通話 則需要設定publishLocalVideoStream(false)設定不publish視頻
        mAliRtcEngine.publishLocalVideoStream(true);

        //設定預設訂閱遠端的音頻和視頻流
        mAliRtcEngine.setDefaultSubscribeAllRemoteAudioStreams(true);
        mAliRtcEngine.subscribeAllRemoteAudioStreams(true);
        mAliRtcEngine.setDefaultSubscribeAllRemoteVideoStreams(true);
        mAliRtcEngine.subscribeAllRemoteVideoStreams(true);

    }

    private void startPreview(){
        if (mAliRtcEngine != null) {

            if (fl_local.getChildCount() > 0) {
                fl_local.removeAllViews();
            }

            findViewById(R.id.ll_video_layout).setVisibility(VISIBLE);
            ViewGroup.LayoutParams layoutParams = new FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT);
            if(mLocalVideoCanvas == null) {
                mLocalVideoCanvas = new AliRtcEngine.AliRtcVideoCanvas();
                SurfaceView localSurfaceView = mAliRtcEngine.createRenderSurfaceView(VideoCallActivity.this);
                localSurfaceView.setZOrderOnTop(true);
                localSurfaceView.setZOrderMediaOverlay(true);
                fl_local.addView(localSurfaceView, layoutParams);
                mLocalVideoCanvas.view = localSurfaceView;
                mAliRtcEngine.setLocalViewConfig(mLocalVideoCanvas, AliRtcVideoTrackCamera);
                mAliRtcEngine.startPreview();
            }
        }
    }

    private void joinChannel() {
        String channelId = mChannelEditText.getText().toString();
        if(!TextUtils.isEmpty(channelId)) {
            String userId = GlobalConfig.getInstance().getUserId();
            String appId = ARTCTokenHelper.AppId;
            String appKey = ARTCTokenHelper.AppKey;
            long timestamp = ARTCTokenHelper.getTimesTamp();
            String token = ARTCTokenHelper.generateSingleParameterToken(appId, appKey, channelId, userId, timestamp);
            mAliRtcEngine.joinChannel(token, null, null, null);
            hasJoined = true;
        } else {
            Log.e("VideoCallActivity", "channelId is empty");
        }
    }

    private AliRtcEngineEventListener mRtcEngineEventListener = new AliRtcEngineEventListener() {
        @Override
        public void onJoinChannelResult(int result, String channel, String userId, int elapsed) {
            super.onJoinChannelResult(result, channel, userId, elapsed);
            handleJoinResult(result, channel, userId);
        }

        @Override
        public void onLeaveChannelResult(int result, AliRtcEngine.AliRtcStats stats){
            super.onLeaveChannelResult(result, stats);
        }

        @Override
        public void onConnectionStatusChange(AliRtcEngine.AliRtcConnectionStatus status, AliRtcEngine.AliRtcConnectionStatusChangeReason reason){
            super.onConnectionStatusChange(status, reason);

            handler.post(new Runnable() {
                @Override
                public void run() {
                    if(status == AliRtcEngine.AliRtcConnectionStatus.AliRtcConnectionStatusFailed) {
                        /* TODO: 務必處理;建議業務提示客戶,此時SDK內部已經嘗試了各種恢複策略已經無法繼續使用時才會上報 */
                        ToastHelper.showToast(VideoCallActivity.this, R.string.video_chat_connection_failed, Toast.LENGTH_SHORT);
                    } else {
                        /* TODO: 可選處理;增加業務代碼,一般用於資料統計、UI變化 */
                    }
                }
            });
        }
        @Override
        public void OnLocalDeviceException(AliRtcEngine.AliRtcEngineLocalDeviceType deviceType, AliRtcEngine.AliRtcEngineLocalDeviceExceptionType exceptionType, String msg){
            super.OnLocalDeviceException(deviceType, exceptionType, msg);
            /* TODO: 務必處理;建議業務提示裝置錯誤,此時SDK內部已經嘗試了各種恢複策略已經無法繼續使用時才會上報 */
            handler.post(new Runnable() {
                @Override
                public void run() {
                    String str = "OnLocalDeviceException deviceType: " + deviceType + " exceptionType: " + exceptionType + " msg: " + msg;
                    ToastHelper.showToast(VideoCallActivity.this, str, Toast.LENGTH_SHORT);
                }
            });
        }

    };

    private AliRtcEngineNotify mRtcEngineNotify = new AliRtcEngineNotify() {
        @Override
        public void onAuthInfoWillExpire() {
            super.onAuthInfoWillExpire();
            /* TODO: 務必處理;Token即將到期,需要業務觸發重新擷取當前channel,user的鑒權資訊,然後設定refreshAuthInfo即可 */
        }

        @Override
        public void onRemoteUserOnLineNotify(String uid, int elapsed){
            super.onRemoteUserOnLineNotify(uid, elapsed);
        }

        //在onRemoteUserOffLineNotify回調中解除遠端視頻流渲染控制項的設定
        @Override
        public void onRemoteUserOffLineNotify(String uid, AliRtcEngine.AliRtcUserOfflineReason reason){
            super.onRemoteUserOffLineNotify(uid, reason);
        }

        //在onRemoteTrackAvailableNotify回調中設定遠端視頻流渲染控制項
        @Override
        public void onRemoteTrackAvailableNotify(String uid, AliRtcEngine.AliRtcAudioTrack audioTrack, AliRtcEngine.AliRtcVideoTrack videoTrack){
            handler.post(new Runnable() {
                @Override
                public void run() {
                    if(videoTrack == AliRtcVideoTrackCamera) {
                        SurfaceView surfaceView = mAliRtcEngine.createRenderSurfaceView(VideoCallActivity.this);
                        surfaceView.setZOrderMediaOverlay(true);
                        FrameLayout view = getAvailableView();
                        if (view == null) {
                            return;
                        }
                        remoteViews.put(uid, view);
                        view.addView(surfaceView, new FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT));
                        AliRtcEngine.AliRtcVideoCanvas remoteVideoCanvas = new AliRtcEngine.AliRtcVideoCanvas();
                        remoteVideoCanvas.view = surfaceView;
                        mAliRtcEngine.setRemoteViewConfig(remoteVideoCanvas, uid, AliRtcVideoTrackCamera);
                    } else if(videoTrack == AliRtcVideoTrackNo) {
                        if(remoteViews.containsKey(uid)) {
                            ViewGroup view = remoteViews.get(uid);
                            if(view != null) {
                                view.removeAllViews();
                                remoteViews.remove(uid);
                                mAliRtcEngine.setRemoteViewConfig(null, uid, AliRtcVideoTrackCamera);
                            }
                        }
                    }
                }
            });
        }

        /* 業務可能會觸發同一個UserID的不同裝置搶佔的情況,所以這個地方也需要處理 */
        @Override
        public void onBye(int code){
            handler.post(new Runnable() {
                @Override
                public void run() {
                    String msg = "onBye code:" + code;
                    ToastHelper.showToast(VideoCallActivity.this, msg, Toast.LENGTH_SHORT);
                }
            });
        }

    };

    private void destroyRtcEngine() {
        if( mAliRtcEngine != null) {
            mAliRtcEngine.stopPreview();
            mAliRtcEngine.setLocalViewConfig(null, AliRtcVideoTrackCamera);
            mAliRtcEngine.leaveChannel();
            mAliRtcEngine.destroy();
            mAliRtcEngine = null;

            handler.post(() -> {
                ToastHelper.showToast(this, "Leave Channel", Toast.LENGTH_SHORT);
            });
        }
        hasJoined = false;
        for (ViewGroup value : remoteViews.values()) {
            value.removeAllViews();
        }
        remoteViews.clear();
        findViewById(R.id.ll_video_layout).setVisibility(View.GONE);
        fl_local.removeAllViews();
        mLocalVideoCanvas = null;
    }
}

完整範例程式碼的詳情與運行請參見:跑通Android Demo樣本。

1、申請許可權請求

進入音視訊通話時,檢查是否已在App中授予了所需要的許可權:

private static final int REQUEST_PERMISSION_CODE = 101;

private static final String[] PERMISSION_MANIFEST = {
    Manifest.permission.RECORD_AUDIO,
    Manifest.permission.READ_PHONE_STATE,
    Manifest.permission.WRITE_EXTERNAL_STORAGE,
    Manifest.permission.READ_EXTERNAL_STORAGE,
    Manifest.permission.CAMERA
};

private static final String[] PERMISSION_MANIFEST33 = {
    Manifest.permission.RECORD_AUDIO,
    Manifest.permission.READ_PHONE_STATE,
    Manifest.permission.CAMERA
};

private static String[] getPermissions() {
    if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) {
        return PERMISSION_MANIFEST;
    }
    return PERMISSION_MANIFEST33;
}

public boolean checkOrRequestPermission() {
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
        if (ContextCompat.checkSelfPermission(this, "android.permission.CAMERA") != PackageManager.PERMISSION_GRANTED
                || ContextCompat.checkSelfPermission(this, "android.permission.RECORD_AUDIO") != PackageManager.PERMISSION_GRANTED) {
            requestPermissions(getPermissions(), REQUEST_PERMISSION_CODE);
            return false;
        }
    }
    return true;
}

2、鑒權Token

加入ARTC頻道需要一個鑒權Token,用於鑒權使用者的合法身份,其鑒權Token建置規則詳情請參見:Token鑒權。Token 產生有兩種方式:單參數方式和多參數方式,不同的Token產生方式需要調用SDK不同的加入頻道(joinChannel)的介面。

上線發布階段:

由於Token的產生需要使用AppKey,寫死在用戶端存在泄露的風險,因此強烈建議線上業務通過業務Server產生下發給用戶端。

開發調試階段:

開發調試階段,如果業務Server還沒有產生Token的邏輯,可以暫時參考APIExample上的Token產生邏輯,產生臨時Token,其參考代碼如下:

public final class ARTCTokenHelper {
    /**
     * RTC AppId
     */
    public static String AppId = "";

    /**
     * RTC AppKey
     */
    public static String AppKey = "";

    /**
     * 根據channelId,userId, timestamp, nonce 產生單參數入會 的token
     * Generate a single-parameter meeting token based on channelId, userId, and nonce
     */
    public static String generateSingleParameterToken(String appId, String appKey, String channelId, String userId, long timestamp,  String nonce) {

        StringBuilder stringBuilder = new StringBuilder()
                .append(appId)
                .append(appKey)
                .append(channelId)
                .append(userId)
                .append(timestamp);
        String token =  getSHA256(stringBuilder.toString());
        try{
            JSONObject tokenJson = new JSONObject();
            tokenJson.put("appid", AppId);
            tokenJson.put("channelid", channelId);
            tokenJson.put("userid", userId);
            tokenJson.put("nonce", nonce);
            tokenJson.put("timestamp", timestamp);
            tokenJson.put("token", token);
            String base64Token = Base64.encodeToString(tokenJson.toString().getBytes(StandardCharsets.UTF_8), Base64.NO_WRAP);
            return base64Token;
        }catch (Exception e) {
            e.printStackTrace();
        }
        return null;
    }

    /**
     * 根據channelId,userId, timestamp 產生單參數入會 的token
     * Generate a single-parameter meeting token based on channelId, userId, and timestamp
     */
    public static String generateSingleParameterToken(String appId, String appKey, String channelId, String userId, long timestamp) {
        return generateSingleParameterToken(appId, appKey, channelId, userId, timestamp, "");
    }

    public static String getSHA256(String str) {
        try {
            MessageDigest messageDigest = MessageDigest.getInstance("SHA-256");
            byte[] hash = messageDigest.digest(str.getBytes(StandardCharsets.UTF_8));
            return byte2Hex(hash);
        } catch (NoSuchAlgorithmException e) {
            // Consider logging the exception and/or re-throwing as a RuntimeException
            e.printStackTrace();
        }
        return "";
    }

    private static String byte2Hex(byte[] bytes) {
        StringBuilder stringBuilder = new StringBuilder();
        for (byte b : bytes) {
            String hex = Integer.toHexString(0xff & b);
            if (hex.length() == 1) {
                // Use single quote for char
                stringBuilder.append('0');
            }
            stringBuilder.append(hex);
        }
        return stringBuilder.toString();
    }

    public static long getTimesTamp() {
        return System.currentTimeMillis() / 1000 + 60 * 60 * 24;
    }
}

3、匯入ARTC SDK 相關類

匯入 ARTC SDK 相關的類和介面:

// 匯入ARTC相關類
import com.alivc.rtc.AliRtcEngine;
import com.alivc.rtc.AliRtcEngineEventListener;
import com.alivc.rtc.AliRtcEngineNotify;

4、建立並初始化引擎

  • 建立RTC引擎

    調用getInstance[1/2]介面建立引擎AliRTCEngine

    private AliRtcEngine mAliRtcEngine = null;
    if(mAliRtcEngine == null) {
        mAliRtcEngine = AliRtcEngine.getInstance(this);
    }
  • 初始化引擎

    • 調用setChannelProfile設定頻道為AliRTCSdkInteractiveLive(互動模式)。

      根據具體的業務需求,可以選擇適用於互動娛樂情境的互動模式,或者適合一對一或一對多廣播的通訊模式。正確的模式選擇能夠確保使用者體驗的流暢性並有效利用網路資源。您可以根據業務情境選擇合適的模式。

      模式

      推流

      拉流

      模式介紹

      互動模式

      1. 有角色限制,只有被賦予主播身份的使用者可以進行推流操作。

      2. 在整個過程中,參與者可以靈活地切換角色。

      無角色限制,所有參與者都擁有拉流的許可權。

      1. 在互動模式中,主播加入或退出會議、以及開始推送直播流的事件都會即時通知給觀眾端,確保觀眾能夠及時瞭解主播的動態。反之,觀眾的任何活動不會通告給主播,保持了主播的直播流程不受幹擾。

      2. 在互動模式下,主播角色負責進行直播互動,而觀眾角色則主要接收內容,通常不參與直播的互動過程。若業務需求未來可能發生變化,導致不確定是否需要支援觀眾的互動參與,建議預設採用互動模式。這種模式具有較高的靈活性,可通過調整使用者角色許可權來適應不同的互動需求。

      通訊模式

      無角色限制,所有參與者都擁有推流許可權。

      無角色限制,所有參與者都擁有拉流的許可權。

      1. 在通訊模式下,會議參與者能夠相互察覺到彼此的存在。

      2. 該模式雖然沒有區分使用者角色,但實際上與互動模式中的主播角色相對應;目的是為了簡化操作,讓使用者能夠通過調用更少的API來實現所需的功能。

    • 調用setClientRole設定使用者角色為AliRTCSdkInteractive(主播)或者AliRTCSdkLive(觀眾)。注意:主播角色預設推拉流,觀眾角色預設關閉預覽和推流,只拉流。

      說明

      當使用者在頻道內切換角色時,系統會相應調整音視頻流的推流狀態:

      • 從主播切換為觀眾(“下麥”):系統將停止推送本地音視頻流,但已訂閱的遠端流不受影響,使用者仍可繼續觀看其他人的音視頻。

      • 從觀眾切換為主播(“上麥”):系統將開始推送本地音視頻流,同時已訂閱的遠端流保持不變,使用者可以繼續觀看其他參與者的內容。

      // 設定頻道模式為互動模式,RTC下都使用AliRTCSdkInteractiveLive
      mAliRtcEngine.setChannelProfile(AliRtcEngine.AliRTCSdkChannelProfile.AliRTCSdkInteractiveLive);
      // 設定使用者角色,既需要推流也需要拉流使用AliRTCSdkInteractive, 只拉流不推流使用AliRTCSdkLive
      mAliRtcEngine.setClientRole(AliRtcEngine.AliRTCSdkClientRole.AliRTCSdkInteractive);
  • 設定常用的回調

    SDK 在運行過程中如遇到異常情況,會優先嘗試內部重試機制以自動回復。對於無法自行解決的錯誤,SDK 會通過預定義的回調介面通知您的應用程式。

    以下是一些 SDK 無法處理、需由應用程式層監聽和響應的關鍵回調:

    異常發生原因

    回調及參數

    解決方案

    說明

    鑒權失敗

    onJoinChannelResult回調中的result返回AliRtcErrJoinBadToken

    發生錯誤時App需要檢查Token是否正確。

    在使用者主動調用API時,若鑒權失敗,系統將在調用API的回調中返回鑒權失敗的錯誤資訊。

    鑒權將要到期

    onWillAuthInfoExpire

    發生該異常時App需要重新擷取最新的鑒權資訊後,再調用refreshAuthInfo重新整理鑒權資訊。

    鑒權到期錯誤在兩種情況下出現:使用者調用API或程式執行期間。因此,錯誤反饋將通過API回調或通過獨立的錯誤回調通知。

    鑒權到期

    onAuthInfoExpired

    發生該異常時App需要重新入會。

    鑒權到期錯誤在兩種情況下出現:使用者調用API或程式執行期間。因此,錯誤反饋將通過API回調或通過獨立的錯誤回調通知。

    網路連接異常

    onConnectionStatusChange回調返回AliRtcConnectionStatusFailed。

    發生該異常時APP需要重新入會。

    SDK具備一定時間斷網自動回復能力,但若斷線時間超出預設閾值,會觸發逾時並中斷連線。此時,App應檢查網路狀態並指導使用者重新加入會議。

    被踢下線

    onBye

    • AliRtcOnByeUserReplaced:當發生該異常時排查使用者userid是否相同。

    • AliRtcOnByeBeKickedOut:當發生該異常時,表示被業務踢下線,需要重新入會。

    • AliRtcOnByeChannelTerminated:當發生該異常時,表示房間被銷毀,需要重新入會。

    RTC服務提供了管理員可以主動移除參與者的功能。

    本地裝置異常

    onLocalDeviceException

    發生該異常時App需要檢測許可權、裝置硬體是否正常。

    RTC服務支援裝置檢測和異常診斷的能力;當本地裝置發生異常時,RTC服務會通過回調的方式通知客戶本地裝置異常,此時,若SDK無法自行解決問題,則App需要介入以查看裝置是否正常。

    private AliRtcEngineEventListener mRtcEngineEventListener = new AliRtcEngineEventListener() {
        @Override
        public void onJoinChannelResult(int result, String channel, String userId, int elapsed) {
            super.onJoinChannelResult(result, channel, userId, elapsed);
            handleJoinResult(result, channel, userId);
        }
    
        @Override
        public void onLeaveChannelResult(int result, AliRtcEngine.AliRtcStats stats){
            super.onLeaveChannelResult(result, stats);
        }
    
        @Override
        public void onConnectionStatusChange(AliRtcEngine.AliRtcConnectionStatus status, AliRtcEngine.AliRtcConnectionStatusChangeReason reason){
            super.onConnectionStatusChange(status, reason);
    
            handler.post(new Runnable() {
                @Override
                public void run() {
                    if(status == AliRtcEngine.AliRtcConnectionStatus.AliRtcConnectionStatusFailed) {
                        /* TODO: 務必處理;建議業務提示客戶,此時SDK內部已經嘗試了各種恢複策略已經無法繼續使用時才會上報 */
                        ToastHelper.showToast(VideoChatActivity.this, R.string.video_chat_connection_failed, Toast.LENGTH_SHORT);
                    } else {
                        /* TODO: 可選處理;增加業務代碼,一般用於資料統計、UI變化 */
                    }
                }
            });
        }
        @Override
        public void OnLocalDeviceException(AliRtcEngine.AliRtcEngineLocalDeviceType deviceType, AliRtcEngine.AliRtcEngineLocalDeviceExceptionType exceptionType, String msg){
            super.OnLocalDeviceException(deviceType, exceptionType, msg);
            /* TODO: 務必處理;建議業務提示裝置錯誤,此時SDK內部已經嘗試了各種恢複策略已經無法繼續使用時才會上報 */
            handler.post(new Runnable() {
                @Override
                public void run() {
                    String str = "OnLocalDeviceException deviceType: " + deviceType + " exceptionType: " + exceptionType + " msg: " + msg;
                    ToastHelper.showToast(VideoChatActivity.this, str, Toast.LENGTH_SHORT);
                }
            });
        }
    
    };
    
    private AliRtcEngineNotify mRtcEngineNotify = new AliRtcEngineNotify() {
        @Override
        public void onAuthInfoWillExpire() {
            super.onAuthInfoWillExpire();
            /* TODO: 務必處理;Token即將到期,需要業務觸發重新擷取當前channel,user的鑒權資訊,然後設定refreshAuthInfo即可 */
        }
    
        @Override
        public void onRemoteUserOnLineNotify(String uid, int elapsed){
            super.onRemoteUserOnLineNotify(uid, elapsed);
        }
    
        //在onRemoteUserOffLineNotify回調中解除遠端視頻流渲染控制項的設定
        @Override
        public void onRemoteUserOffLineNotify(String uid, AliRtcEngine.AliRtcUserOfflineReason reason){
            super.onRemoteUserOffLineNotify(uid, reason);
        }
    
        //在onRemoteTrackAvailableNotify回調中設定遠端視頻流渲染控制項
        @Override
        public void onRemoteTrackAvailableNotify(String uid, AliRtcEngine.AliRtcAudioTrack audioTrack, AliRtcEngine.AliRtcVideoTrack videoTrack){
            handler.post(new Runnable() {
                @Override
                public void run() {
                    if(videoTrack == AliRtcVideoTrackCamera) {
                        SurfaceView surfaceView = mAliRtcEngine.createRenderSurfaceView(VideoChatActivity.this);
                        surfaceView.setZOrderMediaOverlay(true);
                        FrameLayout view = getAvailableView();
                        if (view == null) {
                            return;
                        }
                        remoteViews.put(uid, view);
                        view.addView(surfaceView, new FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT));
                        AliRtcEngine.AliRtcVideoCanvas remoteVideoCanvas = new AliRtcEngine.AliRtcVideoCanvas();
                        remoteVideoCanvas.view = surfaceView;
                        mAliRtcEngine.setRemoteViewConfig(remoteVideoCanvas, uid, AliRtcVideoTrackCamera);
                    } else if(videoTrack == AliRtcVideoTrackNo) {
                        if(remoteViews.containsKey(uid)) {
                            ViewGroup view = remoteViews.get(uid);
                            if(view != null) {
                                view.removeAllViews();
                                remoteViews.remove(uid);
                                mAliRtcEngine.setRemoteViewConfig(null, uid, AliRtcVideoTrackCamera);
                            }
                        }
                    }
                }
            });
        }
    
        /* 業務可能會觸發同一個UserID的不同裝置搶佔的情況,所以這個地方也需要處理 */
        @Override
        public void onBye(int code){
            handler.post(new Runnable() {
                @Override
                public void run() {
                    String msg = "onBye code:" + code;
                    ToastHelper.showToast(VideoChatActivity.this, msg, Toast.LENGTH_SHORT);
                }
            });
        }
    };
    
    mAliRtcEngine.setRtcEngineEventListener(mRtcEngineEventListener);
    mAliRtcEngine.setRtcEngineNotify(mRtcEngineNotify);

5、設定音視頻屬性

  • 設定音頻相關屬性

    調用setAudioProfile設定音訊編碼模式和音頻情境

    mAliRtcEngine.setAudioProfile(AliRtcEngine.AliRtcAudioProfile.AliRtcEngineHighQualityMode, AliRtcEngine.AliRtcAudioScenario.AliRtcSceneMusicMode);
  • 設定視頻相關屬性

    可以設定推出去的視頻流的解析度、碼率、幀率等資訊。

    //設定視頻編碼參數
    AliRtcEngine.AliRtcVideoEncoderConfiguration aliRtcVideoEncoderConfiguration = new AliRtcEngine.AliRtcVideoEncoderConfiguration();
    aliRtcVideoEncoderConfiguration.dimensions = new AliRtcEngine.AliRtcVideoDimensions(
                    720, 1280);
    aliRtcVideoEncoderConfiguration.frameRate = 20;
    aliRtcVideoEncoderConfiguration.bitrate = 1200;
    aliRtcVideoEncoderConfiguration.keyFrameInterval = 2000;
    aliRtcVideoEncoderConfiguration.orientationMode = AliRtcVideoEncoderOrientationModeAdaptive;
    mAliRtcEngine.setVideoEncoderConfiguration(aliRtcVideoEncoderConfiguration);

6、設定推拉流屬性

設定推送音視頻流及預設拉所有使用者的流:

  • 調用publishLocalAudioStream推送音頻流

  • 調用publishLocalVideoStream推送視頻流,如果是語音通話,可以設定成false

//SDK預設會publish音頻,publishLocalAudioStream可以不調用
mAliRtcEngine.publishLocalAudioStream(true);
//如果是視訊通話,publishLocalVideoStream(true)可以不調用,SDK預設會publish視頻
//如果是純語音通話 則需要設定publishLocalVideoStream(false)設定不publish視頻
mAliRtcEngine.publishLocalVideoStream(true);

//設定預設訂閱遠端的音頻和視頻流
mAliRtcEngine.setDefaultSubscribeAllRemoteAudioStreams(true);
mAliRtcEngine.subscribeAllRemoteAudioStreams(true);
mAliRtcEngine.setDefaultSubscribeAllRemoteVideoStreams(true);
mAliRtcEngine.subscribeAllRemoteVideoStreams(true);
說明

SDK預設是自動推拉流模式,預設會推送音視頻流及訂閱頻道內所有使用者的音視頻流,可以通過調用上面的介面關閉自動推拉流模式。

7、開啟本地預覽

  • 調用setLocalViewConfig設定本地渲染視圖,同時設定本地的視頻顯示內容。

  • 調用startPreview 方法,開啟本地視頻預覽

mLocalVideoCanvas = new AliRtcEngine.AliRtcVideoCanvas();
SurfaceView localSurfaceView = mAliRtcEngine.createRenderSurfaceView(VideoChatActivity.this);
localSurfaceView.setZOrderOnTop(true);
localSurfaceView.setZOrderMediaOverlay(true);
FrameLayout fl_local = findViewById(R.id.fl_local);
fl_local.addView(localSurfaceView, layoutParams);
mLocalVideoCanvas.view = localSurfaceView;
mAliRtcEngine.setLocalViewConfig(mLocalVideoCanvas, AliRtcVideoTrackCamera);
mAliRtcEngine.startPreview();

8、加入頻道

調用joinChannel加入頻道,如果token是單參數規則產生的,需要調用SDK單參數的joinChannel[1/3]介面,如果是多參數規則產生的,需要調用SDK多參數的joinChannel[2/3]介面。調用完加入頻道後,可以在onJoinChannelResult回調中拿到加入頻道結果,如果result為0,則表示加入頻道成功,否則需要檢查傳進來的Token是否非法。

 mAliRtcEngine.joinChannel(token, null, null, null);
說明
  • 入會後會按照入會前設定的參數執行相應的推流和拉流。

  • SDK預設會自動推拉流,以減少用戶端需要調用的API數量。

9、設定遠端視圖

在初始化引擎的時候設定對應回調mAliRtcEngine.setRtcEngineNotify,需要在onRemoteTrackAvailableNotify回調中,為遠端使用者佈建遠端視圖,範例程式碼如下:

@Override
public void onRemoteTrackAvailableNotify(String uid, AliRtcEngine.AliRtcAudioTrack audioTrack, AliRtcEngine.AliRtcVideoTrack videoTrack){
    handler.post(new Runnable() {
        @Override
        public void run() {
            if(videoTrack == AliRtcVideoTrackCamera) {
                SurfaceView surfaceView = mAliRtcEngine.createRenderSurfaceView(VideoChatActivity.this);
                surfaceView.setZOrderMediaOverlay(true);
                FrameLayout fl_remote = findViewById(R.id.fl_remote);
                if (fl_remote == null) {
                    return;
                }
                fl_remote.addView(surfaceView, new FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT));
                AliRtcEngine.AliRtcVideoCanvas remoteVideoCanvas = new AliRtcEngine.AliRtcVideoCanvas();
                remoteVideoCanvas.view = surfaceView;
                mAliRtcEngine.setRemoteViewConfig(remoteVideoCanvas, uid, AliRtcVideoTrackCamera);
            } else if(videoTrack == AliRtcVideoTrackNo) {
                FrameLayout fl_remote = findViewById(R.id.fl_remote);
                fl_remote.removeAllViews();
                mAliRtcEngine.setRemoteViewConfig(null, uid, AliRtcVideoTrackCamera);
            }
        }
    });
}

10、離開房間並銷毀引擎

音視頻互動結束,需要離開房間並銷毀引擎,按照下列步驟結束音視頻互動

  1. 調用 stopPreview 停止視頻預覽。

  2. 調用leaveChannel離會。

  3. 調用destroy銷毀引擎,並釋放相關資源。

private void destroyRtcEngine() {
    mAliRtcEngine.stopPreview();
    mAliRtcEngine.setLocalViewConfig(null, AliRtcVideoTrackCamera);
    mAliRtcEngine.leaveChannel();
    mAliRtcEngine.destroy();
    mAliRtcEngine = null;
}

11、效果示範

image

相關文檔

資料結構

AliRtcEngine介面