全部产品
Search
文档中心

视频直播:Android实现音视频通话

更新时间:Jun 07, 2026

本文档将介绍如何在您的Android项目中集成 ARTC SDK, 快速实现一个简单的实时音视频互动App,适用于互动直播和视频通话等场景。

功能简介

在开始之前,了解以下几个关键概念会很有帮助:

  • ARTC SDK:这是阿里云的实时音视频产品,帮助开发者快速实现实时音视频互动的SDK。

  • GRTN:阿里云全球实时传输网络,提供超低延时、高音质、安全可靠的音视频通讯服务。

  • 频道:相当于一个虚拟的房间,所有加入同一频道的用户都可以进行实时音视频互动。

  • 主播:可在频道内发布音视频流,并可订阅其他主播发布的音视频流。

  • 观众:可在频道内订阅音视频流,不能发布音视频流。

image
  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 的调用。

下图展示了实现音视频互动的基本流程:

image

下面是一段实现音视频通话基本流程的完整参考代码:

基本流程代码示例

/**
 * 音视频通话场景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接口