# Android EVS SDK

介绍

iFLYOS 提供了实现 EVS接入协议 的开源 Android SDK ,用于在设备端快速接入EVS。开发者既可以直接使用该 Android SDK,也可以在此基础上进行二次开发,满足定制化需求。为了保证EVS的良好体验,请务必按照 EVS接入协议进行二次开发。

更新日期 更新内容
2019.09.05 上线 1.3 版本

# 准备工作

# 开发环境要求

  • Git
  • Android Studio 3.1.2 或以上
  • 能够正常访问 jCenter、Google Maven 的网络连接
  • 一部 Android 4.4 或以上的开发设备(或以 Android 平板代替)

# 快速上手

# 下载源码:

git clone https://github.com/iFLYOS-OPEN/SDK-EVS-Android

使用 Android Studio 打开clone下来的 SDK-EVS-Android 工程。首次打开需要等待 Gradle 联网下载一些依赖库,可能需要较长时间,下载过程中需要保持网络畅通。

SDK-EVS-Android 项目中包含以下两个模块:

# evs_sdk

EVS接入协议的kotlin实现,主要包括 网络连接认证授权EVS协议解析模块接口定义和默认实现几部分:

名称 说明
网络连接 与iFLYOS建立WebSocket连接,支持ws和wss
认证授权 认证授权协议对设备进行授权
EVS协议解析 向iFLYOS发送EVS request,解析response
模块接口定义和默认实现 提供EVS协议描述的功能模块接口定义,并提供默认实现

SDK当前对EVS接入协议中各模块的实现情况如下:

名称 说明 要求 消息 是否实现
recognizer 识别器,识别语音和文本 必须实现 audio_in
text_in
expect_reply
stop_capture
intermediate_text





audio_player 音频播放器,播放的内容可能是音乐、新闻、闹钟响铃或TTS语音 必须实现 audio_out
playback.progress_sync
tts.progress_sync
ring.progress_sync
tts.text_in






system 系统相关 必须实现 ping
error
state_sync
exception
revoke_authorization





alarm 设备本地闹钟 可选实现 set_alarm
delete_alarm
system_sync



speaker 扬声器控制 可选实现 set_volume
video_player 视频播放器 可选实现 video_out
progress_sync


playback_controller 播放控制,在部分场景下,用户可通过触控或按键控制音频播放进度 可选实现 control_command
app_action APP操作,针对系统的第三方APP进行的操作 可选实现 execute
check
check_result
execute_succeed
execute_failed





screen 屏幕控制 可选实现 set_state
set_brightness
否,预留接口,由开发者实现
template 模板展示,用于通过界面模板给用户反馈更丰富的信息音 可选实现
interceptor 自定义拦截器,用于实现自定义语义理解 可选实现 custom
aiui
否,预留接口,由开发者实现

SDK最外层接口被封装成 EvsService,以Android Service组件方式对外提供。

# demo

一个最基础的 iFLYOS EVS设备端实现,简单的SDK调用示例,用于演示SDK接口调用,体验EVS技能,以及调试查看交互协议。

# 运行demo

demo首页

连上开发设备,运行 demo 模块。demo首页有“授权”和“EVS连接”两个选项,必须在授权成功之后才能与EVS建立连接进行体验。

授权页面 授权web

进入授权页面,这里的client id 对应你在设备接入控制台 (opens new window)创建的设备,将默认的client id 换成你所创建设备的 client id,就能将你的自定义配置应用到demo。device_id为设备唯一标识,demo中采用android_id作为默认值,当然也可以定义自己的生成方式,只需要保证唯一即可。输入client_iddevice_id(点自定义checkbox后可自行指定,否则使用默认值),点击“请求”即可会弹出下方的请求url和二维码。

授权成功

可以选择用App(小飞在线、微信等)扫码或者点击“从浏览器打开”两种方式进入授权认证的登录页面。使用在所创建设备的设备测试页面添加的手机号登录后授权,授权成功后返回demo,提示“授权成功”,如上图所示。

