互动白板 Android 端开发指南

SDK 概述

网易云通信 SDK 提供完善的互动白板(实时会话)开发框架,用于在线白板教学和文件传输等实时场景。实现互动白板通话功能需要用到登录认证服务、互动白板(实时会话)和文档转码服务。 SDK 屏蔽其内部复杂细节,对外提供较为简洁的 API 接口,方便开发者快速集成互动白板功能。

开发准备

网易云通信 Android SDK 支持两种方式集成SDK。

1. 通过 Gradle 集成SDK (推荐)

2. 通过类库配置集成SDK

网易云通信 Android SDK 2.5.0 以上强烈推荐通过 Gradle 集成 SDK。

注意:网易云通信互动白板要求系统版本最低是 Android 4.1。

通过Gradle集成SDK

首先,在整个工程的 build.gradle 文件中,配置repositories,使用 jcenter 或者 maven ,二选一即可,如下:

allprojects {
    repositories {
        jcenter() // 或者 mavenCentral()
    }
}

第二步,在主工程的 build.gradle 文件中,添加 dependencies。根据自己项目的需求,添加不同的依赖即可。注意:版本号必须一致,这里以3.3.0版本为例:


android {
   defaultConfig {
       ndk {
           //设置支持的SO库架构
           abiFilters "armeabi-v7a", "x86","arm64-v8a","x86_64"
        }
   }
}

dependencies {
     compile fileTree(dir: 'libs', include: '*.jar')
    // 基础模块
     compile 'com.netease.nimlib:basesdk:3.3.0'
     // 实时会话和文档转码服务模块(版本号必须和基础模块一样)
     compile 'com.netease.nimlib:rts:3.3.0'
}

再次注意:依赖包的版本号必须一致。

通过类库配置集成SDK

首先到下载页面下载 Android SDK。开发者可以根据实际需求,配置类库。

以下介绍以 Android SDK v2.5及以上版本为例,Android SDK v2.5以下的配置,请咨询技术支持。

SDK 包的libs文件夹中,包含了网易云通信的 jar 文件,各 jni 库文件夹以及 SDK 依赖的第三方库。

实现音视频通话功能,需要将这些文件拷贝到你的工程的 libs 目录下,即可完成配置。列表如下:

libs
├── arm64-v8a
│   ├── libnrtc_engine.so (音视频基础服务底层库)
│   └── libnrtc_network.so(音视频基础服务底层库)
│   └── librts_network.so (互动白板服务底层库)
├── armeabi-v7a
│   ├── libnrtc_engine.so
│   └── libnrtc_network.so
│   └── librts_network.so
├── x86
│   ├── libnrtc_engine.so
│   └── libnrtc_network.so
│   └── librts_network.so
├── x86_64
│   ├── libnrtc_engine.so
│   └── libnrtc_network.so
│   └── librts_network.so
├── nim-basesdk-3.3.0.jar (云通信SDK基础服务)
├── nim-rts-3.3.0.jar (实时会话、文档转码服务)
├── nrtc-sdk.jar(音视频基础服务)

以上文件列表中,jar文件版本号可能会不同,子目录中的文件是 SDK 所依赖的各个 CPU 架构的 so 库。

如果你使用的 IDE 是 Android Studio,要将 jni 库按照 IDEA 工程目录的结构,放置在对应的目录中(一般为 src/main/jniLibs)。或者在 build.gradle 中配置好 jniLibs 的 sourceSets(可参考 demo 的 build.gradle)。

权限与组件

