音视频通话 Android 端开发指南

SDK 概述

网易云通信 SDK 提供完善的音视频通话开发框架,提供基于网络的点对点视频通话和语音通话功能,还提供多人视频和音频会议功能,支持通话前预览,支持通话中音视频设备控制和实时音视频模式切换,支持音视频采集数据回调以实现美颜和变声等自定义功能。实现音视频通话功能需要用到登录认证服务和实时语音视频通话服务。

开发准备

网易云通信 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:avchat:3.3.0'
}

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

通过类库配置集成SDK

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

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

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

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

libs
├── arm64-v8a
│   ├── libvideoeffect.so (视频处理)
│   ├── libnrtc_engine.so (音视频基础服务底层库)
│   └── libnrtc_network.so (音视频基础服务底层库)
├── armeabi-v7a
│   ├── libvideoeffect.so
│   ├── libnrtc_engine.so
│   └── libnrtc_network.so
├── x86
│   ├── libvideoeffect.so 
│   ├── libnrtc_engine.so
│   └── libnrtc_network.so
├── x86_64
│   ├── libvideoeffect.so 
│   ├── libnrtc_engine.so
│   └── libnrtc_network.so
│
├── nim-basesdk-3.3.0.jar (云通信SDK基础服务)
├── nim-avchat-3.3.0.jar (音视频服务)
├── video_effect.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 监测到当前处于未登录状态,会在短时间内进行重连。

语音视频通话

网易云通信提供基于网络的语音、视频聊天功能。支持通话中音视频设备的控制,并支持音视频切换。

注意: 语音视频通话需要 Android 4.1 及以上版本的系统。

语音视频通话配置

使用音视频功能,需要在 AndroidManifest.xml 文件中配置接收器。

<!-- 申明本地电话状态(通话状态)的广播接收器,第三方APP集成时音视频模块时,如果需要在App中处理网络通话与本地电话的交互请加上此接收器 -->
<!-- 在Demo的示例代码中是在Application进行了网络通话与本地电话的互斥处理 -->
<receiver android:name="com.netease.nim.demo.avchat.receiver.IncomingCallReceiver">
    <intent-filter>
        <action android:name="android.intent.action.PHONE_STATE"/>
    </intent-filter>
</receiver>

双人语音视频通话流程

开启音视频引擎

视频或者语音通话都需要先开启音视频引擎。

AVChatManager.getInstance().enableRtc();

场景模式设置

设置默认参数,如果对音乐有特殊需求,可以设置高清音乐。 参考后续场景介绍章节。

AVChatManager.getInstance.setChannelProfile(CHANNEL_PROFILE_DEFAULT);

设置通话可选参数

设置通话可选参数,包括视频质量控制、服务器录制以及一些其它可选参数,可以根据自己的需求在通话前选择性的设置,也可以不设置使用默认参数。

 AVChatManager.getInstance().setParameters(avChatParameters);

视频通话设置

如果是视频通话,需要打开视频模块、设置双方的画布,打开视频预览,语音通话不需要进行这些设置。

//打开视频模块。如果你需要和视频相关的操作,则需要激活视频模块
AVChatManager.getInstance().enableVideo();
//视频视频预览画布
AVChatManager.getInstance().setupLocalVideoRender(IVideoRender render, boolean mirror, int scalingType);
//设置视频采集模块,新版本中可以自定义视频采集模块
AVChatCameraCapturer videoCapturer = AVChatVideoCapturerFactory.createCameraCapturer();
AVChatmanager.getInstance().setupVideoCapturer(videoCapturer);
//打开视频预览
AVChatManager.getInstance().startVideoPreview();

发起通话(主叫方)

音视频发起通话是持续呼叫的,不管被叫方是在线还是离线都对其持续进行呼叫。

会话类型参数 AVChatTypeEnum 主要分为语音通话和视频通话。

会话通知参数 AVChatNotifyOption 包含iOS的通知配置, WebRTC兼容以及可自定义的扩展消息。目前WebRTC为 Beta 版本,如果没有WebRTC客户端参与时不要设置WebRTC相关参数

在发起通话前进行上述的开启音视频引擎、设置通话可选参数、视频通话设置等初始化设置,然后发起通话。

AVChatManager.getInstance().call2(account, callTypeEnum, notifyOption, new AVChatCallback<AVChatData>() { ... });

监听来电(被叫方)

一般是在 APP 启动时注册来电监听,例如在 Application 的 onCreate 里添加。当监听到来电时,会返回来电信息 AVChatData,其中包含呼叫方式(音频或者视频)、来电帐号。

private void enableAVChat() {
    registerAVChatIncomingCallObserver(true);
}

private void registerAVChatIncomingCallObserver(boolean register) {
    AVChatManager.getInstance().observeIncomingCall(new Observer<AVChatData>() {
        @Override
        public void onEvent(AVChatData data) {
            String extra = data.getExtra();
            Log.e("Extra", "Extra Message->" + extra);
            if (PhoneCallStateObserver.getInstance().getPhoneCallState() != PhoneCallStateObserver.PhoneCallStateEnum.IDLE
                    || AVChatProfile.getInstance().isAVChatting()
                    || AVChatManager.getInstance().getCurrentChatId() != 0) {
                LogUtil.i(TAG, "reject incoming call data =" + data.toString() + " as local phone is not idle");
                AVChatManager.getInstance().sendControlCommand(data.getChatId(), AVChatControlCommand.BUSY, null);
                return;
            }
            // 有网络来电打开AVChatActivity
            AVChatProfile.getInstance().setAVChatting(true);
            AVChatActivity.launch(DemoCache.getContext(), data, AVChatActivity.FROM_BROADCASTRECEIVER);
        }
    }, register);
}

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