连接成功

接下来进入“EVS连接”页面,建立连接后即可以通过语音或者输入文本进行交互,交互过程中上传和下发的协议消息都以列表显示出来,点击可以查看详细内容。你可以对demo说“今天的天气”,“我想听新闻”,“明天早上七点叫我起床”等等,来开启EVS体验之旅。

# SDK集成

# 引入SDK工程

将源码工程中的evs_sdk模块作为module引入到目标工程,并在工程的AndroidManifest.xml文件中添加所需权限。在Android 5.0及以上版本添加:

<!--录音权限-->
<uses-permission android:name="android.permission.RECORD_AUDIO" />
<!--网络访问权限-->
<uses-permission android:name="android.permission.INTERNET" />
<!--应用使用统计权限,使用app_action时需要-->
<uses-permission android:name="android.permission.PACKAGE_USAGE_STATS" tools:ignore="ProtectedPermissions" />

Android 5.0以下版本添加:

<!--录音权限-->
<uses-permission android:name="android.permission.RECORD_AUDIO" />
<!--网络访问权限-->
<uses-permission android:name="android.permission.INTERNET" />
<!--获取任务权限,使用app_action时需要-->
<uses-permission android:name="android.permission.GET_TASKS" />

# 认证授权

App可以调用AuthDelegate.getAuthResponseFromPref方法,根据返回的结果判断设备端是否已经被授权。

val authResponse = AuthDelegate.getAuthResponseFromPref(this)
if (authResponse != null) {
    // 已授权
} else {
    // 未授权
}

当检测到未授权(或者授权信息失效)时,调用AuthDelegate的requestDeviceCode方法开启授权过程,示例如下:

AuthDelegate.requestDeviceCode(
    this,       // Context对象
    clientId,   // 在设备接入控制台(https://device.iflyos.cn/products)添加设备时生成的标识
    deviceId,   // 设备标识,可唯一标识某个设备端
    object : AuthDelegate.ResponseCallback<DeviceCodeResponse> {    // 请求deviceCode回调
        override fun onResponse(response: DeviceCodeResponse) {
            // 请求成功回调,从DeviceCodeResponse中可得到授权url
            val authUrl = "${response.verificationUri}?user_code=${response.userCode}"
            
            // 接下来可以生成authUrl的二维码展示出来,以便使用“小飞在线”或浏览器扫码授权
        }
        override fun onError(httpCode: Int?, errorBody: String?, throwable: Throwable?) {
            // 出错回调
        }
    },
    object : AuthDelegate.AuthResponseCallback {
        override fun onAuthSuccess(authResponse: AuthResponse) {
            // 授权成功回调,从AuthResponse中可得到访问EVS的accessToken
        }
        override fun onAuthFailed(errorBody: String?, throwable: Throwable?) {
            // 失败回调
        }
    },
    scope   // 请求的能力范围
    )
}

参数scope定义了设备端请求授权的能力范围,默认值为“user_ivs_all”(不传该参数即使用默认值),当需要开通特殊能力(如TTS)时,这里要传入相应的scope。

onResponse回调后,SDK内部会新建线程等待用户授权(即轮询服务端该设备的授权状态),授权成功回调onAuthSuccess,否则回调onAuthFailed,回调之后线程会正常结束。若设备端要在授权结果返回之前退出授权,则需要调用以下接口来结束轮询:

AuthDelegate.cancelPolling()

否则轮询线程将一直存在,消耗系统资源。

# 自定义Service

EVS SDK只是将对外接口封装成抽像类EvsService。开发者需要在App中先从EvsService派生出具体类,并将类的声明加入AndroidManifest.xml,这样才能通过自定义Service来使用EVS。demo中自定义Service大致如下:

class EngineService : EvsService() {
    private val binder = EngineServiceBinder()

    // binder派生类,外部通过bindService系统调用来获取EngineService对象,以调用EVS功能
    open inner class EngineServiceBinder : Binder() {
        fun getService(): EngineService {
            return this@EngineService
        }
    }
    