AndroidManifest.xml 中加入以下配置:

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
       package="xxx">

    <!-- 权限声明 -->
    <!-- 访问网络状态-->
    <uses-permission android:name="android.permission.INTERNET" />
    <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
    <uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
    <!-- 控制呼吸灯,振动器等,用于新消息提醒 -->
    <uses-permission android:name="android.permission.FLASHLIGHT" />
    <uses-permission android:name="android.permission.VIBRATE" />
    <!-- 外置存储存取权限 -->
    <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>

    <!-- 多媒体相关 -->
    <uses-permission android:name="android.permission.CAMERA"/>
    <uses-permission android:name="android.permission.RECORD_AUDIO"/>
    <uses-permission android:name="android.permission.READ_PHONE_STATE"/>

    <!-- 如果需要实时音视频通话模块,下面的权限也是必须的。否则,可以不加 -->
    <uses-permission android:name="android.permission.BLUETOOTH" />
    <uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />
    <uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS"/>
    <uses-permission android:name="android.permission.BROADCAST_STICKY"/>
    <uses-feature android:name="android.hardware.camera" />
    <uses-feature android:name="android.hardware.camera.autofocus" />
    <uses-feature android:glEsVersion="0x00020000" android:required="true" />

    <!-- SDK 权限申明, 第三方 APP 接入时,请将 com.netease.nim.demo 替换为自己的包名 -->
    <!-- 和下面的 uses-permission 一起加入到你的 AndroidManifest 文件中。 -->
    <permission
        android:name="com.netease.nim.demo.permission.RECEIVE_MSG"
        android:protectionLevel="signature"/>
    <!-- 接收 SDK 消息广播权限, 第三方 APP 接入时,请将 com.netease.nim.demo 替换为自己的包名 -->
     <uses-permission android:name="com.netease.nim.demo.permission.RECEIVE_MSG"/>

    <application
        ...>
        <!-- APP key, 可以在这里设置,也可以在 SDKOptions 中提供。
            如果 SDKOptions 中提供了,取 SDKOptions 中的值。 -->
        <meta-data
            android:name="com.netease.nim.appKey"
            android:value="key_of_your_app" />

        <!-- 声明网易云通信后台服务,如需保持后台推送,使用独立进程效果会更好。 -->
        <service
            android:name="com.netease.nimlib.service.NimService"
            android:process=":core"/>

       <!-- 运行后台辅助服务 -->
        <service
            android:name="com.netease.nimlib.service.NimService$Aux"
            android:process=":core"/>

        <!-- 声明网易云通信后台辅助服务 -->
        <service
            android:name="com.netease.nimlib.job.NIMJobService"
            android:exported="true"
            android:permission="android.permission.BIND_JOB_SERVICE"
            android:process=":core"/>

        <!-- 网易云通信SDK的监视系统启动和网络变化的广播接收器,用户开机自启动以及网络变化时候重新登录,
            保持和 NimService 同一进程 -->
        <receiver android:name="com.netease.nimlib.service.NimReceiver"
            android:process=":core"
            android:exported="false">
            <intent-filter>
                <action android:name="android.intent.action.BOOT_COMPLETED"/>
                <action android:name="android.net.conn.CONNECTIVITY_CHANGE"/>
            </intent-filter>
        </receiver>

        <!-- 网易云通信进程间通信 Receiver -->
        <receiver android:name="com.netease.nimlib.service.ResponseReceiver"/>

        <!-- 网易云通信进程间通信service -->
        <service android:name="com.netease.nimlib.service.ResponseService"/>

    </application>
</manifest>

混淆配置

如果你的 apk 最终会经过代码混淆,请在 proguard 配置文件中加入以下代码:

-dontwarn com.netease.**
-dontwarn io.netty.**
-keep class com.netease.** {*;}
#如果 netty 使用的官方版本,它中间用到了反射,因此需要 keep。如果使用的是我们提供的版本,则不需要 keep
-keep class io.netty.** {*;}

#如果你使用全文检索插件,需要加入
-dontwarn org.apache.lucene.**
-keep class org.apache.lucene.** {*;}

总体接口介绍

网易云通信 SDK 提供了两类接口供开发者调用:一类是第三方 APP 主动发起请求,第二类是第三方 APP 作为观察者监听事件和变化。第一类接口名均以 Service 结尾,例如 AuthService ,第二类接口名均以 ServiceObserver 结尾,例如 AuthServiceObserver,个别太长的类名则可能直接以 Observer 结尾,比如 SystemMessageObserver

可以通过 NIMClientgetService 接口获取到各个服务实例,例如:通过 NIMClient.getService(AuthService.class) 获取到 AuthService 服务实例,通过 NIMClient.getService(AuthServiceObserver.class) 获取到 AuthServiceObserver 观察者接口。

SDK 由于需要保持后台运行,典型场景下会在独立进程中运行,第一类接口基本上都是从主进程发起调用,然后在后台进程执行,最后再将结果返回给主进程。因此,如无特殊说明,所有接口均为异步调用,开发者无需担心调用 SDK 接口阻塞 UI 的问题。如果调用的接口需要一些后期操作,包括结果回调,取消调用等,此类接口会返回一个 InvocationFuture 对象。如果开发者关心调用的结果,可在此返回的接口中设置回调函数。 接口回调接口为 RequestCallback ,有3个接口需要实现:onSuccess, onFailed, onException,分别对应成功,失败,以及出现异常。另外还提供了一个更简洁的接口 RequestCallbackWrapper,作为 RequestCallback 的包裹,封装了上面的 3 个接口,然后将他们合并成一个 onResult 接口,在参数上做不同结果的区分。

还有一类接口,耗时会很长,可能需要传输大量数据,或者要发起网络连接,比如上传下载,登录等。对于这类接口,返回值会是一个 AbortableFuture 对象,该接口继承自 InvocationFuture,增加了一个 abort() 方法,可取消之前的请求。

观察者接口的方法名都是以 observe 开头,并包含一个 register 参数,该值为true时,为注册观察者,为false时,注销观察者。开发者要在不需要观察者时,主动注销,以免造成资源泄露。

注意:除了 NIMClient.init 接口外,其他 SDK 暴露的接口都只能在 UI 进程调用。如果 APP 包含远程 service,该 APP 的 Application 的 onCreate 会多次调用。因此,如果需要在 onCreate 中调用除 init 接口外的其他接口,应先判断当前所属进程,并只有在当前是 UI 进程时才调用。判断代码如下:

public static boolean inMainProcess(Context context) {
    String packageName = context.getPackageName();
    String processName = SystemUtil.getProcessName(context);
    return packageName.equals(processName);
}

