# 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首页有“授权”和“EVS连接”两个选项,必须在授权成功之后才能与EVS建立连接进行体验。
进入授权页面,这里的client id
对应你在设备接入控制台 (opens new window)创建的设备,将默认的client id
换成你所创建设备的 client id
,就能将你的自定义配置应用到demo。device_id
为设备唯一标识,demo中采用android_id作为默认值,当然也可以定义自己的生成方式,只需要保证唯一即可。输入client_id
和device_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
,进行替换,并完成如下操作:
- 在设备接入控制台-设备能力页,打开【有屏配网】能力。
- 在设备接入控制台-测试调试页,填写你绑定设备时使用的手机号。
← ShowCore iOS EVS SDK →