如果自己的帐号有其他端在线(PC、Web),来电会被其他端做了回应,那么移动端会收到一条通知。因此,移动端在收到来电后需要监听 PC 端对主叫方的响应。

AVChatManager.getInstance().observeOnlineAckNotification(onlineAckObserver, register);
Observer<AVChatOnlineAckEvent> onlineAckObserver = new Observer<AVChatOnlineAckEvent>() {
        @Override
        public void onEvent(AVChatOnlineAckEvent ackInfo) {
            if (ackInfo.getClientType() != ClientType.Android) {
                String client; // 做回应的客户端
                switch (ackInfo.getClientType()) {
                    ...
                    case ClientType.Windows:
                        client = "Windows";
                        break;
                    default:
                        break;
                }
                // your code
                avChatUI.closeSessions();
            }
        }
    };

监听主叫方挂断(被叫方)

详见监听对方挂断

同意接听(被叫方)

当监听到来电后启动通话界面,被叫方可以选择接听或者拒绝。当选择接听时,同样需要在接听通话前进行前面的开启音视频引擎、设置通话可选参数、视频通话设置等初始化设置,然后接听通话,建立通话连接。

注意:由于音视频引擎析构需要时间,请尽可能保证上一次通话挂断到本次电话接听时间间隔在2秒以上,否则有可能在接听时出现初始化音视频引擎失败,此问题后期会进行优化。

AVChatManager.getInstance().accept2(avChatData.getChatId(), new AVChatCallback<Void>() { ... });

拒绝接听(被叫方)

AVChatManager.getInstance().hangUp2(avChatData.getChatId(),new AVChatCallback<Void>() { ... });

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

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

AVChatManager.getInstance().observeCalleeAckNotification(callAckObserver, register);
Observer<AVChatCalleeAckEvent> callAckObserver = new Observer<AVChatCalleeAckEvent>() {
        @Override
        public void onEvent(AVChatCalleeAckEvent ackInfo) {
            if (ackInfo.getEvent() == AVChatEventType.CALLEE_ACK_BUSY) {
                // 对方正在忙
            } else if (ackInfo.getEvent() == AVChatEventType.CALLEE_ACK_REJECT) {
                // 对方拒绝接听
            } else if (ackInfo.getEvent() == AVChatEventType.CALLEE_ACK_AGREE) {
                // 对方同意接听
            }
        }
    };

监听对方挂断(主叫方、被叫方)

当被叫方收到来电时(在通话建立之前)需要监听主叫方挂断通知,当双方通话建立之后,都需要监听对方挂断通知来结束本次通话。

AVChatManager.getInstance().observeHangUpNotification(callHangupObserver, register);
Observer<AVChatCommonEvent> callHangupObserver = new Observer<AVChatCommonEvent>() {
        @Override
        public void onEvent(AVChatCommonEvent hangUpInfo) {
            // 结束通话
        }
    };

发送通话控制通知

通话时发送控制命令,在通话控制通知中处理对方发来的不同的命令。

AVChatManager.getInstance().sendControlCommand(chatId, controlCommand, new AVChatCallback<Void>() { ... });

请求音视频切换

双方通话建立之后,就可以发起音视频切换请求,音视频切换使用发送控制命令接口进行实现。发送音频到视频的切换请求,对方需要同意才能切换成功;发送视频到音频的切换请求,对方默认同意,自动切换。

// 请求音频切换到视频
AVChatManager.getInstance().sendControlCommand(avChatData.getChatId(), AVChatControlCommand.SWITCH_AUDIO_TO_VIDEO, new AVChatCallback<Void>() { ...});

// 请求视频切换到音频
AVChatManager.getInstance().sendControlCommand(avChatData.getChatId(), AVChatControlCommand.SWITCH_VIDEO_TO_AUDIO, new AVChatCallback<Void>() {
    @Override
    public void onSuccess(Void aVoid) {
    //关闭视频
    AVChatManager.getInstance().stopVideoPreview();
    AVChatManager.getInstance().disableVideo();
    ...
    }
    @Override
    public void onFailed(int code) {

    }
    @Override
    public void onException(Throwable exception) {

    }
});

音视频切换请求的回应

一方发送音视频切换请求之后,对方会收到通知,见一下小节。当收到对方音频切换到视频的请求时,用户可以选择同意或者拒绝。对方将收到应答结果的通知,见下一小节。

// 同意音频切换到视频
AVChatManager.getInstance().sendControlCommand(avChatData.getChatId(), AVChatControlCommand.SWITCH_AUDIO_TO_VIDEO_AGREE, new AVChatCallback<Void>() {
    @Override
    public void onSuccess(Void aVoid) {
    // 切换操作
    // 打开视频
    AVChatManager.getInstance().enableVideo();
    AVChatManager.getInstance().startVideoPreview();
    // 是否在发送视频 即摄像头是否开启
    if (AVChatManager.getInstance().isLocalVideoMuted()) {
        AVChatManager.getInstance().muteLocalVideo(false);
        ...
    }
    ...
    }
    @Override
    public void onFailed(int code) {

    }
    @Override
    public void onException(Throwable exception) {

    }
});

// 拒绝音频切换到视频
AVChatManager.getInstance().sendControlCommand(avChatData.getChatId(), AVChatControlCommand.SWITCH_AUDIO_TO_VIDEO_REJECT, new AVChatCallback<Void>() { ...});