/**
 * 获取当前进程名
 * @param context
 * @return 进程名
 */
public static final String getProcessName(Context context) {
    String processName = null;

    // ActivityManager
    ActivityManager am = ((ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE));

    while (true) {
        for (ActivityManager.RunningAppProcessInfo info : am.getRunningAppProcesses()) {
            if (info.pid == android.os.Process.myPid()) {
                processName = info.processName;
                break;
            }
        }

        // go home
        if (!TextUtils.isEmpty(processName)) {
            return processName;
        }

        // take a rest and again
        try {
            Thread.sleep(100L);
        } catch (InterruptedException ex) {
            ex.printStackTrace();
        }
    }
}

SDK 提供的接口主要按照业务进行分类,大致说明如下:

SDK 数据缓存目录结构

当收到多媒体消息后,SDK 会负责下载这些多媒体文件,同时 SDK 还要记录一些关键的 log,因此 SDK 需要一个数据缓存目录。 该目录可以在 SDK 初始化时通过 SDKOptions#sdkStorageRootPath 进行设置。 如果不设置,则默认为“/{外卡根目录}/{app_package_name}/nim/”,其中外卡根目录获取方式为 Environment.getExternalStorageDirectory().getPath()。 如果你的 APP 需要清除缓存功能,可扫描该目录下的文件,按照你们的规则清理即可。 在 SDK 初始化完成后可以通过 NimClient#getSdkStorageDirPath 获取 SDK 数据缓存目录。

SDK数据缓存目录下面包含如下子目录:

初始化 SDK

在你的程序的 Application 的 onCreate 中,加入网易云通信 SDK 的初始化代码:

public class NimApplication extends Application {

    public void onCreate() {
        // ... your codes

        // SDK初始化(启动后台服务,若已经存在用户登录信息, SDK 将完成自动登录)
        NIMClient.init(this, loginInfo(), options());

        // ... your codes
        if (inMainProcess()) {
            // 注意:以下操作必须在主进程中进行
            // 1、UI相关初始化操作
            // 2、相关Service调用
        }
    }

    // 如果返回值为 null,则全部使用默认参数。
    private SDKOptions options() {
        SDKOptions options = new SDKOptions();

        // 如果将新消息通知提醒托管给 SDK 完成,需要添加以下配置。否则无需设置。
        StatusBarNotificationConfig config = new StatusBarNotificationConfig();
        config.notificationEntrance = WelcomeActivity.class; // 点击通知栏跳转到该Activity
        config.notificationSmallIconId = R.drawable.ic_stat_notify_msg;
        // 呼吸灯配置
        config.ledARGB = Color.GREEN;
        config.ledOnMs = 1000;
        config.ledOffMs = 1500;
        // 通知铃声的uri字符串
        config.notificationSound = "android.resource://com.netease.nim.demo/raw/msg";
        options.statusBarNotificationConfig = config;

        // 配置保存图片,文件,log 等数据的目录
        // 如果 options 中没有设置这个值,SDK 会使用下面代码示例中的位置作为 SDK 的数据目录。
        // 该目录目前包含 log, file, image, audio, video, thumb 这6个目录。
        // 如果第三方 APP 需要缓存清理功能, 清理这个目录下面个子目录的内容即可。
        String sdkPath = Environment.getExternalStorageDirectory() + "/" + getPackageName() + "/nim";
        options.sdkStorageRootPath = sdkPath;

        // 配置是否需要预下载附件缩略图,默认为 true
        options.preloadAttach = true;

        // 配置附件缩略图的尺寸大小。表示向服务器请求缩略图文件的大小
        // 该值一般应根据屏幕尺寸来确定, 默认值为 Screen.width / 2
        options.thumbnailSize = ${Screen.width} / 2;

        // 用户资料提供者, 目前主要用于提供用户资料,用于新消息通知栏中显示消息来源的头像和昵称
        options.userInfoProvider = new UserInfoProvider() {
             @Override
             public UserInfo getUserInfo(String account) {
                 return null;
             }

             @Override
             public int getDefaultIconResId() {
                 return R.drawable.avatar_def;
             }

             @Override
             public Bitmap getTeamIcon(String tid) {
                 return null;
             }

             @Override
             public Bitmap getAvatarForMessageNotifier(String account) {
                  return null;
             }

             @Override
             public String getDisplayNameForMessageNotifier(String account, String sessionId,
                SessionTypeEnum sessionType) {
                 return null;
             }
         };
         return options;
    }

    // 如果已经存在用户登录信息,返回LoginInfo,否则返回null即可
    private LoginInfo loginInfo() {
        return null;
    }
}

特别提醒:SDK 的初始化方法必须在主进程中调用,在非主进程中初始化无效。请在主进程中调用 SDK XXXService 提供的方法,在主进程中注册 XXXServiceObserver 的观察者(有事件变更,会回调给主进程的主线程)。如果你的模块运行在非主进程,请自行实现主进程与非主进程的通信(Binder/AIDL/BroadcastReceiver等IPC)将主进程回调或监听返回的数据传递给非主进程。

网易云通信Andorid SDK断网重连机制及登录返回码说明

