如何基于 ZEGO SDK 实现 Android 一对一音视频聊天(最新接入指南)

0 阅读21分钟

笔者简介

长期关注 RTC 实时音视频领域的技术演进,参与过音视频算法工程化、接入实现与效果评估等相关工作。

在实际项目中,持续关注包括腾讯 TRTC、即构 ZEGO、 声网 Agora 在内的多家 RTC 厂商在实时通信能力与工程落地层面的实践,重点关注 SDK 接入、通话链路实现与场景化应用。

本系列文章将围绕 RTC SDK 接入与实时互动能力实现,持续输出工程实践与场景分析。

在移动应用开发中,「Android 一对一视频通话如何实现」是一个高频需求,广泛应用于社交、在线咨询、远程服务等场景。开发者通常面临音视频采集、编码、传输、弱网优化等复杂问题,自研成本高、周期长。

开发者可以通过集成成熟厂商的 RTC SDK,大幅降低接入门槛。

本文将以 ZEGO SDK 为例,在无需自建音视频链路的情况下,提供一套完整的 Android 一对一音视频聊天实现方案,覆盖 SDK 集成、权限配置、通话建立等关键步骤。快速完成一个基础的 Android 视频通话功能搭建,并理解实时音视频通话的核心实现流程,适用于「Android 视频通话实现」「RTC SDK 接入」「一对一音视频聊天开发」等需求场景。

说明:本文基于最新版本的 ZEGO SDK 接入文档整理,对部分接入流程和配置方式进行了更新。

1 准备环境

在开始集成 ZEGO Express SDK 前,请确保开发环境满足以下要求:

  • Android Studio 2021.1.1 或以上版本。

  • Android SDK 25、Android SDK Build-Tools 25.0.2、Android SDK Platform-Tools 25.x.x 或以上版本。

  • Android 4.4 或以上版本,且支持音视频的 Android 设备。

  • Android 设备已经连接到 Internet。

2 项目准备

2.1 创建项目

进入即构官网,注册帐号后进入 ZEGO 控制台 ,在左侧导航栏单击“项目管理”,点击“创建项目”,按以下场景操作:

场景1:首次创建项目

如果是首次创建项目,您可以按照教程快速跑通实时音视频源码。

  1. 输入项目名称,选择主营业务地区,完成后点击“创建”进入下一步。

  1. 根据您需要接入的终端,下载对应的源码文件。

  1. 解压步骤二下载的源码。示例源码中缺少 SDK 初始化所需的 “AppID”、“AppSign” 和 “Token”等配置信息,可以从本界面获取。根据“操作说明”,找到示例源码的对应文件,将相关配置信息填写到对应位置即可。

项目创建过程预计1-2分钟,创建完成后即可获取 APP ID 等信息,用于 SDK 集成和配置。

场景2:非首次创建项目

点击“创建项目”,在弹出的“创建项目”窗口中,填写“项目名称”、并按需选择“主营业务地区”。

2.2 Token 鉴权

开发者在登录房间时必须带上 Token 参数,来验证用户的合法性,详细接入可参考 Token 鉴权教程

在前期测试阶段,可以通过 ZEGO 控制台生成临时 Token,便于调试,参考 控制台-项目信息-音视频临时 Token ;也可以借助此页面,校验您的服务端生成的 Token 的正确性,避免因权限控制缺失或操作不当引发的安全风险问题。

获取音视频临时 Token

前往 ZEGO 控制台-开发辅助-Token 生成和校验,获取临时 Token。

  1. 单击“点击生成”。
  2. 在弹出的“生成临时 Token”窗口中输入 “UserId” ,单击“生成”,即可快速生成临时 Token。
  3. 临时 Token 有效期为 24 小时,项目上线前必须生成正式的 Token,详情请参考 使用 Token 鉴权

2.3 集成

2.3.1 新建项目