    override fun onBind(intent: Intent?): IBinder? {
        // 一绑定就返回binder对象给外部
        return binder
    }

    // 封装端能力原始接口,简化外部调用
    fun sendAudioIn(replyKey: String? = null) {
        getRecognizer().sendAudioIn(replyKey)
    }

    fun sendTextIn(query: String, replyKey: String? = null) {
        getRecognizer().sendTextIn(query, replyKey)
    }

    fun sendTts(text: String) {
        getAudioPlayer().sendTtsText(text)
    }
    
    ...

    // 若要监控EVS连接状态,则复写onEvsConnected和onEvsDisconnected
    override fun onEvsConnected() {
        super.onEvsConnected()

        sendBroadcast(Intent(ACTION_EVS_CONNECTED))
    }

    override fun onEvsDisconnected(code: Int, message: String?) {
        super.onEvsDisconnected(code, message)

        val intent = Intent(ACTION_EVS_DISCONNECTED)
        intent.putExtra(EXTRA_CODE, code)
        intent.putExtra(EXTRA_MESSAGE, message)
        sendBroadcast(intent)
    }
}

EvsService类定义了一系列回调方法(如onEvsConnected,onEvsDisconnected等),可在派生的Service类中根据需要覆盖它们,并选择合适的方式通知到Service外部。代码中是通过广播将EVS的连接状态通知外部,当然也可以通过其他方式(如回调)实现。也可对各模块的原始接口进一步封装,简化外部调用。完整的代码请参见EngineService (opens new window)

最后别忘记声明Service组件:

<application>
    <service android:name=".EngineService" />
    ...
</application>

# 端能力配置

EvsService类中定义了一些以“overrideXXX”命名(XXX为模块名称,如Recognizer)的模板方法用于创建各个模块实例。可以在派生类中覆盖这些方法,返回自己开发的模块以替换SDK中的默认实现,若不覆盖则使用默认实现。对于可选模块也可以直接返回null,表示不启用该模块(即设备端不支持该项端能力):

/**
 * 创建识别模块。
 */
open fun overrideRecognizer(): Recognizer {
	// 返回识别模块实现对象
	return RecognizerImpl()
}

/**
 * 创建视频播放器。
 */
open fun overrideVideoPlayer(): VideoPlayer? {
    // 返回null表示不启动视频播放器
    return null
}

EVS SDK会根据各模块的创建情况来上报设备端能力(参见evs_sdk中RequestBuilder类的buildContext方法 (opens new window)),若某模块不为null,在发送reuqest时就会在“iflyos_context”字段中加上该模块的信息,若为null则会忽略该端能力。

EVS当前各端能力模块的创建要求如下:

模块 是否必需
recognizer
audio_player
system
alarm
speaker
video_player
playback_controller
app_action
screen
template
interceptor

对于必需模块,必须在相应的创建方法中返回模块实例,可选模块则可根据具体需求返回null或者实例。

# 初始化EVS

初始化EVS即启动自定义Service组件并获取到用来调用EVS接口的组件对象,一般在Android组件初始化的地方(如Application,Activity,Service的onCreate方法)调用,示例如下:

private var engineService: EngineService? = null
private val serviceConnection = object : ServiceConnection {
    override fun onServiceConnected(name: ComponentName?, service: IBinder?) {
        if (service is EngineService.EngineServiceBinder) {
            // 通过binder对象获取Service对象,以直接调用其中的方法
            engineService = service.getService()
        }
    }

    override fun onServiceDisconnected(name: ComponentName?) {
        engineService = null
    }
}

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    
    ...
    val intent = Intent(this, EngineService::class.java)
    startService(intent)
    bindService(intent, serviceConnection, Context.BIND_AUTO_CREATE)
}

这里需要先调用startService来启动服务,再调用bindService绑定该服务并获取Service对象。这样,即使当前使用EVS的组件销毁(销毁之前一般会调用unbindService来解绑Service),EVS Service也会一直在后台保持运行,不会被销毁。