登录与登出

登录集成必读

手动登录

一般 APP 在首次登录、切换帐号登录、注销重登时需要手动登录,开发者需要调用 AuthService 提供的 login 接口主动发起登录请求。该接口返回类型为 AbortableFuture,允许用户在后面取消登录操作。如果服务器一直没有响应,30 秒后 RequestCallback 的 onFailed 会被调用,参数为 408 (网络连接超时)。

public class LoginActivity extends Activity {
    public void doLogin() {
        LoginInfo info = new LoginInfo(); // config...
        RequestCallback<LoginInfo> callback =
            new RequestCallback<LoginInfo>() {
            // 可以在此保存LoginInfo到本地,下次启动APP做自动登录用
        };
        NIMClient.getService(AuthService.class).login(info)
                .setCallback(callback);
    }
}

登录成功后,可以将用户登录信息 LoginInfo 信息保存到本地,下次启动APP时,读取本地保存的 LoginInfo 进行自动登录。

说明:在手动登录过程中,如果网络断开或者与网易云通信服务器建立连接失败,会返回登录失败(错误码 415),在线状态切换为 NET_BROKEN; 如果连接建立成功,SDK 发出登录请求后网易云通信服务器一直没有响应,那么 30s 后将导致登录超时,那么会返回登录失败(错误码 408),在线状态切换为 UNLOGIN。

注意:从SDK 2.2.0版本开始, LoginInfo 中添加了可选属性 AppKey,支持在登录的时候设置 AppKey;如果不填,则优先使用 SDKOptions 中配置的 AppKey;如果也没有,则使用 AndroidManifest.xml 中配置的 AppKey(默认方式)。建议使用默认方式。

特别提醒: 登录成功之前,调用服务器相关请求接口(由于与网易云通信服务器连接尚未建立成功,会导致发包超时)会报408错误;调用本地数据库相关接口(手动登录的情况下数据库未打开),会报1000错误,建议用户在登录成功之后,再进行相关接口调用。

自动登录

如果上次登录已经存在用户登录信息,那么在初始化 SDK 时传入 LoginInfo,SDK 后台会自动登录,并在登录发起前即打开相关账号的数据库,供上层调用。开发者此时无需再手动调用登录接口,可以跳过登录界面直接进入主界面。

进入主界面后,可以通过监听用户在线状态(每次注册用户在线状态监听都会立即回调通知当前的用户在线状态),或者主动获取当前用户在线状态,来判断自动登录是否成功。

在初始化 SDK 时自动登录示例:

public class NimApplication extends Application {

    public void onCreate() {
        // ... your codes

        // SDK初始化(启动后台服务,若已经存在用户登录信息,SDK 将完成自动登录)
        NIMClient.init(this, loginInfo(), options());

        // ... your codes
    }

    private LoginInfo loginInfo() {
        // 从本地读取上次登录成功时保存的用户登录信息
        String account = Preferences.getUserAccount();
        String token = Preferences.getUserToken();

        if (!TextUtils.isEmpty(account) && !TextUtils.isEmpty(token)) {
            DemoCache.setAccount(account.toLowerCase());
            return new LoginInfo(account, token);
        } else {
            return null;
        }
    }
}

说明:在自动登录过程中,如果没有网络或者网络断开或者与网易云通信服务器建立连接失败,会上报在线状态 NET_BROKEN,表示当前网络不可用,当网络恢复的时候,会触发断网自动重连;如果连接建立成功但登录超时,会上报在线状态 UNLOGIN,并触发自动重连,无需上层手动调用登录接口。

特别提醒: 在自动登录成功前,调用服务器相关请求接口(由于与网易云通信服务器连接尚未建立成功,会导致发包超时)会报408错误。但可以调用本地数据库相关接口获取本地数据(自动登录的情况下会自动打开相关账号的数据库)。自动登录过程中也会有用户在线状态回调。

监听用户在线状态

登录成功后,SDK 会负责维护与服务器的长连接以及断线重连等工作。当用户在线状态发生改变时,会发出通知。此外,自动登录过程中也会有状态回调。开发者可以通过加入以下代码监听用户在线状态改变:

NIMClient.getService(AuthServiceObserver.class).observeOnlineStatus(
    new Observer<StatusCode> () {
        public void onEvent(StatusCode status) {
            Log.i("tag", "User status changed to: " + status);
            if (code.wontAutoLogin()) {
                // 被踢出、账号被禁用、密码错误等情况,自动登录失败,需要返回到登录界面进行重新登录操作
            }
        }
}, true);

被踢出的情况说明:

  1. 当用户在线时被踢出,会立刻收到被踢出的状态变更通知;
  2. 当用户离线后在其他设备成功登录,又在本设备重新自动登录时,也会收到被踢出的状态变更通知。

开发者也可以主动获取当前用户在线状态:

StatusCode status = NIMClient.getStatus();

数据同步状态通知

登录成功后,SDK 会立即同步数据(用户资料、用户关系、群资料、离线消息、漫游消息等),同步开始和同步完成都会发出通知。

注册登录同步状态通知:

NIMClient.getService(AuthServiceObserver.class).observeLoginSyncDataStatus(new Observer<LoginSyncStatus>() {
    @Override
    public void onEvent(LoginSyncStatus status) {
        if (status == LoginSyncStatus.BEGIN_SYNC) {
            LogUtil.i(TAG, "login sync data begin");
        } else if (status == LoginSyncStatus.SYNC_COMPLETED) {
            LogUtil.i(TAG, "login sync data completed");
        }
    }
}, register);

同步开始时,SDK 数据库中的数据可能还是旧数据(如果是首次登录,那么 SDK 数据库中还没有数据,重新登录时 SDK 数据库中还是上一次退出时保存的数据)。

同步完成时, SDK 数据库已完成更新。

在同步过程中,SDK 数据的更新会通过相应的 XXXServiceObserver 接口发出数据变更通知。

一般来说, APP 开发者在登录完成后可以开始构建数据缓存:

多端登录

登录成功后,可以注册多端登录状态观察者。当有其他端登录或者注销时,会通过此接口通知到UI。登录成功后,如果有其他端登录着(在线),也会发出通知。返回的 OnlineClient 能够获取当前同时在线的客户端类型和操作系统。

NIMClient.getService(AuthServiceObserver.class).observeOtherClients(new Observer<List<OnlineClient>>() { ... }, true);

如果需要主动踢掉当前同时在线的其他端, 需要传入 OnlineClient

NIMClient.getService(AuthService.class).kickOtherClient(onlineClient).setCallback(new RequestCallback<Void>() { ... });

当被其他端踢掉,可以通过在线状态观察者来接收监听消息:

NIMClient.getService(AuthServiceObserver.class).observeOnlineStatus(
    new Observer<StatusCode> () {
        public void onEvent(StatusCode status) {
            // 判断在线状态,如果为被其他端踢掉,做登出操作
        }
}, true);

网易云通信内置多端登录互踢策略为:移动端( Android 、 iOS )互踢,桌面端( PC 、 Web )互踢,移动端和桌面端共存( 可以采用上述 kickOtherClient 主动踢下共存的其他端)。

如果当前的互踢策略无法满足业务需求的话,可以联系我们取消内置互踢,根据多端登录的回调和当前的设备列表,判断本设备是否需要被踢出。如果需要踢出,直接调用登出接口并在界面上给出相关提示即可。

登出

如果用户手动登出,不再接收消息和提醒,开发者可以调用 logout 方法,该方法没有回调。

注意: 登出操作,不要放在 Activity(Fragment) 的 onDestroy 方法中。

NIMClient.getService(AuthService.class).logout();

离线查看数据

对于一些弱IM场景,需要在登录成功前或者未登录状态下访问指定账号的数据(聊天记录、好友资料等)。 SDK 提供两种方案:

  1. 使用自动登录。在登录成功前,可以访问 SDK 服务来读取本地数据(但不能发送数据)。

  2. 使用 AuthService#openLocalCache 接口打开本地数据,这是个同步方法,打开后即可读取 SDK 数据库中的记录。可以通过注销来切换账号查看本地数据。

NIMClient.getService(AuthService.class).openLocalCache(account);

断线重连机制

SDK 提供三种断线重连的策略(重新建立与网易云通信服务器的连接并重新登录):

1. 当网络由连通变为断开时,SDK 会启动立即上报网络断开的状态,并启动重连定时器,采用特定的策略并根据当前网络状态进行重连(如果 APP 处于后台,重连时间间隔会较长)。

2. SDK会监听设备的网络连接状况,当监听到手机断网重连上网络的通知后,会立即进行重连并登录。

3. 应用长时间处于后台(后台进程可能活着但网络连接被系统切断)后切回到前台(恢复网络连通),SDK 监测到当前处于未登录状态,会在短时间内进行重连。

实时会话(白板)

网易云通信提供数据通道(DATA/Audio)来满足实时会话的需求,如在线白板教学(参考demo)、文件传输等场景,其中 DATA 通道,可以同时存在多个,语音通道全局只能有一个。

服务器白板录制: 服务器会将用户发送的数据,每个成员录制到一个文件中。网易云通信 SDK 3.2版本之前,录制数据是纯裸数据录制;SDK 3.2 版本开始,针对用户发的每条数据前追加8字节数据,包括32位的长度 和 32位的时间戳。

实时会话配置

使用白板会话功能,需要在 AndroidManifest.xml 文件中声明一个接收器。

<!-- 申明白板会话的广播接收器,第三方APP集成时,action中的com.netease.nim.demo请替换为自己的包名 -->
<receiver
    android:name="com.netease.nimlib.receiver.RTSBroadcastReceiver"
    android:enabled="true"
    android:exported="false">
    <intent-filter>
        <action android:name="com.netease.nim.demo.ACTION.RECEIVE_RTS_NOTIFICATION"/>
    </intent-filter>
</receiver>

实时会话主流程

发起会话/创建数据通道(主叫方)