此步骤以如何创建新项目为例,如果是集成到已有项目,可忽略此步。

  1. 打开 Android Studio,选择 “File > New > New Project” 菜单。

  1. 填写项目名及项目存储路径。

  1. 其它按照默认设置,单击 “Next”,最后单击 “Finish” 完成新工程创建。

2.3.2 导入 SDK

目前支持的平台架构包括:armeabi-v7a、arm64-v8a、x86、x86_64。

开发者可通过以下任意一种方式实现集成 SDK。

  1. 方式 1:自动集成 SDK(推荐)

  2. 复制 SDK AAR 文件手动集成

  3. 方式 3:复制 SDK JAR 和 SO 文件手动集成

此处以方式 1:自动集成 SDK为例进行说明。

  1. 进入项目根目录,打开 “settings.gradle” 文件,在 “dependencyResolutionManagement” 中加入如下代码。
...
dependencyResolutionManagement {
    repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
    repositories {
        maven { url 'https://maven.zego.im' }
        google()
        mavenCentral()
    }
}

若您的 Android Gradle Plugin 版本低于 v7.1.0,请按照如下方式操作:

进入项目根目录,打开 “build.gradle” 文件,在 “allprojects” 中加入如下代码。

...
allprojects {
    repositories {
        maven { url 'https://maven.zego.im' }
        google()
        mavenCentral()
    }
}

进入 “app” 目录,打开 “build.gradle” 文件,在 “dependencies” 中添加 implementation 'im.zego:express-video:x.y.z',请从 发布日志 查询 SDK 最新版本,并将 x.y.z 修改为具体的版本号。

...
dependencies {
    ...
    implementation 'im.zego:express-video:x.y.z'
}

2.3.3 设置权限

根据实际应用需要,设置应用所需权限。

进入 “app/src/main” 目录,打开 “AndroidManifest.xml” 文件,添加权限。

<!-- SDK 必须使用的权限 -->
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
<uses-permission android:name="android.permission.RECORD_AUDIO" />
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.BLUETOOTH" />
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />

<!-- App 需要使用的部分权限 -->
<uses-permission android:name="android.permission.READ_PHONE_STATE" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />

<uses-feature
    android:glEsVersion="0x00020000"
    android:required="true" />

<uses-feature android:name="android.hardware.camera" />
<uses-feature android:name="android.hardware.camera.autofocus" />

此处有 2 点应注意:

  • 由于 Android 6.0 在一些比较重要的权限上要求必须申请动态权限,不能只通过 “AndroidMainfest.xml” 文件申请静态权限。因此还需要参考执行如下代码,其中 “requestPermissions” 是 “Activity” 的方法。
  • 关于 BLUETOOTH 权限:仅 Android 6.0 以下版本需要声明,Android 6.0 及以上版本无需声明。
String[] permissionNeeded = {
    "android.permission.CAMERA",
    "android.permission.RECORD_AUDIO"};

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) {
        //101 为 requestCode,可以是任何大于 0 的数字,会透传到权限请求结果回调 onRequestPermissionsResult
        requestPermissions(permissionNeeded, 101);
    }
}

具体的权限说明如下:

必要性权限权限说明申请原因
必要权限INTERNET访问网络权限。SDK 基本功能都需要在联网的情况下才可以使用。
必要权限ACCESS_WIFI_STATE获取当前 WiFi 状态权限。SDK 会根据网络状态的改变执行不同的操作。例如,当网络重连时,SDK 内部会将网络断开时的状态都恢复,用户不需做额外的操作。
必要权限ACCESS_NETWORK_STATE获取当前网络状态权限。SDK 会根据网络状态的改变执行不同的操作。例如,当网络重连时,SDK 内部会将网络断开时的状态都恢复,用户不需做额外的操作
必要权限CAMERA访问相机权限。预览和发送视频时需要使用该权限。
必要权限RECORD_AUDIO录制音频权限。发送音频时需要使用该权限。
必要权限BLUETOOTH连接蓝牙设备权限。连接蓝牙设备时需要使用该权限。注意仅 Android 6.0 以下版本需要声明,Android 6.0 及以上版本无需声明。
必要权限MODIFY_AUDIO_SETTINGS修改音频配置权限。修改音频设备配置时需要使用该权限。
非必要权限READ_PHONE_STATE允许以只读方式访问电话状态,包括当前的呼叫状态。SDK 会根据当前的呼叫状态,启停音频设备。如监听到当前为呼叫状态,则 SDK 会自动停止使用音频设备,直到通话结束。
非必要权限WRITE_EXTERNAL_STORAGE内置 SDK 写权限。若需要使用媒体播放器或音效播放器加载 Android 外部存储内的媒体资源文件,则需要申请此权限,否则 SDK 无法加载资源。