监听通话控制通知

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

AVChatManager.getInstance().observeControlNotification(callControlObserver, register);

Observer<AVChatControlEvent> callControlObserver = new Observer<AVChatControlEvent>() {
        @Override
        public void onEvent(AVChatControlEvent event) {
            handleCallControl(event);
        }
    };

private void handleCallControl(AVChatControlEvent event) {
        switch (event.getControlCommand()) {
            case SWITCH_AUDIO_TO_VIDEO:
                // 对方请求切换音频到视频
                break;
            case SWITCH_AUDIO_TO_VIDEO_AGREE:
                // 对方同意切换音频到视频
                // 切换操作
                // 打开视频
                AVChatManager.getInstance().enableVideo();
                AVChatManager.getInstance().startVideoPreview();
                // 是否在发送视频 即摄像头是否开启
                if (AVChatManager.getInstance().isLocalVideoMuted()) {
                    AVChatManager.getInstance().muteLocalVideo(false);
                    ...
                }
                ...
                break;
            case SWITCH_AUDIO_TO_VIDEO_REJECT:
                // 对方拒绝切换音频到视频
                break;
            case SWITCH_VIDEO_TO_AUDIO:
                // 对方请求视频切换到音频
                //关闭视频
                AVChatManager.getInstance().stopVideoPreview();
                AVChatManager.getInstance().disableVideo();
                ...
                break;
            case NOTIFY_VIDEO_OFF:
                // 对方关闭视频的通知
                break;
            case NOTIFY_VIDEO_ON:
                // 对方开启视频的通知
                break;
            default:
                break;
        }
    }

监听呼叫或接听超时通知

主叫方在拨打网络通话时,超过 45 秒被叫方还未接听来电,则自动挂断。被叫方超过 45 秒未接听来听,也会自动挂断,在通话过程中网络超时 30 秒自动挂断。未来不再提供此接口,用户可以自己实现超时挂断通话。

AVChatManager.getInstance().observeTimeoutNotification(timeoutObserver, register);
Observer<AVChatTimeOutEvent> timeoutObserver = new Observer<AVChatTimeOutEvent>() {
    @Override
    public void onEvent(AVChatTimeOutEvent event) {
            // 超时类型
        }
    }
};

结束通话

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