目前我们提供两种数据通道:DATA 通道和 Audio 通道。通道类型见 RTSTunnelType,可选参数见 RTSOptions(包含推送内容、发起方附带给其他参与者的内容、是否录制通道数据等) 一个会话可以同时创建多个通道,但全局只能有一个语音通道。发起会话时,RTSNotifyOption 中可进行iOS端推送通知自定义配置,同时也包含一个可自定的扩展字段。 例如:


List<RTSTunnelType> types = new ArrayList<>(1);
types.add(RTSTunnelType.AUDIO);
types.add(RTSTunnelType.DATA);

String pushContent = account + "发起一个会话";
String extra = "extra_data";
RTSOptions options = new RTSOptions().setRecordAudioTun(true).setRecordTCPTun(true);
RTSNotifyOption notifyOption = new RTSNotifyOption();

sessionId = RTSManager.getInstance().start(account, types, options, notifyOption, new RTSCallback<RTSData>() { ... });
if (sessionId == null) {
    // 发起会话失败,音频通道同时只能有一个会话开启
    onFinish();
}

start 接口返回的为会话 ID,开发者务必保存起来,下面调用该会话的相关接口都需要传入此 sessionId。注意,若返回 null,表示发起失败,原因是语音通道同时只能有一个会话开启。

监听会话请求/数据通道请求(被叫方)

一般是在 APP 启动时监听会话(新通道)请求,例如在 Application 的 onCreate 里添加。当监听到新通道请求时,会返回基本信息 RTSData,其中包括通道类型列表、发起方帐号。

private void registerRTSIncomingCallObserver(boolean register) {
    RTSManager.getInstance().observeIncomingSession(new Observer<RTSData>() {
        @Override
        public void onEvent(RTSData rtsData) {
            // 启动会话界面
        }
    },register);
}

监听该帐号其他端回应(被叫方)

如果自己的帐号有其他端在线(例如 PC 端),其他端做了回应(接受或者拒绝),那么移动端会收到一条通知。因此,移动端在收到会话请求后需要监听 PC 端对发起方的响应。