说明:其中非必要权限 “android.permission.READ_PHONE_STATE” 仅用于实现 SDK 的打断事件处理,因此只需在 AndroidMainfest.xml 文件中进行声明即可,不需要动态申请(业务方有需求则另外处理)。

2.3.4 防止混淆代码

在 “proguard-rules.pro” 文件中,为 SDK 添加 -keep 类的配置,防止混淆 SDK 公共类名称。

-keep class **.zego.**{*;}

3 实现视频通话

3.1 视频通话 API 调用时序

3.2 实现流程

用户通过 ZEGO Express SDK 进行视频通话的基本流程为:

用户 A、B 加入房间,用户 B 预览并将音视频流推送到 ZEGO 云服务(推流),用户 A 收到用户 B 推送音视频流的通知之后,在通知中播放用户 B 的音视频流(拉流)。

3.2.1 初始化

1.创建界面

根据场景需要,为你的项目创建视频通话的用户界面。我们推荐你在项目中添加如下元素:

  • 本地视频窗口
  • 远端视频窗口
  • 结束通话按钮

2.准备基础工作,导入 ZEGO Express SDK 的相关类和常量

import im.zego.zegoexpress.ZegoExpressEngine;
import im.zego.zegoexpress.callback.IZegoDestroyCompletionCallback;
import im.zego.zegoexpress.callback.IZegoEventHandler;
import im.zego.zegoexpress.constants.ZegoPlayerState;
import im.zego.zegoexpress.constants.ZegoPublisherState;
import im.zego.zegoexpress.constants.ZegoRoomStateChangedReason;
import im.zego.zegoexpress.constants.ZegoScenario;
import im.zego.zegoexpress.constants.ZegoStreamQualityLevel;
import im.zego.zegoexpress.constants.ZegoUpdateType;
import im.zego.zegoexpress.entity.ZegoCanvas;
import im.zego.zegoexpress.entity.ZegoEngineProfile;
import im.zego.zegoexpress.entity.ZegoRoomConfig;
import im.zego.zegoexpress.entity.ZegoStream;
import im.zego.zegoexpress.entity.ZegoUser;
ZegoExpressEngine engine;
@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);

    // 在通话前需请求相应摄像头、录音权限
    requestPermission();

    // 开始通话按钮
    findViewById(R.id.startButton).setOnClickListener(new View.OnClickListener() {
        // 点击开始通话
        @Override
        public void onClick(View view) {
            // 创建Express SDK 实例
            createEngine();
            // 监听常用事件
            setEventHandler();
            // 登录房间
            loginRoom();
            // 开始预览及推流
            startPublish();
        }
    });

    // 停止通话按钮
    findViewById(R.id.stopButton).setOnClickListener(new View.OnClickListener() {
        // 点击停止通话
        @Override
        public void onClick(View view) {
            engine.logoutRoom();
            ZegoExpressEngine.destroyEngine(new IZegoDestroyCompletionCallback() {
                @Override
                public void onDestroyCompletion() {
                    //销毁成功
                }
            });

        }
    });
}