# 连接操作

建立连接:

// deviceId必须与授权时上传的保持一致
engineService?.connect(deviceId)

断开连接:

engineService?.disconnect()

连接成功后会回调EvsService的onEvsConnected方法,与服务端断开连接则会回调onEvsDisconnected方法,可在这两个方法中添加相应的处理逻辑。若要在Service外部监听连接状态,则需要根据自定义Service中的抛出方法添加相应实现。

demo中采用广播方式,所以在外部注册相应的广播即可:

private val broadcastReceiver = object : BroadcastReceiver() {
    override fun onReceive(context: Context?, intent: Intent?) {
        when (intent?.action) {
            EngineService.ACTION_EVS_DISCONNECTED -> {
                // 连接已断开
            }
            EngineService.ACTION_EVS_CONNECTED -> {
                // 连接已建立
            }
        }
    }
}

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    ...
    val intentFilter = IntentFilter()
    intentFilter.addAction(EngineService.ACTION_EVS_CONNECTED)
    intentFilter.addAction(EngineService.ACTION_EVS_DISCONNECTED)
    ...
}

# 交互操作

与EVS交互是通过获取某个功能模块,调用具体接口实现的。以下是几种常见的交互操作示例:

开启录音识别:

engineService?.getRecognizer().sendAudioIn(replyKey)

停止录音识别:

engineService?.getRecognizer().requestEnd()

识别文本:

engineService?.getRecognizer().sendTextIn(query, replyKey)

合成文本:

engineService?.getAudioPlayer().sendTtsText(text)

播放控制,可以通过PlaybackController向云端发送请求,云端回复控制消息来操作播放器完成,也可以在本地直接调用播放器接口。

通过PlaybackController:

// 下一个
engineService?.getPlaybackController()?.sendCommand(PlaybackController.Command.Next)

// 暂停
engineService?.getPlaybackController()?.sendCommand(PlaybackController.Command.Pause)

// 上一个
engineService?.getPlaybackController()?.sendCommand(PlaybackController.Command.Previous)

// 恢复
engineService?.getPlaybackController()?.sendCommand(PlaybackController.Command.Resume)

本地操作播放器:

// 暂停某类音频播放,pause函数传入参数值为:AudioPlayer.TYPE_PLAYBACK(歌曲、新闻等内容)、AudioPlayer.TYPE_RING(闹钟)、AudioPlayer.TYPE_TTS(合成播报)
engineService?.getAudioPlayer().pause(AudioPlayer.TYPE_PLAYBACK)

// 恢复播放,同样需要传入音频类型
engineService?.getAudioPlayer().resume(AudioPlayer.TYPE_PLAYBACK)

详细的播放器接口请参考evs_sdk中AudioPlayer和VideoPlayer类,可以在默认实现的基础上加以修改来满足需求。

音量控制:

engineService?.getSpeaker()?.setVolume(50)

# 销毁EVS

当确定不再需要使用EVS Service时,即可进行销毁以释放资源:

val intent = Intent(this, EngineService::class.java)
unbindService(serviceConnection)
stopService(intent)

# 高级功能

# Interceptor

当通用技能无法满足需求时,你可以通过拦截器进行定制开发。在设备接入控制台 (opens new window)配置好拦截器后,EVS设备端可能收到name为interceptor.custom和interceptor.aiui这两种类型的自定义response,分别为自定义和AIUI返回的语义结果。设备端需要定义自己的Interceptor实现类来接收和处理它们,示例如下:

// 定义Interceptor实现类
class InterceptorImpl(val context: Context) : Interceptor() {
    override fun onResponse(name: String, payload: JSONObject) {
        when (name) {
            Interceptor.NAME_CUSTOM -> {
                // 自定义结果处理
            }
            Interceptor.NAME_AIUI -> {
                // AIUI语义结果处理
            }
        }
    }
}