RTSManager.getInstance().observeOnlineAckNotification(sessionId, onlineAckObserver, register);
Observer<RTSOnlineAckEvent> onlineAckObserver = new Observer<RTSOnlineAckEvent>() {
    @Override
    public void onEvent(RTSOnlineAckEvent rtsOnlineAckEvent) {
        if (rtsOnlineAckEvent.getClientType() != ClientType.Android) {
            String client = null;
            switch (rtsOnlineAckEvent.getClientType()) {
                case ClientType.Web:
                    client = "Web";
                    break;
                case ClientType.Windows:
                    client = "Windows";
                    break;
                default:
                    break;
            }
            // your code
            onFinish();
        }
    };

监听主叫方结束会话(被叫方)

详见监听对方结束会话

接受会话(被叫方)

当监听到会话请求后,被叫方可以选择接受或者拒绝,可以传入可选参数:是否录制通道数据。当选择接受时,SDK 会自动开启相关的通道,建立与对方的连接。

RTSOptions options = new RTSOptions().setRecordAudioTun(true).setRecordTCPTun(true);
RTSManager.getInstance().accept(sessionId, options, new RTSCallback<Boolean>() { ... });

如果回调 onFailed 返回 -1,表示本地正在使用语音通道,不能同时存在两个语音通道。

拒绝会话(被叫方)

RTSManager.getInstance().close(sessionId, new RTSCallback<Void>() { ... });

监听被叫方回应(主叫方)

主叫方在发起会话成功后需要监听被叫方的回应,监听接口 observeCalleeAckNotification,回调返回 RTSCalleeAckEvent,其中包含被叫方的回应结果:

RTSManager.getInstance().observeCalleeAckNotification(sessionId, calleeAckEventObserver, register);
private Observer<RTSCalleeAckEvent> calleeAckEventObserver = new Observer<RTSCalleeAckEvent>() {
    @Override
    public void onEvent(RTSCalleeAckEvent rtsCalleeAckEvent) {
        if (rtsCalleeAckEvent.getEvent() == RTSEventType.CALLEE_ACK_AGREE) {
            // 判断SDK自动开启通道是否成功
            if (!rtsCalleeAckEvent.isTunReady()) {
                return;
            }
            // 进入会话界面
        } else if (rtsCalleeAckEvent.getEvent() == RTSEventType.CALLEE_ACK_REJECT) {
            // 被拒绝,结束会话
        }
    }
};

监听对方结束会话(主叫方、被叫方)

当被叫方收到会话请求时需要监听主叫方结束会话的通知;当双方会话建立之后,需要监听对方结束会话的通知。

RTSManager.getInstance().observeHangUpNotification(sessionId, endSessionObserver, register);
private Observer<RTSCommonEvent> endSessionObserver = new Observer<RTSCommonEvent>() {
    @Override
    public void onEvent(RTSCommonEvent rtsCommonEvent) {
        // 结束会话
    }
};

发送控制信息

双方会话建立之后,就可以相互发送控制信息了。

RTSManager.getInstance().sendControlCommand(sessionId, content, new RTSCallback<Void>() { ... });

监听会话控制通知

双方会话建立之后,需要监听会话控制通知。

RTSManager.getInstance().observeControlNotification(sessionId, controlObserver, register);

Observer<RTSControlEvent> controlObserver = new Observer<RTSControlEvent>() {
    @Override
    public void onEvent(RTSControlEvent rtsControlEvent) {
        // your code
    }
};

监听(发起)创建新通道或接受新通道超时通知

主叫方在(发起会话)创建通道时,超过 40 秒被叫方还未接受,则自动挂断。被叫方超过 40 秒接受会话,也会自动挂断。

Observer<RTSTimeOutEvent> timeoutObserver = new Observer<RTSTimeOutEvent>() {
    @Override
    public void onEvent(RTSTimeOutEvent rtsTimeOutEvent) {
        // 超时,结束会话
    }
};

监听数据通道的状态

发起会话(对方接受后),或者接受了会话请求后,需要立即注册对数据通道状态的监听。

RTSManager.getInstance().observeChannelState(sessionId, channelStateObserver, register);

RTSChannelStateObserver channelStateObserver = new RTSChannelStateObserver() {

    @Override
    public void onConnectResult(String localSessionId, RTSTunnelType tunType, long channelId, int code, String recordFile) {
        // 与服务器连接结果通知,成功返回 200, 同时返回服务器录制文件的地址
    }

    @Override
    public void onChannelEstablished(String localSessionId, RTSTunnelType tunType) {
        // 双方通道连接建立(对方用户已加入)
    }

    @Override
    public void onUserJoin(String localSessionId, RTSTunnelType tunType, String account) {
        // 用户加入
    }

    @Override
    public void onUserLeave(String localSessionId, RTSTunnelType tunType, String account, int event) {
        // 用户离开
    }

    @Override
    public void onDisconnectServer(String localSessionId, RTSTunnelType tunType) {
        // 与服务器断开连接
    }

    @Override
    public void onError(String localSessionId, RTSTunnelType tunType, int error) {
        // 通道发生错误
    }

    @Override
    public void onNetworkStatusChange(String localSessionId, RTSTunnelType channelType, int value) {
        // 网络信号强弱
    }
};

结束会话

发起结束通话的一方,调用 close 接口。另外一方,需要注册 observeHangUpNotification 监听挂断通知,收到通知后,做相应处理,代码示例见监听对方结束会话(主叫方、被叫方)

RTSManager.getInstance().close(sessionId, new RTSCallback<Void>() { ... });

数据收发

数据通道建立之后就可以进行数据的收发。

发送数据

发送数据,需要构造 RTSTunData, 需要指定会话 ID,通道类型,对方帐号,数据(字节数组)及数据的长度。如果需要发送数据到所有用户,对方帐号填 null。

RTSTunData channelData = new RTSTunData(sessionId, RTSTunnelType.DATA, toAccount, data.getBytes("UTF-8"), data.getBytes().length);
RTSManager.getInstance().sendData(channelData);

接收数据

数据通道建立完之后,就可以监听对方发送来的数据。

RTSManager.getInstance().observeReceiveData(sessionId, receiveDataObserver, register);
Observer<RTSTunData> receiveDataObserver = new Observer<RTSTunData>() {
    @Override
    public void onEvent(RTSTunData rtsTunData) {
        String data = "[parse bytes error]";
        try {
            data = new String(rtsTunData.getData(), 0, rtsTunData.getLength(), "UTF-8");
        } catch (UnsupportedEncodingException e) {
            e.printStackTrace();
        }

        TransactionCenter.getInstance().onReceive(sessionId, data);
    }
};

音频设备控制

在语音通道建立成功之后,可以对音频设备进行操作,目前开放的操作接口如下:

静音开关

开启静音后,对方将收不到语音。

RTSManager.getInstance().setMute(sessionId, true);

扬声器开关

语音默认在听筒播放,开启扬声器后,由听筒切换到扬声器。

RTSManager.getInstance().setSpeaker(sessionId, true);

多人实时会话

网易云通信提供数据通道(DATA)来满足实时会话的需求,如在线白板教学(参考demo)、文件传输等场景。 多人实时会话内部不包含 Audio 通道,不包含呼叫通知协议等。

推荐使用多人实时会话来实现各种需要实时数据传输的场景。

服务器白板录制: 服务器会将用户发送的数据,每个成员录制到一个文件中。网易云通信 SDK 3.2版本之前,录制数据是纯裸数据录制;SDK 3.2 版本开始,针对用户发的每条数据前追加8字节数据,包括32位的长度 和 32位的时间戳。

实时会话主流程

创建多人实时会话频道

创建一个多人实时会话频道,通过传入 sessionId 来表示频道名, extraMessage 创建会话时传入的附加信息,所有加入频道的用户都会收到此消息。 例如:

String sessionId = "sessionId"
String extraMessage = "extra msg";
RTSManager2.getInstance().createSession(sessionId, extraMessage, new RTSCallback<Void>() { ... });

加入多人实时会话频道

通过 sessionId 频道名来加入一个已经创建好的频道, 加入时可以指定 enableServerRecord 开决定是否录制传输数据。

String sessionId = "sessionId"
boolean enableServerRecord = true;
RTSManager2.getInstance().joinSession(sessionId, enableServerRecord,  new RTSCallback<RTSData>() { ... });

离开多人实时会话频道

通过 sessionId 频道名来离开一个已经加入的频道。

String sessionId = "sessionId"
RTSManager2.getInstance().leaveSession(String sessionId, new RTSCallback<Void>() { ... });

发送控制信息

双方会话建立之后,就可以相互发送控制信息了。

RTSManager.getInstance().sendControlCommand(sessionId, content, new RTSCallback<Void>() { ... });

监听会话控制通知

双方会话建立之后,需要监听会话控制通知。

RTSManager.getInstance().observeControlNotification(sessionId, controlObserver, register);

Observer<RTSControlEvent> controlObserver = new Observer<RTSControlEvent>() {
    @Override
    public void onEvent(RTSControlEvent rtsControlEvent) {
        // your code
    }
};

监听数据通道的状态

发起会话(对方接受后),或者接受了会话请求后,需要立即注册对数据通道状态的监听。

RTSManager.getInstance().observeChannelState(sessionId, channelStateObserver, register);

RTSChannelStateObserver channelStateObserver = new RTSChannelStateObserver() {

    @Override
    public void onConnectResult(String localSessionId, RTSTunnelType tunType, long channelId, int code, String recordFile) {
        // 与服务器连接结果通知,成功返回 200, 同时返回服务器录制文件的地址
    }

    @Override
    public void onChannelEstablished(String localSessionId, RTSTunnelType tunType) {
        // 双方通道连接建立(对方用户已加入)
    }

    @Override
    public void onUserJoin(String localSessionId, RTSTunnelType tunType, String account) {
        // 用户加入
    }

    @Override
    public void onUserLeave(String localSessionId, RTSTunnelType tunType, String account, int event) {
        // 用户离开
    }

    @Override
    public void onDisconnectServer(String localSessionId, RTSTunnelType tunType) {
        // 与服务器断开连接
    }

    @Override
    public void onError(String localSessionId, RTSTunnelType tunType, int error) {
        // 通道发生错误
    }

    @Override
    public void onNetworkStatusChange(String localSessionId, RTSTunnelType channelType, int value) {
        // 网络信号强弱
    }
};

数据收发

数据通道建立之后就可以进行数据的收发。

发送数据

发送数据,需要构造 RTSTunData, 需要指定会话 ID,通道类型,对方帐号,数据(字节数组)及数据的长度。如果需要发送数据到所有用户,对方帐号填 null。

RTSTunData channelData = new RTSTunData(sessionId, RTSTunnelType.DATA, toAccount, data.getBytes("UTF-8"), data.getBytes().length);
RTSManager.getInstance().sendData(channelData);

接收数据

数据通道建立完之后,就可以监听对方发送来的数据。

RTSManager.getInstance().observeReceiveData(sessionId, receiveDataObserver, register);
Observer<RTSTunData> receiveDataObserver = new Observer<RTSTunData>() {
    @Override
    public void onEvent(RTSTunData rtsTunData) {
        String data = "[parse bytes error]";
        try {
            data = new String(rtsTunData.getData(), 0, rtsTunData.getLength(), "UTF-8");
        } catch (UnsupportedEncodingException e) {
            e.printStackTrace();
        }

        TransactionCenter.getInstance().onReceive(sessionId, data);
    }
};

音频设备控制

在语音通道建立成功之后,可以对音频设备进行操作,目前开放的操作接口如下:

静音开关

开启静音后,对方将收不到语音。

RTSManager.getInstance().setMute(sessionId, true);

扬声器开关

语音默认在听筒播放,开启扬声器后,由听筒切换到扬声器。

RTSManager.getInstance().setSpeaker(sessionId, true);

文档转码

网易云通信提供文档转码服务,可用于在线教育、多人会议等场景,文档转码接口包含文档查询和文档删除,在文档查询的结果中有转码后文档的详细信息和下载地址,开发者可以通过下载地址自行下载后进行视觉展现。

文档分页查询

文档分页查询协议,只有文档的所有者有权限进行查询,传入文档documentId来表示查询的起始文档的Id,若为空,表示从头开始查找,按照文档转码的发起时间降序排列,传入limit来表示查询的文档的最大数目,有最大值限制,目前为30,在回调结果中对查询结果处理。代码示例如下:

DocumentManager.getInstance().queryDocumentDataList(documentId, limit, new RequestCallback<List<DMData>>() { ... });

单个文档查询

单个文档的查询协议,应用内的所有用户都可以查询,传入文档documentId来表示文档的id,在回调结果中对查询结果进行处理。代码示例如下:

DocumentManager.getInstance().querySingleDocumentData(documentId, new RequestCallback<DMData>() { ... });

单个文档删除

单个文档删除协议(对于正在转码中的文档,删除后将不会收到转码结果的下发),传入documentId来表示需要删除的文档id,在回调结果中对删除结果进行处理。代码示例如下:

DocumentManager.getInstance().delete(documentId, new RequestCallback<Void>() { ... });