//请求摄像头、录音权限
private void requestPermission() {
    String[] permissionNeeded = {
            "android.permission.CAMERA",
            "android.permission.RECORD_AUDIO"};
    if (ContextCompat.checkSelfPermission(getApplicationContext(), "android.permission.CAMERA") != PackageManager.PERMISSION_GRANTED ||
            ContextCompat.checkSelfPermission(getApplicationContext(), "android.permission.RECORD_AUDIO") != PackageManager.PERMISSION_GRANTED) {
        //101 为requestCode,可以是任何大于 0 的数字,会透传到权限请求结果回调 onRequestPermissionsResult
        requestPermissions(permissionNeeded, 101);
    }
}

3.创建引擎

调用 createEngine 接口,将申请到的 AppID 和 AppSign 传入参数 “appID” 和 “appSign”。

根据 App 实际的音视频业务选择一个合适的场景,请参考 场景化音视频配置 文档,把选择好的场景枚举传入参数 "scenario"。

// 创建 ZegoExpress 实例,监听常用事件
void createEngine() {
    ZegoEngineProfile profile = new ZegoEngineProfile();
    profile.appID = appID;  // 请通过官网注册获取,格式为:1234567890L
    profile.appSign = appSign; //请通过官网注册获取,格式为:@"0123456789012345678901234567890123456789012345678901234567890123"(共64个字符)
    profile.scenario = ZegoScenario.BROADCAST;  // 指定使用直播场景 (请根据实际情况填写适合你业务的场景)
    profile.application = getApplication();
    engine = ZegoExpressEngine.createEngine(profile, null);
}

4.设置回调

您可以通过实现 IZegoEventHandler 接口中的特定方法(或者通过匿名内部类),以监听并处理所关注的事件回调。然后将接口实现类(或者匿名内部类)的对象作为eventHandler参数传递给 createEngine 方法或者传递给 setEventHandler 注册回调。

常用通知回调