// 覆盖Service中的方法,返回自定义Interceptor实现
override fun overrideInterceptor(): Interceptor? {
    return InterceptorImpl(this)
}

AIUI语义结果格式参考AIUI设备能力

# 音视频焦点管理

如果你有自己需要实现的音视频播放器,并且不希望与 SDK 中的语音识别、音视频播放产生冲突,则应当采用以下描述方式来加入 SDK 的焦点。

SDK 对音频通道提供了 AudioFocusChannel ,对视觉通道提供了 VisualFocusChannel,在 ExternalFocusChannel (opens new window) 中已定义。两个通道的使用、原理相近,下面以注册音频通道为例。

首先创建一个你自己的音频通道实例。

val audioChannel = object : AudioFocusChannel() {
    override fun onFocusChanged(focusStatus: FocusStatus) {
        // 根据焦点管理通知的状态处理做对应的处理
        ...
    }

    // 返回用于判断优先级的通道
    override fun getChannelName(): String {
        ...
    }

    // 返回音频通道的类型
    override fun getType(): String {
        return "TTS"
    }
}

回调声明:

  • onFocusChanged 代表 SDK 音频焦点管理认为你的音频通道应当进入某种状态,FocusStatus 包含以下几种状态,可参阅EVS设备体验参考规范知悉进入各个状态时播放器推荐的处理是什么

    变量 对应含义
    FocusStatus.Idle 静默状态,收到回调时应将播放器停止
    FocusStatus.Foreground 前景活跃状态,正常播放时的状态
    FocusStatus.Background 背景状态,收到回调时应将播放器进入不影响前景活跃通道交互的状态,例如降低音量或者暂停
  • getChannelName 应返回 AudioFocusManager 定义好的通道名(例如 AudioFocusManager.CHANNEL_OUTPUT),否则该对象会被视为无效。通道定义如下,可参阅EVS设备体验参考规范知悉各个通道之间的优先关系

    变量 对应通道
    CHANNEL_OUTPUT 语音输出通道
    CHANNEL_DIAL 通话通道
    CHANNEL_INPUT 语音输入通道
    CHANNEL_ALARM 闹钟通道
    CHANNEL_CONTENT 内容通道
  • getChannelType 返回可与你注册的其他通道区分开的类型名即可。

而后在你自定义的服务初始化时,像下面这样调用,通道的注册就结束了。

class YourService() : EvsService() {
    override fun onCreate() {
        super.onCreate()

        ...
        audioChannel.setupManager(AudioManager)
        ...
    }
}

注册完成后,当你的音频播放器开始播放时,将音频通道声明为活跃。

audioChannel.requestActive()

当你的音频播放器停止后,丢弃音频焦点。

audioChannel.requestAbandon()

TIP

外部视觉焦点管理与上述样例相似,此处不再赘述

# 附录

# 云端错误码

当设备端request出错时,云端通过name为system.error的response返回错误码,错误码说明参见请求出错

# 网络错误码

WebSocket连接可能会抛出以下错误码:

错误码 说明 建议操作
1000 WebSocket正常断开 一般是设备端主动断开,不需要处理
1005 服务端主动断开 查看设备是否被解绑
-1 网络连接出错 查看设备是否有网络连接

# 接入常见问题

问:打开 Android Studio 项目下载内容时出现网络失败?

答:需要能够正常访问 jCenter、Google Maven 的网络连接。

问:网络确实连上了,但是绑定帐号提示「网络异常,请重试。」

答:请检查设备的时间是否正确。不正确的时间会导致与服务端的 SSL 通讯失败。

问:二维码显示正常,但是绑定设备提示「无效应用。」

答:demo 中内置的 client_id是仅作为测试用的,如果此 Id 失效,请参考控制台管理设备介绍获取你创建的自定义设备的 client_id,进行替换,并完成如下操作:

  1. 在设备接入控制台-设备能力页,打开【有屏配网】能力。
  2. 在设备接入控制台-测试调试页,填写你绑定设备时使用的手机号。