// 如果是视频通话,需要先关闭本地预览
AVChatManager.getInstance().stopVideoPreview();
AVChatManager.getInstance().hangUp2(avChatData.getChatId(), new AVChatCallback<Void>() {};
AVChatManager.getInstance().disableRtc();

多人语音视频通话流程

创建多人会话房间

通过一个房间名 roomName 来创建多人会话房间。

可以传入一个扩展字段 extraMessage。 后续加入房间的用户会收到这个扩展字段。

如果需要兼容 Web 端音视频可以设置字段 webRTCCompat目前WebRTC为 Beta 版本,如果没有WebRTC客户端参与时不要设置WebRTC相关参数

AVChatManager.getInstance().createRoom(roomName, extraMessage, new AVChatCallback<AVChatChannelInfo>() {});

AVChatManager.getInstance().createRoom(roomName, extraMessage, webRTCCompat, new AVChatCallback<AVChatChannelInfo>() {});

加入多人会话房间

通过一个房间名 roomName 来加入一个已经创建好的多人会话房间。

加入房间时需要指定自己的会话类型 AVChatType。 主要为音频通话和视频通话两种。

加入房间前需要进行和双人通话流程中类似的开启音视频引擎、设置通话可选参数、视频通话设置等初始化设置。

//开启音视频引擎
AVChatManager.getInstance().enableRtc();
//设置场景, 如果需要高清音乐场景,设置 AVChatChannelProfile#CHANNEL_PROFILE_HIGH_QUALITY_MUSIC
AVChatManager.getInstance.setChannelProfile(CHANNEL_PROFILE_DEFAULT);
//设置通话可选参数
AVChatParameters parameters = new AVChatParameters();
AVChatManager.getInstance().setParameters(parameters);
//视频通话设置
AVChatManager.getInstance().enableVideo();
AVChatManager.getInstance().setupLocalVideoRender(IVideoRender render, boolean mirror, int scalingType);
//设置视频采集模块
AVChatCameraCapturer videoCapturer = AVChatVideoCapturerFactory.createCameraCapturer();
AVChatManager.getInstance().setupVideoCapturer(videoCapturer);
//开启视频预览
AVChatManager.getInstance().startVideoPreview();
//加入房间
AVChatManager.getInstance().joinRoom2(roomName, callType, new AVChatCallback<AVChatData>() {};

离开多人会话房间

离开一个已经加入的多人会话房间。

//关闭视频预览
AVChatManager.getInstance().stopVideoPreview();
//离开房间
AVChatManager.getInstance().leaveRoom2(roomName, new AVChatCallback<Void>() {};
//关闭音视频引擎
AVChatManager.getInstance().disableRtc();

通话状态监听

实现 AVChatStateObserver 监听通话过程中状态变化。被叫方同意来电请求后,SDK 自动进行音视频服务器连接,并返回相应信息供上层应用使用。

public class AVChatActivity implements AVChatStateObserver {
    AVChatManager.getInstance().observeAVChatState(this, register);
}

当前音视频服务器连接回调

首先返回服务器连接是否成功的回调 onJoinedChannel,并通过返回的 result code 做相应的处理。

参数 code 返回加入频道是否成功。常见错误码参考 JoinChannelCode 参数 filePath fileName 在开启服务器录制的情况下返回录制文件的保存路径。

@Override
public void onJoinedChannel(int code, String filePath, String fileName, int elapsed) { }

加入当前音视频频道用户帐号回调

其他用户音视频服务器连接成功后,会回调 onUserJoined,可以获取当前通话的用户帐号。

@Override
public void onUserJoined(String account) {
// 如果是视频通话,需要设置远端用户视频画布
AVChatManager.getInstance().setupRemoteVideoRender(String account, IVideoRender render, boolean mirror, int scalingType);
}

当前用户离开频道回调

通话过程中,若有用户离开,则会回调 onUserLeave

// @param event   -1,用户超时离开  0,正常退出
@Override
public void onUserLeave(String account, int event) {}

自己成功离开频道回调

@Override
public void onLeaveChannel() {}

版本协议不兼容回调

若语音视频通话双方软件版本不兼容,则会回调 onProtocolIncompatible

// @param status 0 自己版本过低  1 对方版本过低
@Override
public void onProtocolIncompatible(int status) {}

服务器断开回调

通话过程中,从服务器断开连接, 当自己断网超时后,会回调 onDisconnectServer

@Override
public void onDisconnectServer() {}

当前通话网络状况回调

通话过程中网络状态发生变化,会回调 onNetworkQuality

// @param value 0~3 ,the less the better; 0 : best; 3 : worst
@Override
public void onNetworkQuality(String account, int value, AVChatNetworkStats stats) {}

音视频连接成功建立回调

音视频连接建立,会回调 onCallEstablished。音频切换到正在通话的界面,并开始计时等处理。视频则通过为用户设置对应画布并添加到相应的 layout 上显示图像。

@Override
public void onCallEstablished() {
    if (state == AVChatTypeEnum.AUDIO.getValue()) {
        aVChatUIManager.onCallStateChange(CallStateEnum.AUDIO);
    } else {
        aVChatUIManager.initSmallSurfaceView();
        aVChatUIManager.onCallStateChange(CallStateEnum.VIDEO);
    }
    isCallEstablished = true;
}

音视频设备状态通知

音视频设备状态发生改变时,会回调 onDeviceEvent

@Override
public void onDeviceEvent(String account, int code, String desc) {}

截图结果回调

用户执行截图后会回调 onTakeSnapshotResult

@Override
public void onTakeSnapshotResult(String account, boolean success, String file) {}

本地网络类型发生改变回调

本地客户端网络类型发生改变时回调,会通知当前网络类型。

@Override
public void onConnectionTypeChanged(int netType) {}

音视频录制回调

当用户录制音视频结束时回调,会通知录制的用户id和录制文件路径。

@Override
void onAVRecordingCompletion(String account, String filePath) {}

当用户录制语音结束时回调,会通知录制文件路径。

@Override
void onAudioRecordingCompletion(String filePath) {}

当存储空间不足时的警告回调,存储空间低于20M时开始出现警告,出现警告时请及时关闭所有的录制服务,当存储空间低于10M时会自动关闭所有的录制。

@Override
void onLowStorageSpaceWarning(long availableSize) {}

用户第一帧画面通知

当用户第一帧视频画面绘制前通知。

@Override
public void onFirstVideoFrameAvailable(String account) {}

用户第一帧画面绘制后通知

当用户第一帧视频画面绘制后通知。

@Override
public void onFirstVideoFrameRendered(String user) {}

用户视频画面分辨率改变通知

当用户视频画面的分辨率改变时通知。

@Override
public void onVideoFrameResolutionChanged(String user, int width, int height, int rotate) {}

用户视频帧率汇报

实时汇报用户的视频绘制帧率。

@Override
public void onVideoFpsReported(String account, int fps) {}

采集视频数据回调

当用户开始外部视频处理后,采集到的视频数据通过次回调通知。 用户可以对视频数据做相应的美颜等不同的处理。 需要通过setParameters开启视频数据处理。

@Override
public boolean onVideoFrameFilter(AVChatVideoFrame frame, boolean maybeDualInput)) {}

采集语音数据回调

当用户开始外部语音处理后,采集到的语音数据通过次回调通知。 用户可以对语音数据做相应的变声等不同的处理。需要通过setParameters开启语音数据处理。

@Override
public boolean onAudioFrameFilter(AVChatAudioFrame frame) {}

语音播放设备变化通知

当用户切换扬声器或者耳机的插拔等操作时, 语音的播放设备都会发生变化通知。 语音设备参考 AVChatAudioDevice

@Override
public void onAudioOutputDeviceChanged(int device) {}

语音正在说话用户声音强度通知

正在说话用户的语音强度回调,包括自己和其他用户的声音强度。如果一个用户没有说话,或者说话声音小没有被参加到混音,那么这个用户的信息不会在回调中出现。

@Override
void onReportSpeaker(Map<String, Integer> speakers, int mixedEnergy) {}

伴音事件通知

当伴音出错或者结束时,通过此回调进行通知

@Override
void onAudioMixingEvent(int event) {}

实时统计信息汇报

通过此回调通知实时统计信息

@Override
void onSessionStats(AVChatSessionStats sessionStats) {}

互动直播事件通知

通过此回调进行通知互动直播事件

@Override
void onLiveEvent(int event) {}

本地视频前处理

SDK的发布包中打包有滤镜SDK,开发者可以根据需要接入滤镜功能,轻松实现磨皮、滤镜、静态水印和动态水印功能。API文档

滤镜模块需要运行在GLES 3.0及以上版本的系统中,对应API版本为API >= 18

滤镜的依赖包括video_effect.jar,libvideoeffect.so以及对应的一些assets资源,这些资源都位于sdk目录下。

接入时,首先需要将以上三个资源分别拷贝到对应的工程结构目录中,并在采集视频数据的回调方法中自定义滤镜功能,该回调需要设置对应参数才会触发。

    boolean onVideoFrameFilter(final AVChatVideoFrame frame, final boolean maybeDualInput);

滤镜模块的功能实现由VideoEffect类提供,使用前需要通过VideoEffectFactory.getVCloudEffect创建一个对应实例。滤镜的主要API列举如下:

public void init(Context context, boolean useFilter, boolean hasGLContext);

context:应用上下文,建议使用ApplicationContext

useFilter:是否使用滤镜功能

context:调用线程是否具有EGLContext。如果当前线程没有EGLContext,那么该接口会创建一个。后续所有的滤镜API必须保证在同一个具有GL Context的线程中调用

注意:滤镜的功能需要在onVideoFrameFilter方法中同步处理,因此该初始化方法也需要在该方法中进行完成调用。

 public abstract void setFilterType(FilterType type);
 public abstract void setFilterLevel(float level);
 public void addWaterMark(Bitmap bitmap, int x, int y);

bitmap:水印图片,当为null值时,会清除之前设置的水印

x:水印水平坐标X,以图像左上角为基准点

y:水印垂直坐标Y,以图像左上角为基准点

 public void addDynamicWaterMark(Bitmap[] bitmapArray, int x, int y, int fps, int cameraFps, boolean looped);

bitmapArray:水印图片数组

x:水印水平坐标X,以图像左上角为基准点

y:水印垂直坐标Y,以图像左上角为基准点

fps:动态水印的帧率

cameraFps:相机采集的帧率

looped:是否循环播放

 public void closeDynamicWaterMark(boolean on);
 public void addGraffiti(Bitmap bitmap, int x, int y);
 public abstract void setBeautyLevel(int level);
 public abstract byte[] filterBufferToRGBA(DataFormat format,byte[] data, int width, int height);

format:输入的数据格式

data:待处理图片原始数据,使用AVChatVideoFrame.data的数据

width:图片宽度,使用AVChatVideoFrame.width

height:图片高度,使用AVChatVideoFrame.height

对图片原始数据进行滤镜处理,并返回RGBA格式的数据,如果开启了滤镜,则每一帧都需要调用该方法

 public YUVData[] TOYUV420(byte[] src, DataFormat dataFormat, int inWidth, int inHeight, int cameraRotation,
                               int displayOrientation, int outWidth, int outHeight, boolean needMirrorData,boolean autoEffect)

src:待处理的数据

dataFormat:待处理数据的格式

inWidth:输入图片的宽

inHeight:输入图片的高

cameraRotation:输入图片的旋转角度

displayOrientation:计算后的显示角度,使用固定值90

outWidth:输出图片的宽,一般与输入相同,否则会裁剪

outHeight:输出图片的高,一般与输入相同

needMirrorData:是否需要镜像后的数据(主要用于前置摄像头下水印、涂鸦等本地镜像显示和编码数据的区别),一般设置为onVideoFrameFilter方法的第二个参数

autoEffect:是否自动添加上水印、涂鸦等,一般为true,否则需要手动处理

该方法将输入的原始视频数据转为YUV格式数据,同时会根据autoEffect配置是否为true自动进行水印、涂鸦、旋转等操作。

方法会返回两路数据,但只有第一路数据是始终有效的,并需要将之拷贝至AVChatVideoFrame.data中去。

仅当needMirrorDatatrue时,第二路数据有效,并需要将第二路数据拷贝至AVChatVideoFrame.dataMirror中,并设置AVChatVideoFrame.dualInputtrue

 public void unInit();

通话中的设备控制

通话进行中,可以进行设备静音,扬声器,摄像头切换,开关摄像头和切换通话模式的设置。

通话中实时设置参数

在通话过程中, 可以实时设置部分参数。用户可以通过这些参数进行软硬件编解码切换,清晰度切换等。

AVChatParameters params = new AVChatParameters();
params.setBoolean(AVChatParameters.KEY_VIDEO_FPS_REPORTED, false);
AVChatManager.getInstance().setParameters(params);

通话中实时获取参数

在通话过程中, 可以实时获取部分参数。

AVChatParameters params = new AVChatParameters();
params.requestKey(AVChatParameters.KEY_VIDEO_FPS_REPORTED);
AVChatManager.getInstance().getParameters(params);

设置视频采集模块

目前我们提供了独立的视频采集模块, 使用 AVChatCameraCapturer 来控制Camera视频源。 Camera 视频源提供了 切换摄像头,对焦设置,焦距调整等功能。

通过AVChatVideoCapturerFactory来创建视频采集模块, 调用AVChatManager#setupVideoCapturer把视频源设置到SDK内部。

//切换摄像头
public abstract int switchCamera();
//多摄像头判断 
public static boolean hasMultipleCameras();
//手动对焦
public abstract void setFocus();
//自动对焦
public abstract void setAutoFocus(boolean isAutoFocus);
//焦距调整
public abstract void setZoom(int zoomValue);
//获取当前焦距
public abstract int getCurrentZoom();
//获取最大焦距
public abstract int getMaxZoom();
//前置摄像头闪光灯控制
public abstract int setFlash(boolean flash);

设置视频绘制画布

目前支持三种画布方式,使用内置的 AVChatSurfaceViewRendererAVChatTextureViewRenderer 或者自定义实现 AVChatExternalVideoRender

当设置 AVChatSurfaceViewRendererAVChatTextureViewRenderer 为用户的视频画布时,同时还可以制定画布是否镜像处理,以及相应的缩放模式。

当设置 AVChatExternalVideoRender 为用户的视频画布时,镜像和缩放方式会忽略,用户需要自己去绘制 I420 原始视频数据。

对于交换用户画布的操作,需要先把当前用户的画布解除,通过此接口传入null即可解除,然后再设置新的画布。

如果需要启动开启会话后立即预览本地视频数据,在加入通话前调用 setupLocalVideoRender 即可。

// 设置本地用户视频画布
AVChatManager.getInstance().setupLocalVideoRender(IVideoRender render, boolean mirror, int scalingType);
// 设置远端用户视频画布
AVChatManager.getInstance().setupRemoteVideoRender(String account, IVideoRender render, boolean mirror, int scalingType);

自定义音频数据输入

SDK支持外部自定义音频数据输入,用户可以从文件或者其他设备读取语音PCM数据。

仅支持PCM数据, 通过参数 AVChatParameters#KEY_AUDIO_EXTERNAL_CAPTURE 来启用外部自定义语音数据输入, 然后使用 AVChatManager#pushExternalAudioData 输入外部语音数据。语音数据长度尽量不要超过 40ms, 数据时间戳需要精确到毫秒, 如果需要内部自动生成时间戳 timeMs 可以设置为 -1.

public void setParameters(AVChatParameters params);
public int pushExternalAudioData(byte[] data,
                                 int samples,
                                 int sampleRate,
                                 int channel,
                                 int bytesPerSample,
                                 long timeMs);

自定义视频数据输入

SDK支持自定义外部视频数据输入。开发者根据自己的需求,只需从文件或相机等外部视频源读入或采集视频数据,并通过SDK提供的对应接口提交每一帧的视频数据即可,SDK会负责完成余下的工作。具体的,开发者需要继承于AVChatExternalVideoCapturer,重写类中相关方法并使用类中提供的Observer接口提交视频帧数据。开发者首先需要将AVChatExternalVideoCapturer的自定义实例通过AVChatManager#setupVideoCapturer方法进行设置,以便SDK能向其中注册Observer接口供开发者使用,该接口的方法说明如下:

AVChatExternalVideoCapturer作为自定义外部视频输入的父类使用,开发者在具体实现时只需要关注以下这些方法:

设置本地语音流静音

将设置本地发送语音是否静音。

AVChatManager.getInstance().muteLocalAudio(true);

设置远端用户语音流静音

将设置是否播放其他用户的语音数据。

AVChatManager.getInstance().muteRemoteAudio(account, true);

设置本地视频流静音

设置本地视频数据是否发送。

AVChatManager.getInstance().muteLocalVideo(true);

设置远端用户视频流静音

将设置是否绘制远端用户的视频数据。

AVChatManager.getInstance().muteRemoteVideo(account, true);

设置扬声器

// 设置扬声器是否开启
AVChatManager.getInstance().setSpeaker(!AVChatManager.getInstance().speakerEnabled());

切换通话模式

具体见请求音视频切换音视频切换请求的回应

客户端本地录制音视频数据

通话进行中,可以录制用户的音频和视频数据, 文件将以MP4格式保存在客户端本地, 也可以录制所有用户的语音数据,录音文件格式为wav,文件保存在客户端本地。程序卸载时录制的本地文件也会随程序一并删除。

客户端本地开始音视频录制接口,通过返回值判断是否调用成功。录制account的音频和视频文件,前后摄像头切换时录制文件可能存在多个。

// 开始录制用户的音视频数据
AVChatManager.getInstance().startAVRecording(account);

客户端本地停止音视频录制接口, 停止录制后将会通过回调函数返回结果。

// 停止录制用户的音视频数据
AVChatManager.getInstance().stopAVRecording(account);

客户端本地开始录音接口,包含所有用户的语音数据,录音文件格式为wav,文件保存在客户端本地。

// 开始录音
AVChatManager.getInstance().startAudioRecording();

客户端本地停止录音接口,停止录制后将会通过回调函数返回结果。

// 停止录音
AVChatManager.getInstance().stopAudioRecording();

录制结束后会通过网络通话状态通知告知。

/**
 * 用户音视频数据录制结束
 *
 * @param account 用户账号
 * @param filePath 录制文件路径,当发生视频清晰度等情况时会存在多个MP4文件
*/
void onAVRecordingCompletion(String account, String filePath);
/**
 * 语音录制结束
 *
 * @param filePath 录制语音文件路径
*/
void onAudioRecordingCompletion(String filePath);
/**
 * 存储空间不足警告,存储空间低于20M时开始出现警告,出现警告时请及时关闭所有的录制服务,当存储空间低于10M时会自动关闭所有的录制功能
 *
 * @param availableSize 可用空间
*/
void onLowStorageSpaceWarning(long availableSize);

视频画面截图

截取指定用户的视频画面, 截图结果将会通过 onTakeSnapshotResult 回调通知。

AVChatManager.getInstance().takeSnapshot(account);

多人模式观众角色设置

是否打开观众角色, 设置观众角色后所有的语音和视频数据的采集和发送会关闭,仅允许接收和播放远端其他用户的数据。

AVChatManager.getInstance().enableAudienceRole(true);

本地语音伴音

在通话过程中,可以指定本地音频文件来和麦克风采集的音频流进行混音或者替换。

开启本地语音伴音,可以通过参数指定是否循环,替换本地语音,以及初始音量[0.0f - 1.0f]。 伴音的状态通知 onDeviceEvent,事件类型为 AUDIO_MIXING_ERROR 以及 AUDIO_MIXING_FINISHED.

AVChatManager.getInstance().startAudioMixing(filePath, loopback, replace, cycle, volume);

暂停本地语音伴音

AVChatManager.getInstance().pauseAudioMixing();

恢复本地语音伴音,伴音文件将从暂停时位置开始播放

AVChatManager.getInstance().resumeAudioMixing();

停止本地语音伴音

AVChatManager.getInstance().stopAudioMixing();

本地语音伴音音量,通过参数 KEY_AUDIO_MIXING_STREAM_VOLUME 来调整伴音音量,音量范围[0.0f - 1.0f].

AVChatManager.getInstance().setParameters(param);

网络通话可选参数

网络通话可选参数分为通话前可设置参数以及通话过程中可设置参数。 详细参数定义参考 AVChatParameters参数介绍

参数设置接口:

AVChatManager.getInstance().setParameters(param);
AVChatManager.getInstance().setParameter(Key<T> key, T value);
AVChatParameters param = AVChatManager.getInstance().getParameters(params);
T  AVChatManager.getInstance().getParameter(Key<T> key);

网络通话其它接口

权限检查

在Android 6.0 及以上系统中提供网络通话权限检查,需要保证所有权限获取后再进行网络通话。

//返回缺失的权限
AVChatManager.getInstance().checkPermission(context);

网络探测

在通话前或者通话中进行网络探测,用于检测用户当前网络的接入质量。

开启网络探测,返回本次任务的ID

AVChatNetDetector.startNetDetect(callback);

结束网络探测,传入探测任务ID

AVChatNetDetector.stopNetDetect(id);

网络探测结果通知

AVChatNetDetectCallback#onDetectResult( ... );

网络探测情况分级表

在结果信息中,lossRate、rttAverage、rttMeanDeviation这三个值最能反应当前客户端的实际网络情况。由这三个值可以计算出当前的网络状况指数:

网络状况指数 = (lossRate/20)*50% +(rttAverage/1200)*25% +(rttMeanDeviation/150)*25%

经过我们的反复测试,现提供三个网络状况指数节点

网络状况指数节点 lossRate(%) rttAverage(ms) rttMeanDeviation(ms) 网络状况指数
A 3 500 50 0.2625
B 10 800 80 0.55
C 20 1200 150 1

备注:

  1. 当网络状况指数≤0.2625时,网络状况非常好,音视频通话流畅;

  2. 当0.2625<网络状况指数≤0.55时,网络状况好,音视频通话偶有卡顿;

  3. 当0.55<网络状况指数≤1时,网络状况差,音频通话流畅;

  4. 当网络状况指数>1时,网络状况非常差,音频通话偶有卡顿。

音视频通话分场景模式说明

对于不同的音视频使用场景,对于流量音质人声音乐的要求都不同。我们归纳了四种模式:

音视频通话兼容性适配方案

由于Android设备众多,硬件配置、特性支持等也不尽相同,可能导致SDK提供的音视频通话功能在某些特定的机型或设备上无法正常运行。兼容性适配方案提取音视频通话过程中普遍存在的一些需要适配的项,在不发布新版本的前提下,允许开发者对这些适配项进行动态设置和修改,以确保通话能够正常使用和进行。

兼容性适配依赖一个Json格式的配置文件来完成适配工作。支持的适配类型及优先级如下:

类型(优先级依次降低)
本地文件适配
远端服务器适配

不同的适配类型对应不同的优先级,当多种不同适配类型包含相同一个适配项的配置时,会优先使用高优先级的适配配置。开发者可以自定义本地文件适配和远端服务器适配,使用其中一种或者两者同时使用。默认的本地配置文件路径为/sdcard/Android/data/{yourpackagename}/files/config/nrtc.cfg,在开始通话之前,可以通过设置AVChatParameters#KEY_COMPATIBILITY_CONFIG_LOCAL参数覆写该路径。与此同时,也可以通过AVChatParameters#KEY_COMPATIBILITY_CONFIG_SERVER设置服务器适配的配置文件URL地址,SDK会通过该地址自动下载远端的配置文件并解析。开发者需要保证所有Json配置文件的格式正确,否则会导致解析失败而无法应用适配。

注意:设置需要在通话建立之前,即在加入房间之前完成,通话过程中设置无效。服务器适配真正生效时间会延迟至成功下载解析后的下一次通话,首次只会进行下载和解析工作。

配置文件规则

无论是本地还是远端配置文件,统一使用Json格式。如下所示为一个简单的配置文件模版:

{
    "common":{
        "{compat-string-key}":"{compat-string-value}",
        "{compat-int-key}": 1,
        "{compat-long-key}": 10,
        "{compat-double-key}": 100.0,
        "{compat-bool-key}": true"{compat-immutable-array-key}": ["hello", "nrtc"],
        "{compat-mutable-array-key}": {
            "include":["red", "green", "blue"],
            "exclude":["yellow"]
        }
    },

    "{DeviceID}":{
        "{compat-string-key}":"{compat-string-value_2}",
        "{compat-int-key}": 2,
        "{compat-long-key}": 20,
        "{compat-double-key}": 200.0,
        "{compat-bool-key}": false"{compat-immutable-array-key}": ["netease", "nrtc"],
        "{compat-mutable-array-key}": {
            "include":["red", "green", "blue"],
            "exclude":["yellow"]
        }
    }

}

配置文件以块为单位,每一块又是一个JsonObject,以common或者DeviceID为Key值。其中common是所有设备通用的配置块,而{DeviceID}以Android设备的Build#MANUFACTURERBuild#BOARDBuild#MODEL作为参考,使用其中一个值或者几个值通过#号连接组合而成。具体的设备只会解析配置文件中匹配自己DeviceID的部分,其他部分不会解析。如果配置文件中包含多个匹配自己DeviceID的块,则会按照优先级进行解析,高匹配度部分的配置会覆盖低匹配部分的配置。匹配块可使用的KEY及其优先级如下表所示:

匹配块关键字(优先级低到高排序) 关键字说明
“common” 通用配置,会应用到所有设备
"{MANUFACTURER}" 匹配相同厂商的设备
"{MODEL}" 匹配相同型号的设备
"{BOARD}" 匹配相同主板的设备
"{MANUFACTURER}#{BOARD}" 匹配厂商和主板
"{BOARD}#{MODEL}" 匹配主板和型号
"{MANUFACTURER}#{BOARD}#{MODEL}" 精确匹配厂商、主板和型号

在配置块中,每一条配置以键值对的形式声明。根据具体不同的适配点,配置不同的合法的类型和值即可。如果配置不正确,则该条适配不生效。配置的值支持基本类型String、int、long、double以及boolean。此外,还支持两种复合类型,可变数组不可变数组类型。不可变数组使用JsonArray声明,在查找适配值时,我们会将本地和远程的适配值进行合并成一个数组使用。如本地声明配置值为:["red", "yellow"],远端同一配置声明为:["red", "blue"],则最后应用时的配置值为:["red", "yellow", "blue"]

即使本地配置优先级更高,但我们无法通过不可变数组完成通过本地适配值覆盖远端适配值的目的,但可变数组允许我们这样做。可变数组使用一个JsonObject声明,包含了includeexclude两个部分,分别对应两个JsonArray。在应用适配值时,我们也会对include部分进行合并,但是会从中剔除exclude声明的部分。如本地适配为:

    "key":{
        "include":["red", "yellow"],
        "exclude":["blue"]
    }

远端适配为:

    "key":{
        "include":["green", "blue"],
        "exclude":["yellow"]
    }

则最后的是配置为:["red","yellow","green"]。因为高优先级的本地配置声明了剔除blue,所以最后的结果中从远端配置中排除了blue;虽然远端也声明了剔除yellow,但它优先级比较低,无法更改优先级比它高的本地适配值。

适配规则总结:

可供适配的键、值表

适配键 适配说明 类型 取值
“video.hw_encoder.name” 视频硬件编码器名称 String -(合法的String类型值)
“video.hw_encoder.color” 视频硬件编码器color format int -(合法的int类型值)
“video.hw_decoder.name” 视频硬件解码器名称 String -
“video.hw_decoder.color” 视频硬件解码器color format int -
“video.hw_decoder.texture” 是否使用纹理加速解码 boolean -
“audio.low_latency.blacklist” 是否包含在语音低延迟黑名单中 boolean -
“audio.decoder.mime.blacklist” 语音解码mimetype黑名单 可变数组类型 -
“audio.hw_agc.blacklist” 是否包含在语音硬件自动增益黑名单中 boolean -
“audio.hw_aec.blacklist” 是否包含在语音硬件回音消除黑名单中 boolean -
“audio.hw_ns.blacklist” 是否包含在语音硬件噪声抑制黑名单中 boolean -
“audio.opensles.blacklist” 是否包含在语音OpenSL ES黑名单中 boolean -
“video.hw_encoder.h264.blacklist” 是否包含在视频硬件编码H264黑名单中 boolean -
“video.hw_decoder.h264.blacklist” 是否包含在视频硬件解码H264黑名单中 boolean -
“audio.recorder.source” 录音时指定语音输入源 int 参照MediaRecorder.AudioSource#VOICE_XX设置
“audio.mode” 通话过程中设置的语音模式 int 参照AudioManager#MODE_XX值进行设置

适配文件示例

{

  "common":{

    "audio.low_latency.blacklist": true,
    "audio.hw_agc.blacklist": false,
    "audio.hw_aec.blacklist": true,
    "audio.hw_ns.blacklist": true,
    "audio.opensles.blacklist": true,

    "video.hw_encoder.h264.blacklist": true,
    "video.hw_decoder.h264.blacklist": true

  },

  "Nexus 5": {

    "video.hw_encoder.name": "OMX.MS.AVC.Encoder",
    "video.hw_encoder.color": 19,
    "video.hw_decoder.name": "OMX.MS.AVC.Decoder",
    "video.hw_decoder.color": 19,
    "video.hw_decoder.texture": true,

    "audio.low_latency.blacklist": false,
    "audio.opensles.blacklist": false,

    "video.hw_encoder.h264.blacklist": false,
    "video.hw_decoder.h264.blacklist": false,

    "audio.recorder.source": 7,
    "audio.mode": 3,

    "audio.decoder.mime.blacklist": {
      "include": ["audio/mpeg", "audio/opus"],
      "exclude": ["audio/ogg", "audio/mp4"]
    }

  }
}

注意,以上配置仅供参考,不能应用于实际配置中