void setEventHandler() {
        engine.setEventHandler(new IZegoEventHandler() {
            @Override
            // 房间内其他用户推流/停止推流时,我们会在这里收到相应用户的音视频流增减的通知
            public void onRoomStreamUpdate(String roomID, ZegoUpdateType updateType, ArrayList<ZegoStream> streamList, JSONObject extendedData) {
                super.onRoomStreamUpdate(roomID, updateType, streamList, extendedData);
                //当 updateType 为 ZegoUpdateType.ADD 时,代表有音视频流新增,此时我们可以调用 startPlayingStream 接口拉取播放该音视频流
                if (updateType == ZegoUpdateType.ADD) {
                    // 开始拉流,设置远端拉流渲染视图,视图模式采用 SDK 默认的模式,等比缩放填充整个 View
                    ZegoStream stream = streamList.get(0);
                    String playStreamID = stream.streamID;
                    // 如下 remoteUserView 为 UI 界面上的 TextureView.这里为了使示例代码更加简洁,我们只拉取新增的音视频流列表中第的第一条流,在实际的业务中,建议开发者循环遍历 streamList ,拉取每一条音视频流
                    ZegoCanvas playCanvas = new ZegoCanvas(findViewById(R.id.remoteUserView));
                    engine.startPlayingStream(playStreamID, playCanvas);
                }
            }

            //同一房间内的其他用户进出房间时,您可通过此回调收到通知。回调中的参数 ZegoUpdateType 为 ZegoUpdateType.ADD 时,表示有用户进入了房间;ZegoUpdateType 为 ZegoUpdateType.DELETE 时,表示有用户退出了房间。
            // 只有在登录房间 loginRoom 时传的配置 ZegoRoomConfig 中的 isUserStatusNotify 参数为 true 时,用户才能收到房间内其他用户的回调。
            // 房间人数大于 500 人的情况下 onRoomUserUpdate 回调不保证有效。若业务场景存在房间人数大于 500 的情况,请联系 ZEGO 技术支持。
            @Override
            public void onRoomUserUpdate(String roomID, ZegoUpdateType updateType, ArrayList<ZegoUser> userList) {
                super.onRoomUserUpdate(roomID, updateType, userList);
                // 您可以在回调中根据用户的进出/退出情况,处理对应的业务逻辑
                if (updateType == ZegoUpdateType.ADD) {
                    for (ZegoUser user : userList) {
                        String text = user.userID + "进入了房间";
                        Toast.makeText(getApplicationContext(), text, Toast.LENGTH_LONG).show();
                    }
                } else if (updateType == ZegoUpdateType.DELETE) {
                    for (ZegoUser user : userList) {
                        String text = user.userID + "退出了房间";
                        Toast.makeText(getApplicationContext(), text, Toast.LENGTH_LONG).show();
                    }
                }
            }

            // 房间连接状态改变
            @Override
            public void onRoomStateChanged(String roomID, ZegoRoomStateChangedReason reason, int i, JSONObject jsonObject) {
                super.onRoomStateChanged(roomID, reason, i, jsonObject);
                if(reason == ZegoRoomStateChangedReason.LOGINING) {
                    // 正在登录房间。当调用 [loginRoom] 登录房间或 [switchRoom] 切换到目标房间时,进入该状态,表示正在请求连接服务器。通常通过该状态进行应用界面的展示。
                } else if(reason == ZegoRoomStateChangedReason.LOGINED) {
                    //登录房间成功。当登录房间或切换房间成功后,进入该状态,表示登录房间已经成功,用户可以正常收到房间内的其他用户和所有流信息增删的回调通知。
                    //只有当房间状态是登录成功或重连成功时,推流(startPublishingStream)、拉流(startPlayingStream)才能正常收发音视频
                } else if(reason == ZegoRoomStateChangedReason.LOGIN_FAILED) {
                    //登录房间失败。当登录房间或切换房间失败后,进入该状态,表示登录房间或切换房间已经失败,例如 AppID 或 Token 不正确等。
                } else if(reason == ZegoRoomStateChangedReason.RECONNECTING) {
                    //房间连接临时中断。如果因为网络质量不佳产生的中断,SDK 会进行内部重试。
                } else if(reason == ZegoRoomStateChangedReason.RECONNECTED) {
                    //房间重新连接成功。如果因为网络质量不佳产生的中断,SDK 会进行内部重试,重连成功后进入该状态。
                } else if(reason == ZegoRoomStateChangedReason.RECONNECT_FAILED) {
                    //房间重新连接失败。如果因为网络质量不佳产生的中断,SDK 会进行内部重试,重连失败后进入该状态。
                } else if(reason == ZegoRoomStateChangedReason.KICK_OUT) {
                    //被服务器踢出房间。例如有相同用户名在其他地方登录房间导致本端被踢出房间,会进入该状态。
                } else if(reason == ZegoRoomStateChangedReason.LOGOUT) {
                    //登出房间成功。没有登录房间前默认为该状态,当调用 [logoutRoom] 登出房间成功或 [switchRoom] 内部登出当前房间成功后,进入该状态。
                } else if(reason == ZegoRoomStateChangedReason.LOGOUT_FAILED) {
                    //登出房间失败。当调用 [logoutRoom] 登出房间失败或 [switchRoom] 内部登出当前房间失败后,进入该状态。
                }
            }

            //用户推送音视频流的状态通知
            //用户推送音视频流的状态发生变更时,会收到该回调。如果网络中断导致推流异常,SDK 在重试推流的同时也会通知状态变化。
            @Override
            public void onPublisherStateUpdate(String streamID, ZegoPublisherState state, int errorCode, JSONObject extendedData) {
                super.onPublisherStateUpdate(streamID, state, errorCode, extendedData);
                if (errorCode != 0) {
                    //推流状态出错
                }
                if (state == ZegoPublisherState.PUBLISHING) {
                    //正在推流中
                } else if (state == ZegoPublisherState.NO_PUBLISH){
                    //未推流
                } else if (state == ZegoPublisherState.PUBLISH_REQUESTING){
                    //正在请求推流中
                }
            }
            //用户拉取音视频流的状态通知
            //用户拉取音视频流的状态发生变更时,会收到该回调。如果网络中断导致拉流异常,SDK 会自动进行重试。
            @Override
            public void onPlayerStateUpdate(String streamID, ZegoPlayerState state, int errorCode, JSONObject extendedData) {
                super.onPlayerStateUpdate(streamID, state, errorCode, extendedData);
                if (errorCode != 0) {
                    //拉流状态出错
                }
                if (state == ZegoPlayerState.PLAYING) {
                    //正在拉流中
                } else if (state == ZegoPlayerState.NO_PLAY){
                    //未拉流
                } else if (state == ZegoPlayerState.PLAY_REQUESTING){
                    //正在请求拉流中
                }
            }

            @Override
            public void onNetworkQuality(String userID, ZegoStreamQualityLevel zegoStreamQualityLevel, ZegoStreamQualityLevel zegoStreamQualityLevel1) {
                super.onNetworkQuality(userID, zegoStreamQualityLevel, zegoStreamQualityLevel1);
                if (userID == null) {
                    // 代表本地用户(我)的网络质量
                    //("我的上行网络质量是 %lu", (unsigned long)upstreamQuality);
                    //("我的下行网络质量是 %lu", (unsigned long)downstreamQuality);
                } else {
                    //代表房间内其他用户的网络质量
                    //("用户 %s 的上行网络质量是 %lu", userID, (unsigned long)upstreamQuality);
                    //("用户 %s 的下行网络质量是 %lu", userID, (unsigned long)downstreamQuality);
                }

                /*
                ZegoStreamQualityLevel.EXCELLENT, 网络质量极好
                ZegoStreamQualityLevel.GOOD, 网络质量好
                ZegoStreamQualityLevel.MEDIUM, 网络质量正常
                ZegoStreamQualityLevel.BAD, 网络质量差
                ZegoStreamQualityLevel.DIE, 网络异常
                ZegoStreamQualityLevel.UNKNOWN, 网络质量未知
                */
            }
        });
}

3.2.2 登录房间

你可以调用 loginRoom 接口登录房间。如果房间不存在,调用该接口时会创建并登录此房间。roomID 和 user 的参数由您本地生成,但是需要满足以下条件:

  • 同一个 AppID 内,需保证 “roomID” 全局唯一。
  • 同一个 AppID 内,需保证 “userID” 全局唯一,建议开发者将 “userID” 与自己业务的账号系统进行关联。
//登录房间
void loginRoom() {
    // ZegoUser 的构造方法 public ZegoUser(String userID) 会将 “userName” 设为与传的参数 “userID” 一样。“userID” 不能为 “null” 否则会导致登录房间失败。
    ZegoUser user = new ZegoUser("user2");

    ZegoRoomConfig roomConfig = new ZegoRoomConfig();
    //如果您使用 appsign 的方式鉴权,token 参数不需填写;如果需要使用更加安全的 鉴权方式: token 鉴权,请参考[如何从 AppSign 鉴权升级为 Token 鉴权](https://doc-zh.zego.im/faq/express-token-upgrade)
    //roomConfig.token = ;
    // 只有传入 “isUserStatusNotify” 参数取值为 “true” 的 ZegoRoomConfig,才能收到 onRoomUserUpdate 回调。
    roomConfig.isUserStatusNotify = true;

    // roomID 由您本地生成,需保证 “roomID” 全局唯一。不同用户要登录同一个房间才能进行通话
    String roomID = "room1";

    // 登录房间
    engine.loginRoom(roomID, user, roomConfig, (int error, JSONObject extendedData)->{
        // 登录房间结果,如果仅关注登录结果,关注此回调即可
        if (error == 0) {
            // 登录成功
            Toast.makeText(this, "登录成功", Toast.LENGTH_LONG).show();
        } else {
            // 登录失败,请参考 errorCode 说明 /real-time-video-android-java/client-sdk/error-code
            Toast.makeText(this, "登录失败,请参考 errorCode 说明 /real-time-video-android-java/client-sdk/error-code", Toast.LENGTH_LONG).show();
        }
    });
}
登录状态(房间连接状态)回调

调用登录房间接口之后,您可通过监听 onRoomStateChanged 回调实时监控自己在本房间内的连接状态。

3.2.3 预览自己的画面,并推送到 ZEGO 音视频云

1.(可选)预览自己的画面

说明

无论是否调用 startPreview 预览,都可以将自己的音视频流推送到 ZEGO 音视频云。

如果希望看到本端的画面,可调用 startPreview 接口设置预览视图,并启动本地预览。

2.将自己的音视频流推送到 ZEGO 音视频云

在用户调用 loginRoom 接口后,可以直接调用 startPublishingStream 接口,传入 “streamID”,将自己的音视频流推送到 ZEGO 音视频云。您可通过监听 onPublisherStateUpdate 回调知晓推流是否成功。

“streamID” 由您本地生成,但是需要保证:

同一个 AppID 下,“streamID” 全局唯一。如果同一个 AppID 下,不同用户各推了一条 “streamID” 相同的流,后推流的用户推流失败。

//预览并推流
void startPublish() {
    // 设置本地预览视图并启动预览,视图模式采用 SDK 默认的模式,等比缩放填充整个 View
    ZegoCanvas previewCanvas = new ZegoCanvas(findViewById(R.id.preview));;
    engine.startPreview(previewCanvas);

    // 开始推流
    // 用户调用 loginRoom 之后再调用此接口进行推流
    // 在同一个 AppID 下,开发者需要保证“streamID” 全局唯一,如果不同用户各推了一条 “streamID” 相同的流,后推流的用户会推流失败。
    engine.startPublishingStream("stream2");
}

3.2.4 拉取其他用户的音视频

进行视频通话时,我们需要拉取到其他用户的音视频。

在同一房间内的其他用户将音视频流推送到 ZEGO 音视频云时,我们会在 onRoomStreamUpdate 回调中收到音视频流新增的通知,并可以通过 ZegoStream 获取到某条流的 “streamID”。

我们可以在该回调中,调用 startPlayingStream,传入 “streamID” 拉取播放该用户的音视频。您可通过监听 onPlayerStateUpdate 回调知晓是否成功拉取音视频。

// 监听回调
void setEventHandler() {
        engine.setEventHandler(new IZegoEventHandler() {
            @Override
            // 房间内其他用户推流/停止推流时,我们会在这里收到相应用户的音视频流增减的通知
            public void onRoomStreamUpdate(String roomID, ZegoUpdateType updateType, ArrayList<ZegoStream> streamList, JSONObject extendedData) {
                super.onRoomStreamUpdate(roomID, updateType, streamList, extendedData);
                //当 updateType 为 ZegoUpdateType.ADD 时,代表有音视频流新增,此时我们可以调用 startPlayingStream 接口拉取播放该音视频流
                if (updateType == ZegoUpdateType.ADD) {
                    // 开始拉流,设置远端拉流渲染视图,视图模式采用 SDK 默认的模式,等比缩放填充整个 View
                    ZegoStream stream = streamList.get(0);
                    String playStreamID = stream.streamID;
                    // 如下 remoteUserView 为 UI 界面上的 TextureView.这里为了使示例代码更加简洁,我们只拉取新增的音视频流列表中第的第一条流,在实际的业务中,建议开发者循环遍历 streamList ,拉取每一条音视频流
                    ZegoCanvas playCanvas = new ZegoCanvas(findViewById(R.id.remoteUserView));
                    engine.startPlayingStream(playStreamID, playCanvas);
                }
            }
        });
}
注意事项

如果用户在音视频通话的过程中,遇到相关错误,可查询 错误码

3.2.5 在线测试推拉流功能

在真机中运行项目,运行成功后,可以看到本端视频画面。

为方便体验,ZEGO 提供了一个 Web 端调试示例,在该页面下,输入相同的 AppID、RoomID,输入不同的 UserID、以及对应的 Token,即可加入同一房间与真机设备互通。当成功开始音视频通话时,可以听到远端的音频,看到远端的视频画面。

3.2.6 停止音视频通话

停止推送和拉取音视频流

1.停止推流,停止预览

调用 stopPublishingStream 接口停止向远端用户发送本端的音视频流。

// 停止推流
engine.stopPublishingStream();

如果启用了本地预览,调用 stopPreview 接口停止预览。

// 停止本地预览
engine.stopPreview();

2.停止拉流

调用 stopPlayingStream 接口停止拉取远端推送的音视频流。

// 停止拉流
engine.stopPlayingStream("stream1");
退出房间

调用 logoutRoom 接口退出房间。

// 退出房间
engine.logoutRoom();
销毁引擎

如果用户彻底不使用音视频功能时,可调用 destroyEngine 接口销毁引擎,释放麦克风、摄像头、内存、CPU 等资源。

  • 如果需要监听回调,来确保设备硬件资源释放完成,可在销毁引擎时传入 “callback”。该回调只用于发送通知,开发者不可以在回调内释放与引擎相关的资源。
  • 如果不需要监听回调,可传入 “null”。
ZegoExpressEngine.destroyEngine(null);

至此,我们已经完成了 Android 一对一音视频通话的基础能力搭建。通过集成 ZEGO SDK,可以在无需自建音视频链路的情况下,实现从 SDK 初始化、登录房间、推流与拉流到通话结束的完整流程。

这一实现路径覆盖了实时音视频通话的核心能力,适用于社交、在线咨询、远程服务等典型场景。在此基础上,你可以进一步扩展多人通话、语聊房、互动直播等更复杂的实时互动玩法。

如果你在接入过程中遇到问题,或者想了解 RTC 价格方案,可以参考下方常见问题或官方文档进一步排查。

注册即构 ZEGO 开发者帐号,快速开始;

查看 即构 ZEGO 官方接入文档、下载 demo 体验通话效果;

查看文档中心,了解更多互动相关产品接入:IM 即时通讯、AI 美颜、AI Agent 等;

常见问题补充

附上在实际接入过程中,开发者通常会关注以下几个问题:

Q1:Android 如何实现一对一视频通话? A:可以通过集成 RTC SDK(如本文示例的 ZEGO RTC SDK),快速完成音视频采集、编码、传输和播放能力,无需自建底层音视频链路。

Q2:实现视频通话需要自己搭建服务器吗? A:不需要。RTC SDK 已内置全球音视频传输网络,开发者只需完成客户端集成和基础业务逻辑即可。

Q3:ZEGO SDK 支持哪些 Android 版本? A:通常支持 Android 4.4 及以上版本(具体以官方文档为准),并要求设备具备音视频能力。

Q4:视频通话的核心实现流程是什么? A:核心流程包括:初始化 SDK → 登录房间 → 本地预览与推流 → 远端拉流播放 → 结束通话与资源释放。

Q5:开发过程中最常见的问题有哪些? A:常见问题包括权限未正确申请、Token 鉴权错误、AppID/AppSign 配置错误、网络环境不稳定等。

本文主要介绍了 Android 一对一音视频通话的基础接入流程。围绕实时音视频能力,我们还会持续整理更多细分场景与实现细节,包括但不限于:

  • Android 视频通话完整实现流程拆解
  • RTC 推流 / 拉流机制与核心原理
  • 常见接入问题与排查方法(权限、Token、连接失败等)
  • 多人通话、语聊房、互动直播等进阶场景实现

本系列将围绕「RTC SDK 接入与实时互动能力实现」持续更新,适合需要系统搭建音视频能力的开发者参考。