若接收烟台小樱桃网络科技有限公司(以下称为“小樱桃”)的此份文档,即表示您已同意以下条款。若不同意以下条款,请立即停止使用本文档。
本文档版权所有烟台小樱桃网络科技有限公司。保留任何未在本文档中明示授予的权利。文档中涉及小樱桃的专有信息。未经小樱桃事先书面许可,任何单位和个人不得复制、传递、分发、使用和泄漏该文档以及该文档包含的任何图片、表格、数据及其他信息。本网页版文档仅在xswitch.cn上发布。
本产品符合有关环境保护和人身安全方面的设计要求,产品的存放、使用和弃置应遵照产品手册、相关合同或相关国法律、法规的要求进行。
本文档按“现状”和“仅此状态”提供。本文档中的信息随着小樱桃的产品和技术的进步将不断更新,小樱桃不再通知此类信息的更新。
烟台小樱桃网络科技有限公司
地址:烟台市高新区蓝海路1号
邮编:264000
电话:0535-6753997
XSwitch是一个电信级的IP电话软交换系统和综合实时音视频多媒体通信平台
本文档描述XSwitch XCC API的设计原则和使用方法。XCC是XSwitch Call Control(XSwitch呼叫控制)的缩写。
XSwitch支持电话、传真、视频会议、呼叫中心等。支持主流的通信协议如SIP、H323、WebRTC、RTMP等,支持单机部署、云原生集群部署,支持无限扩容及动态伸缩。目标是为用户提供一站式语音、视频、会议解决方案,可以作为IP-PBX、视频会议服务器、传真服务器、多协议网关、呼叫中心服务器等使用。XSwitch提供REST、Websocket二次开发接口。XSwitch基于开源技术构建,如FreeSWITCH、PostgreSQL、Nginx等。
XSwitch是模块化,积木式按需叠加和无限伸缩扩容的通信产品,更可以通过定制支持集群部署,实现更强大的功能。
XSwitch的位置、组件和逻辑关系如下图所示:
其中,SIP话机通过SIP协议接入XSwitch,管理员可以通过Web浏览器进行系统管理和维护,也可以通过WebRTC打电话,静态Web页面都由Web服务器提供,动态API请求会由Web服务器转发到XSwitch端进行处理。
XCC相关的呼叫控制架构如下图所示:
其中,XSwitch是核心交换节点,负责SIP信令和媒体交换。当SIP客户端(或通过运营商IMS线路)有呼入时,XSwitch会通过消息队列通知控制器,控制器进而可以接管整个通话流程。控制器只是发送控制指令和消息,实际的音视频媒体都在XSwitch内处理。
有时候,为了定义更明确,XSwitch又称为XNode,即交换节点。消息队列使用NATS实现,控制器简称XCtrl,因而也可以以下图方式表示,它其实与上图是等价的。
XSwitch底层基于开源的FreeSWITCH开发,FreeSWITCH作为XSwitch的核心交换节点,即XNode。FreeSWITCH性能强悍,但应对大规模应用时还是需要组建集群。因此我们设计了一个集群架构。
集群可以运行在裸金属服务器或虚拟机上,但更理想的环境是运行在K8S上,便于更方便地进行动态伸缩。FreeSWITCH节点可以随时加入和退出服务,即所谓的动态伸缩。
系统中,所有组件都可以多个实例工作,多个组件可以分布在多个物理节点上,一个物理节点发生故障不影响整个集群运行。
通过消息队将通话和控制解耦。
FreeSWITCH博大精深,用好FreeSWITCH需要对它充分了解,不仅仅是底层的SIP通信机制会让一些非通信专业的程序员头痛,FreeSWITCH的通话控制逻辑也让很多人感觉晕头转向。考虑以下问题:
FreeSWITCH是一个B2BUA,当A发起一个呼叫时,FreeSWITCH会创建一个Channel,然后在内部会执行一个Application跟对端进行交互,比如playback
就是直接播放一个声音,这个比较容易理解。当A想跟B通话时,FreeSWITCH又会发起一个呼叫,然后将两路呼叫桥接起来(称为bridge
)。更多方的呼叫可以用会议(conference
)方式实现。
那么,FreeSWITCH怎么知道在什么情况下执行哪些Application呢?很简单,你要告诉它。这就需要你预先制定一定的规则(我们称为dialplan
,即拨号计划),FreeSWITCH默认使用XML的拨号计划。
规则是死的,如果你希望更灵活的规则,你就可以用Lua脚本,或者ESL接口在外部进行控制。
问题是,通话逻辑是很复杂的,在通话中你不仅希望灵活地放音,还希望有转接、录音、收DTMF、ASR语音识别、甚至发送特定的SIP消息等,这些都需要很灵活的控制,但不幸的是,这很复杂。
我们再来思考一个更一般的A与B互通的逻辑:A呼B,如果有一方挂机,对方就会自动挂掉,这在电话交换专业中叫做“互不控制”。但是,实际的场景却又有不同的需求,比如在呼叫中心场景中,如果座席挂机,那么要引导客户对座席的服务进行评价,就需要客户侧不能自动挂机……
是的,FreeSWITCH提供了很多实现方法,比如通过Application与拨号计划的有机组合,但什么时候需要转拨号计划?什么时候Application就可以?有什么副作用?要达到完全理解和领会并用得出神入化,需要很长时间。
本文档描述的接口就是致力于解决这些问题——让控制FreeSWITCH程序员不用纠结于FreeSWITCH的内部逻辑,从更高的视角分析业务需求,进而调用合适的API进行控制。
我们深知,看到本文档的程序员都是深资的程序员,大家熟悉各种数据结构和交互逻辑,只是不熟悉FreeSWITCH的交互和各种SIP控制。把专业的事情交给专业的人做,我们做好底层通信能力,您就可以省出时间更多的关注自己的业务逻辑。
通过消息队列将控制和媒体分离,最大程度地减少耦合,是我们设计的初衷。
值得一得的是,单独的这些XCC接口并不能完全让你无视FreeSWITCH,但至少可以让你大部分无视。我们在XCC接口的基础上又做了一个更高级的呼叫中心服务和业务逻辑,就可以真正做到“与FreeSWITCH无关”了。
系统整体控制架构和弹性伸缩基于NATS消息队列实现。
NATS1是一个消息队列,它实现了Pub/Sub(生产者-消费者)消息机制,通过它也可以实现“请求-响应”式的RPC(Remote Procedure Call,远程过程调用)交互。
NATS使用主题(Subject)对消息进行标记,实际消息内容可以是任何文本或二进制消息。XSwitch使用JSON-RPC消息封装。
在使用NATS前,需要了解NATS的核心概念,参考文档:https://docs.nats.io/nats-concepts/core-nats。文档都是英文的,下面是简要的中文解释。
在实际使用时,XNode和Ctrl分别向NATS服务器订阅自己关心的主题,然后就可以互相发送消息了。消息本身是异步的,也就是说,生产者产生一条消息,发送出去就完了,而消费者可以有0个或多个,订阅同一个主题的消费者都能收到这条消息,如果没有消费者,消息就会被丢弃。
参考文档:https://docs.nats.io/nats-concepts/core-nats/pubsub
通过订阅一个一次性的主题,可以实现阻塞的“请求-响应”调用。这个一次性的主题称为“邮箱”(Mailbox),如下图:
其中,XNode发出一条消息前订阅了一个邮箱,Ctrl收到这条消息后往这个邮箱里回复了一条响应消息。
当然,这个一次性的主题也可以扩展为N(N>1
)的场景,在此不做讨论。
参考文档:https://docs.nats.io/nats-concepts/core-nats/reqreply
NATS通过队列组可以执行类似负载均衡的分发策略。对于多个消费者来说,订阅了相同Queue的一组消费者中,只有一个可以收到消息,也就是说消息分发是互斥的。如下图,控制器1、2、3以同一个队列(queue-1
)方式订阅了同一个Subject,只有一个能收到消息,而控制器4没有以队列方式订阅,控制器5使用了另一个队列(queue-2
),它们也能收到消息。
通过这种方式,可以实现Round Robin(轮循)方式的消息分发。比如同时来了300路通话,每个Ctrl可以处理100路。
参考文档:https://docs.nats.io/nats-concepts/core-nats/queue
NATS支持集群,多个NATS间也可以自由转发消息。如果连接到其中一个NATS服务器失败,NATS客户端库会自动尝试连接下一个NATS服务器。
基于NATS实现的XSwitch集群将在后面的章节讨论。
NATS有各种语言的客户端SDK,因此使用起来很方便,具体的例子见后文。
远程过程调用使用JSON-RPC 2.0格式定义。下面是一个JSON-RPC请求:
{
"jsonrpc": "2.0",
"id": "1",
"method": "XNode.Answer",
"params": {
"ctrl_uuid": "ctrl_uuid ... ",
"uuid": "..."
}
}
JSON-RPC中,所有请求都有一个id
,代表期望返回一个结果。且返回结果中的id
与请求中的id
一致。JSON-RPC规定id
可以是null
、数字或字符串,但为了简单起见,我们只使用字符串。
id
可以为不存在(注意不是null
,null
是一个值),如果id
不存在,则认为是一个事件(或称通知,Notification),事件其实也是一个请求,但是不需要对方返回结果。
如果返回结果中有result
,则为正常返回,否则,应该返回error
。两者都可以是任何合法的JSON对像或值。
{
"jsonrpc": "2.0",
"id": "...",
"result": {
...
}
}
{
"jsonrpc": "2.0",
"id": "...",
"error": {
...
}
}
详细的JSONRPC规则可参考:
通过NATS使用JSON-RPC进行远程过程调用的API称为XCC API。
JSON-RPC相当于一个信封。在信封内部,是XCC API具体请求的内容,统一放到params
中。信封中的出错信息通常是因为信封出错导致的,如必选参数不存在、方法不存在等。如果信封合法,则具体的返回结果都放到result
中,返回结果中均有如下参数:
code
:代码,参照HTTP代码规范,如2xx
代表成功,4xx
代表客户端错误,5xx
代表服务端错误等。message
:对代码的解释。有一些message
中会带有一些00
开头的错误码,这些错误码没有任何含义,仅为方便追踪错误而设。uuid
:如果请求中有uuid
,则结果中也有uuid
,代表当前的Channel UUID。通用code
返回值说明
100
:临时响应,实际的响应消息将在后续以异步的方式返回。200
:成功。202
:请求已成功收到,但尚不知道结果,后续的结果将以事件(NOTIFY
)的形式发出。206
:成功,但是数据不全,如发生在放音过程中通过API暂停的情况。400
:客户端错误,多发生在参数不全或不合法的情况。410
:Gone。发生在放音或ASR检测过程中用户侧挂机的情况。419
:冲突。如一个电话被接管,另一个控制器又尝试接管的情况。500
:内部错误。6xx
:系统错误,如发生在关机或即将关机的情况下,拒绝呼叫。XCC API即控制指令。控制指令分类两类:
uuid
参数,uuid
即为当前Channel的uuid
。uuid
参数。如对FreeSWITCH的控制,发起外呼等。API在XSwitch后台使用多线程调度,所有API在前面看起来都是阻塞的,但由于消息队列的异步特性,可以被同步或异步地调用。
在XSwitch中,Session与Channel通用,代表一路呼叫。XSwitch是个B2BUA,如果是单路的IVR,则是一个Channel,在a呼b的场景中,是两个Channel。每个Channel在XSwitch中也称为一条“腿”(Leg),呼入的腿一般称为a-leg
,呼出的腿称为b-leg
。
所有的XCC API都是针对Channel进行操作(通过uuid
参数),如Play、TTS等。有的API(如ChannelBridge
)可以同时操作两条腿,甚至更多的腿。
XSwitch侧所有来话均会被置于park
(暂停、挂起)状态,然后XSwitch广播一个Event.Channel
(state=START
)事件到消息队列。关注该消息队列的控制服务可以接管这个呼叫。
控制服务称为XCtrl。多个XCtrl可以协同工作。第一个发送接管指令(Accept
)并接管成功的XCtrl将接管该呼叫。后续XSwitch内所有跟当前通话相关的事件都会发到这个节点上。
如果一个来话在10
秒内没有被接管,则呼叫将会被挂断。
如果想对来话直接应答,则可以直接使用Answer
应答,Answer
会隐含进行Accept
。流程图如下:
去话,即外呼,使用Dial
方法实现,由于外呼可能持续比较长的时间,外呼在XSwitch侧是在单独的线程中处理的,所以外呼请求会收到code = 202
消息表示该外呼请求已被接受正在排队。如果外呼不成功,或者有成功进展,都会有后续的Event.Channel
或Event.Result
消息。
简单外呼的流程图如下:
Channel状态。
START
:来话第一个事件,XCtrl应该从该事件开始处理,第一个指令必须是Accept或AnswerRINGING
:振铃ANSWERED
:应答BRIDGE
:桥接UNBRIDGE
: 断开桥接DESTROY
:挂机CALLING
:去话第一个事件RINGING
:振铃ANSWERED
:应答MEDIA
:媒体建立BRIDGE
:桥接READY
:就绪UNBRIDGE
: 断开桥接DESTROY
: 挂机在调用XNode.Dial
外呼的时候,在ignore_early_media=false
(默认)的情况下,收到MEDIA
就会触发READY事件。如果为true
,则需要等到ANSWERED
以后才会触发READY
状态。不管什么情况,都需要在收到READY
状态后才可以对Channel进行控制。
在执行XNode.Bridge
时,没有READY
事件,这时可以根据ANSWERED
或BRIDGE
事件处理业务逻辑。
在XNode中,一个Channel从创建开始(state = START
或state = CALLING
),到销毁(state = DESTROY
),是一个完整的生命周期。销毁前,会发送Event.CDR
事件,通常会在单独的Topic上发出(可配置)。
一般来说,只要Channel被创建,总会有对应的DESTROY
消息。但是,在XNode发生崩溃的情况下,需要准备超时垃圾回收机制。
使用NATS客户端库的request
函数发起同步调用。同步调用在Ctrl侧会阻塞直到返回结果。同步调用有一个超时参数timeout
,如果发生超时,不一定代表出错。考虑以下场景,播放一个声音文件(file
):
request(play, file, timeout = 10)
如果文件长度为5秒,则该函数会在5秒播放完成后返回。
如果文件长度为15
秒,则该函数会在10
秒后超时。但文件的播放仍在进行。客户端可以选择继续等待播放完成的事件(Event.PLAYBACK_STOP
),或停止当前播放(调用Stop
方法)。
注意:在实际开发时,要尽量避免出现NATS timeout
,也就是说要让timeout
足够长,因为在出现timeout
的情况下(如上述场景),实际上文件可能没有播放完成,也可能是网络断了或XSwitch崩溃了等各种原因,因而,Channel的状态在Ctrl侧是是未知的,这就需要很小心的错误处理以应对各种情况(如,可以主动查询一下Channel状态,但查询可能又会发生超时……)。
expect_100_trying
为了方便在同步调用时快速知道执行结果,在同步调用中有一个实验性的参数params.exepect_100_trying
。使用该参数时必须提供params.ctrl_uuid
。如果expect_100_trying = true
,则调用会立即返回(code = 100
或错误消息),如果返回的是错误消息,则错误消息为最终响应,不会有后续结果。如果返回的是100
,则实际的调用结果将发送到异步订阅的Controller主题上,该主题规则为ctrl_uuid
的拼接,如cn.xswitch.ctrl.$ctrl_uuid
。如果ctrl_uuid
不存在,则会发送到Accept
或Answer
接管的Controller指定的位置。
目前支持该操作的接口有:
action = RECORD
)时可用)使用NATS的publish
函数可以发起异步调用。异步调用时,如果rpc.id
为空,表示Ctrl不需要看到结果,XSwitch不会发送API的执行结果消息,但是有可能产生相关的事件,如(PLAYBACK_STOP
)。
如果异步调用时rpc.id
为非空,则FreeSWITCH会返回Event.Result
消息,Ctrl应该订阅该消息(Subject为cn.xswitch.ctrl.<ctrl_uuid>
),以获取执行结果。结果中的rpc.id
与请求时中的rpc.id
一致。
每一个Channel上都有很多参数(param
),也叫变量(variable
),如caller_id_name
,caller_id_number
等。这些变量通常在Event.Channel
事件中发送。对于每一路通话,XSwitch Channel上会有数百个相关的变量,然而,大多数时候我们并不需要所有变量,因而,为了简洁起见,XSwitch并不发送所有变量,而是只发送很少的变量。
如果在开发应用中有特殊需求,可以在Controller侧用以下方式“订阅”更多变量:
xcc.conf
中静态配置,这个配置是全局的,即所有Channel事件都会带这些变量Accept
或Answer
时,通过channel_params
字符串数组动态订阅变量Dial
或Bridge
的Destination
参数中通过channel_params
数组动态订阅变量SetVar
中的channel_params
数组动态订阅变量。订阅的变量会在Channel生命期内一直有效,xcc.conf
中静态配置的变量会与动态订阅的变量合并(去重)发送。Event.Channel
事件中,会在params.params
参数中携带,参数和值全部都是字符串。
订阅的变量也会体现在Event.CDR
中,但会与Event.CDR
中的变量混在一起,没有二级params.params
。
Info/Event显示名称 | 通道变量名称 | 说明 |
---|---|---|
Channel-State | state | 当前Channel的状态 |
Channel-State-Number | state_number | Channel状态整数值 |
Channel-Name | channel_name | Channel名称 |
Unique-ID | uuid | Channel UUID,唯一标志 |
Call-Direction | direction | 呼叫方向,inbound/outbound |
Answer-State | state | 应答状态 |
Channel-Read-Codec-Name | read_codec | 接收语音编码 |
Channel-Read-Codec-Rate | read_rate | 接收采样率 |
Channel-Write-Codec-Name | write_codec | 发送语音编码 |
Channel-Write-Codec-Rate | write_rate | 发送采样率 |
Caller-Username | username | 鉴权用户名 |
Caller-Dialplan | dialplan | Dialplan名称,如XML、Lua等 |
Caller-Caller-ID-Name | caller_id_name | 主叫名称 |
Caller-Caller-ID-Number | caller_id_number | 主叫号码 |
Caller-ANI | ani | ANI,一般与主叫号码相同 |
Caller-ANI-II | aniii | ANI II 2 |
Caller-Network-Addr | network_addr | 主叫IP地址 |
Caller-Destination-Number | destination_number | 被叫号码 |
Caller-Unique-ID | uuid | Channel UUID |
Caller-Source | source | 源模块,如mod_sofia |
Caller-Context | context | Dialplan Context |
Caller-RDNIS | rdnis | 转移后的DNIS信息,见transfer Application |
Caller-Channel-Name | channel_name | |
Caller-Profile-Index | profile_index | . |
Caller-Channel-Created-Time | created_time | 创建时间 |
Caller-Channel-Answered-Time | answered_time | 应答时间 |
Caller-Channel-Hangup-Time | hangup_time | 挂机时间 |
Caller-Channel-Transfer-Time | transfer_time | 转移时间 |
详细的通道变量参见以下链接:
https://freeswitch.org/confluence/display/FREESWITCH/Channel+Variables
接下来,我们看几个实际的例子,以便能快速理解和使用XCC API控制呼叫。
本章的示例大部分以Node.js
为例,因为它基于Javascript语言,易懂,也容易讲解。
在XSwitch中创建如下路由:
1234
xcc
当呼叫1234
时,路由到xcc
,它会固定往cn.xswitch.ctrl
上发Event.Channel
消息。如果想让消息发到特定的Subject上,目的地中可以填xcc cn.xswitch.ctrl.你自己的Ctroller的UUID
。
呼叫流程看代码中的注释(简单起见没有加太多错误处理代码)。本示例使用了NATS Node.js V2 SDK,使用了xcc.js
简单封装。具体代码可以参见本文档尾部相关链接。
'use strict';
const XCC = require('./xcc')
let nats_url = process.env.NATS_URL;
let service = process.env.XSWITCH_SUBJECT || 'cn.xswitch.node.test';
let xctrl_subject = process.env.XCTRL_SUBJECT || 'cn.xswitch.ctrl';
const options = {
nats_url: nats_url,
log_level: 7,
service: service,
}
const xcc = new XCC(options); // 新建一个对象
.on('connected', () => {
xccconsole.log("connected"); // 连接后打印日志
;
})
.on('START', (call) => {
xccivr(call); // 来话处理
;
})
async () => {
(await xcc.init(); // 连接到NATS
.listen(xctrl_subject, 'play'); // 等待呼入
xcc;
})()
async function ivr(call) { //呼叫处理
console.log(`>> call from ${call.cid_number} to ${call.dest_number} uuid=${call.uuid} Total Calls: ${xcc.ncalls()}`);
.on('DESTROY', (params) => {
call// xcc.log('>> DESTROY', params);
.log('>> DESTROY', params.uuid, `Total Calls: ${xcc.ncalls()}`);
xcc;
})await call.answer(); // 应答
// await call.play('/tmp/test.wav'); // 本地wav文件
await call.play('https://xswitch.cn/download/wav/xiaoyingtao.wav'); // http文件
await call.play('silence_stream://1000'); // 1秒静音
await call.play('silence_stream://2000'); // 2秒
await call.play('silence_stream://3000'); // 3秒
await call.hangup('NORMAL_CLEARING'); // 挂机
console.log(`Done uuid=${call.uuid} Total Calls: ${xcc.ncalls()}`);
}
如果要播放TTS,则需要设置TTS引擎,代码如下:
await call.answer();
.set_tts_params('ali', 'default'); // 设置TTS引擎和语法
callawait call.speak('您好');
V1版的SDK看起来就比较长了,但也比较直观,简要注释如下,供参考:
const NATS = require('nats') // 引入NATS
let nats_url = process.env.NATS_URL; // 从环境变量中获取NATS服务器地址
// 连接NATS服务器,默认为`nats://localhost://4222`
const nc = NATS.connect(nats_url);
const tts_engine = "ali" // 设置TTS引擎,相关模块必须存在
const uuidv4 = require('uuid/v4'); // UUID库
const ctrl_uuid = uuidv4(); // 随机生成一个UUID,作为本控制器的UUID
console.log("IVR started, waiting ...")
function default_rpc()
{var rpc = { // 构造JSON-RPC对象
jsonrpc: "2.0",
method: "XNode.Answer",
id: "fake-uuid-answer",
params: {
ctrl_uuid: ctrl_uuid
}
}
return rpc
}
// 应答函数,service为XNode侧的Subject,uuid为当前Channel的UUID
function xnode_answer(service, uuid, callback)
{// 设置应答请求的参数
var rpc = default_rpc();
.method = "XNode.Answer";
rpc.params.uuid = uuid
rpc
// 转为JSON字符串
const msg = JSON.stringify(rpc);
console.log("sending " + msg);
// 发送请求,超时时间为1000毫秒,然后会执行callback函数
.request(service, msg, { max: 1, timeout: 1000 }, callback);
nc
}
// 播放函数,uuid为当前Channel的UUID,file为语音文件名
function xnode_play(service, uuid, file, callback)
{var rpc = default_rpc();
.id = 'fake-play';
rpc.method = "XNode.Play";
rpc.params.uuid = uuid;
rpc.params.media = {
rpcdata: file,
}
const msg = JSON.stringify(rpc);
console.log("sending " + msg);
// 发送请求,简单起见,超时时间硬编码为20秒,可自行调整
.request(service, msg, { max: 1, timeout: 20000 }, callback);
nc
}
// 订阅本Ctrl的Subject,当收到消息时打印相关消息
.subscribe('cn.xswitch.ctrl.' + ctrl_uuid, function (msg, reply, subject, sid) {
ncconsole.log('Received a message: ' + subject + ' ' + msg)
})
// 订阅消息,等待呼入,电话呼入后的第一个消息会发到这里
.subscribe('cn.xswitch.ctrl', {queue: 'controller'}, function (msg, reply, subject, sid) {
ncconsole.log('Received a message: ' + subject + ' ' + msg)
// 收到的是一个字符串,转换成Javascript对象
= JSON.parse(msg);
m
// 简单的合法性检查
if (m.method) {
if (m.method == 'Event.Channel') {
if (m.params.state == 'START') { // 第一个消息的state = START
// 注意,这个消息中没有`id`,也就是说这只是一个事件通知
console.log('new call from ' + m.params.cid_number) // 打印主叫号码
const channel_uuid = m.params.uuid; // 这是当前Channel的UUID
// node_uuid是XNode侧的UUID,拼成一个Subject,XNode侧应该已经订阅了该Subject
const service = 'cn.xswitch.node.' + m.params.node_uuid;
// 应答,它将会发送一个应答指令,这是一个JSON-RPC请求消息
xnode_answer(service, channel_uuid, (msg) => { // 应答请求的回调函数
if (msg instanceof NATS.NatsError && msg.code === NATS.REQ_TIMEOUT) {
console.log('request timed out');
else {
} console.log('Got a response: ' + msg);
= JSON.parse(msg);
m if (m.result.code == 200) { // 200代表请求成功
var file = '/tmp/welcome.wav'; // 播放文件
xnode_play(service, channel_uuid, file, (msg) => {
if (msg instanceof NATS.NatsError && msg.code === NATS.REQ_TIMEOUT) {
console.log('request timed out');
else {
} console.log('Got a response: ' + msg);
};
})
}
};
})
}
}
}
})
// 永久等待,防止退出
function wait() {
// console.log('tick ... ');
setTimeout(wait, 3000);
}
wait();
Node.js实际上只有一个线程,所有请求的结果都使用回调函数来实现。Async/Await的版本可以以同步的方式写异步代码,代码看起来更简单,但其实只是个语法糖,学起来也是有门槛的,需要懂Promise。
外呼代码片断如下。
let auto_play = false; // 是否自动播放,见后面
// 设置呼叫字符串,该示例号码会自动接听
const dial_string = 'sofia/public/10000200@rts.xswitch.cn:20003;transport=tcp';
function default_rpc() {
var rpc = {
jsonrpc: "2.0",
method: "XNode.Answer",
id: "fake-uuid-answer",
params: {
// 简单起见使用硬编码,实际使用时应该保证与其它Controller不重复,最好是个真正的UUID
ctrl_uuid: 'test-nodejs-controller'
}
}
return rpc
}
// 播放
function xnode_play(service, uuid, file, callback) {
var rpc = default_rpc();
.method = "XNode.Play";
rpc.id = uuid
rpc.params.uuid = uuid
rpc.params.media = {
rpcdata: file
}
const msg = JSON.stringify(rpc);
console.log("sending " + msg);
.request(service, msg, { max: 1, timeout: 5000 }, callback);
nc
}
// 挂机
function xnode_hangup(service, uuid, callback) {
var rpc = default_rpc();
.id = 'fake-uuid-hangup'
rpc.method = "XNode.Hangup";
rpc.params.uuid = uuid
rpc
const msg = JSON.stringify(rpc);
console.log("sending " + msg);
.request(service, msg, { max: 1, timeout: 1000 }, callback);
nc
}
console.log("caller started, waiting ...");
// controller handler,与该Controller相关的消息都会发到这里
.subscribe('cn.xswitch.ctrl.test-nodejs-controller', function (msg, reply, subject, sid) {
ncconsole.log('Received a message: ' + subject + ' ' + msg)
= JSON.parse(msg);
m
if (m && m.params) {
if (m.params.state == 'DESTROY') {
= false;
running else if (m.params.state == 'READY') { // 呼叫成功后,收到的第一个消息
} if (auto_play) { // 如果使用auto_play,这里我们什么也不需要做
// auto_play mode, just waiting for hangup
console.log("call is auto playing, waitinng to hangup");
return;
}
// 根据node_uuid构造XNode侧的Subject
var service = 'cn.xswitch.node.' + m.params.node_uuid;
const channel_uuid = m.params.uuid;
var file = 'silence_stream://3000';
xnode_play(service, channel_uuid, file, (msg) => { // 播放文件
if (msg instanceof NATS.NatsError && msg.code === NATS.REQ_TIMEOUT) {
console.log('request timed out');
else {
} console.log('Got a response: ' + msg);
}
})
}
}
})
// 每一路电话都有一个UUID,这里我们事先构造一个
const uuid = uuidv4()
console.log('calling ' + uuid + ' ...')
var rpc = default_rpc();
.method = "XNode.Dial" // 外呼
rpc.id = 'call1'; // 请求id,实际使用时应该使用不重复的id以方便跟踪
rpc.params.destination = {
rpcglobal_params: {
ignore_early_media: "true" // 忽略早期媒体(如回铃音、彩铃等)
,
}call_params: [{ // 这是个数组,数组中的每一个对象代码一路,可以并行呼叫,这里我们只呼一路
uuid: uuid, // Channel UUID
dial_string: dial_string, // 呼叫字符串
params: { // 呼叫参数
leg_timeout: "20", // 无应答超时
// absolute_codec_string: "G729", // 使用何种编解码
}
}]
}
if (auto_play) { // 在auto_play的情况下,呼通后会自动执行这些app,而无须本侧再控制,比较简单
.params.apps = [
rpc// 播放一次铃声
app: "playback", data: "tone_stream://%(100,1000,800);loops=1"},
{// 播放三次铃声
app: "loop_playback", data: "+3 tone_stream://%(100,1000,800);loops=1"},
{// 挂机
app: "hangup", data: "NORMAL_CLEARING"}
{;
]
}
// XNode侧订阅的Subject
= "cn.xswitch.node.test"
service
// 发送呼叫请求,呼叫进展消息(振铃、应答等)将可在cn.xswitch.ctrl.test-nodejs-controller这个主题上接收
.request(service, JSON.stringify(rpc), { max: 1, timeout: 30000 }, (msg) => {
ncif (msg instanceof NATS.NatsError && msg.code === NATS.REQ_TIMEOUT) {
console.log('request timed out');
else {
} console.log('Got a response: ' + msg);
= JSON.parse(msg);
m if (m.result && m.result.code == 200) { // good
console.log("so far so good");
}
}; })
如果你已经熟悉FreeSWITCH和ESL,也可以直接使用XCC API提供的NativeAPI
和NativeApp
进行开发。当然,如果你不熟悉,也可以直接忽略本节的内容。
XCC API可以完全替代ESL,也可以支持ESL中的inbound和outbound模式。本质上,ESL主要有三种概念和操作:
api
命令实现,用于控制FreeSWITCH,向FreeSWITCH发送指令sendmsg
实现,用于执行FreeSWITCH中的Application事件订阅可以直接使用NATS提供的subscribe
功能实现,可以订阅FreeSWITCH中原生的事件,如:cn.xswitch.ctrl.event.CHANNEL_ANSWER
,或直接订阅所有原生事件:cn.xswitch.ctrl.event.>
。事件的Subject前缀可以在配置文件中通过publish-events-subject-prefix
指定,所需的事件类型可以在bindings
部分配置,这些都需要在XSwitch侧的xcc.conf
配置文件中指定,详见第章配置文件。
原生事件以JSON-RPC格式发出,可以在各种语言中很简单的解析其中的内容。
在FreeSWITCH中,inbound和outbound都是相对FreeSWITCH而言的。前者相当于Ctrl侧连接到FreeSWITCH上主动控制它。在XCC中,实际上XNode和XCtrl双方都是连接到NATS上,因而可以互相收发消息。
Ctrl侧可以使用使用NativeAPI
发送API命令,查询系统状态或发起通话,如:
{
"jsonrpc": "2.0",
"id": "0",
"method": "XNode.NativeAPI",
"params": {
"ctrl_uuid": "ctrl_uuid",
"cmd": "status"
}
}
{
"jsonrpc": "2.0",
"id": "0",
"method": "XNode.NativeAPI",
"params": {
"ctrl_uuid": "ctrl_uuid",
"cmd": "originate",
"args": "user/1000 &echo"
}
}
当FreeSWITCH侧有来话时,可以通过Dialplan查询请求Ctrl侧动态生成Dialplan,或者,可以简单地直接执行park
Application,Ctrl侧就可以收到CHANNEL_PARK
事件,进而可以使用NativeApp
进行应答,放音等处理,如:
{
"jsonrpc": "2.0",
"id": "0",
"method": "XNode.NativeApp",
"params": {
"ctrl_uuid": "ctrl_uuid",
"uuid": "channel_uuid",
"cmd": "answer"
}
}
{
"jsonrpc": "2.0",
"id": "0",
"method": "XNode.NativeApp",
"params": {
"ctrl_uuid": "ctrl_uuid",
"uuid": "channel_uuid",
"cmd": "playback",
"args": "/tmp/welcome.wav"
}
}
实际上,上一节最后一个示例也相当于outbound模式,只是由于XNode和XCtrl之间的连接是“天然”的,因而XNode无需像ESL中那样再初始化一个连接。一旦连接建立,后续的控制流程都是一样的。
当然,XCC API也专门设计了一个xcc
Application,类似于outbound模式与Ctrl侧连立一个“虚”连接,这在XNode与XCtrl多对多的情况下非常有用,这不属于原生的FreeSWITCH功能,但更强大,下面我们来做一个对比:
在ESL中,来话的Channel通过socket
Application可以初始化一个outbound连接到你自己实现的ESL Server,你的ESL Server检测到连接到来时在底层回复一个“connect\n\n
”来确认接收这个请求,然后FreeSWITCH才开始发送Channel Data事件以启动后续的交互。
在而XCC中,xcc
也是一个Application,它向NATS广播一个Event.Channel
事件,所有订阅并收到这个事件的Ctrl都可以竞争接管这路呼叫,接管可以通过Accept
或Answer
实现。
所以,XCC API提供的NativeAPI
和NativeApp
完全可以代替原来ESL的功能,其它的XCC API功能更强大,且有更多的保护,推荐尽量使用非Native的XCC API。
track
模式有时候,在使用ESL或原生Dialplan的场景下,也希望接收XCC的Event.Channel
消息,这时候可以使用xcc_track
Application实现。使用方法:
xcc_track <cn.xswitch.ctrl.$ctrl_uuid>
answer
playback ...
XCC消息会发到对应的ctrl_uuid
上,当且仅当在这种情况下,Event.Channel
消息的state = TRACK
。
注意,该模式主要用于获取事件模式。在这种状态下,XCC API可能不能正常工作,因此,不建议使用XCC API控制Channel。
已知可以工作的XCC API如下:
id
为空,使用publish
而非request
请求的模式,结果会发送到对应的ctrl_uuid
上。如果取消控制,可以使用xcc_untrack
Application。
参见示例:https://git.xswitch.cn/xswitch/xcc-examples/src/branch/master/python#关于-asr-event-py 。
本系统提供的API称为XCC API。XCC API也支持调用FreeSWITCH底层原生的API,但原生API可能失去XCC的控制,请尽量使用XCC API。
通用参数:
ctrl_uuid
: 当前Controller的UUID。接管来话,当前通话信息以后所有的事件都将会发到接管的Ctrl上。
uuid
: 当前Channel的UUID。takeover
: true|false
。用于多个Ctrl的场景,另一个Ctrl可以接替原来的Ctrl处理呼叫。在呼入场景中,这应该是Ctrl在收到Event.Channel(state = START
)后发送的第一个消息。否则,后续的API可能失败,但Answer除外,Answer会隐含Accept,这主要是为Ctrl在开发时提供一些便利。
用于播放回铃音。调用ring_ready
发送SIP 180消息。
应答。
播放本地的语音文件,或存放在HTTP服务器上的文件,或TTS等。
media
:媒体media
参数定义:
type
:枚举字符串,文件类型,见下文data
:字符串,语音文件或TTSengine
:字符串,TTS引擎,目前支持华为、迅飞、阿里、百度等。voice
:字符串,嗓音,由TTS引擎决定,默认为default
。loop
:正整数,重复次数,暂未实现。offset
:正整数,语音文件播放偏移量。以采样点为单位,如文件采样率为8000
,则偏移8000
即1
秒。暂仅支持单个文件。对于file_string
类型只对第一个文件起作用。其中engine
和voice
为TTS参数。
文件类型有:
FILE
:文件TEXT
:TTS,即语音合成SSML
:TTS,SSML格式支持(并非所有引擎都支持SSML)在播放的过程中,会检测DTMF并以事件(Event.DTMF
)形式发出。
停止播放。
播放本地的语音文件,或存放在HTTP服务器上的文件,或TTS等。如果uuid
处于BRIDGE
状态,则播放到通话的双方。
uuid
:当前UUID。media
:媒体,参见Play。option
:参数,默认为BOTH
。option参数:
BOTH
:双方。ALEG
:只有当前UUID能听到。BLEG
:当前UUID的对端能听到。AHOLDB
:同ALEG
,同时对B放保持音乐(如果配置了的话)。BHOLDA
:同BLEG
,同时对A放保持音乐。录音。传入相同的path
可以开始和结束录音。录音将存储在FreeSWITCH本地文件,Ctrl收到相关信息后可以通过其它方式下载录音。具体录音文件的存储不在本文档范围内。
录音有两种情况:
非阻塞录音:后台在单独的线程中启动录音,不影响当前操作,如可以继续Play或Bridge。
阻塞录音:适用于单腿的,语言留言之类的情况。
path
:文件路径。
limit
:可选,时长,单位为秒。
action
:动作。
动作可以有:
RECORD
:阻塞录音。START
:开始录音。STOP
:结束录音。MASK
:屏蔽敏感信息,如DTMF或密码等。UNMASK
:停止屏蔽。在非阻塞录音会立即返回,可以在腿上执行后续的操作如Play或Bridge等。非阻塞录音无法获取录音结果,但相关结果可以通过监听RECORD_START
和RECORD_STOP
事件获取。
阻塞录音有以下参数:
beep
:播放一个“嘀”声,可以输入default
,或者任何合法的TGML:https://freeswitch.org/confluence/display/FREESWITCH/TGML 。terminators
:字符串如1234
,按键可以打断录音。thresh
:VAD声音检测阈值,0
为禁用。1~100000
,值越大声音越小,即越敏感。silence_seconds
:静音时长,如果检测到静音超过这个时长则自动停止录音。阻塞录音将在录音结束后返回结果。
terminator
:如果录音被打断,则返回相关按键。path
:文件路径。挂机。
flag integer
值为
0
:挂断自己1
:挂断对方2
:挂断双方cause:挂机原因,参见Dial
中相关定义。
data:是一个对象,可以同时设置多个属性。如果挂机前需要设置channel变量,可以使用该字段。该字段设置后,会自动订阅,不需要再修改配置文件或者使用SetVar接口去专门订阅。
例子:
{
"method": "XNode.Hangup",
"params": {
"flag": 0,
"data": {
"var1": "value1",
"var2": "value2"
}
}
}
播放一个语音并获取用户按键信息,将在收到满足条件的按键后返回。
min_digits
:最小位长。max_digits
:最大位长。timeout
:超时,默认5000ms
。digit_timeout
:位间超时,默认2000ms
。terminators
:结束符,如#
。data
:播放的媒体,可以是语音文件或TTS。返回结果:
dtmf
:收到的按键。terminator
:结束符,如果有的话。本接口将在收到第一个DTMF按键后打断当前的播放。
检测语音,可以同时检测DTMF。如果不需要检测语音,仅检测DTMF,请使用ReadDTMF接口。
media
:媒体,参见Play。dtmf
:DTMF参数,参见ReadDTMF中的定义,如果不需要同时检测DTMF,可以不传该参数。speech
:对象,语音识别请求。speech
定义:
engine
:字符串,ASR引擎。no_input_timeout
:正整数,未检测到语音超时,默认为5000
ms。speech_timeout
:正整数,语音超时,即如果对方讲话一直不停超时,最大只能设置成6000
ms,默认为6000
ms。max_speech_timeout
:正整数,语音最大超时,和参数speech_timeout
作用相同,如果max_speech_timeout
的值大于speech_timeout
,则以max_speech_timeout
为主,用于一些特殊场景的语音时长设置。partial_events
:Bool,是否返回中间结果。有些引擎支持返回中间识别结果。以事件形式发出。nobreak
:Bool,禁止打断。用户讲话不会打断放音。放音期间识别到用户讲话的内容将全部缓存。多“句”话之间使用“。
”拼接成一个text
字段发出。其它字段如confidence
等将使用第一个返回的结果,可能没有代表性。merge_delimiter
:String,合并分隔符。暂未实现。默认为“。
”。在nobreak
状态下,如果需要拼接结果中插入分隔符。disable_detected_data_event
:Bool,默认会发送Event.DetectedData
事件,如果为true
则不发送。params
:不同引擎特定的参数,全部为字符串键值对,由具体的引擎定义。识别结果:
uuid
:当前Channel的UUID。text
:识别到的文本。confidence
:double
类型,结果可信度,跟具体引擎相关。is_final
:是否是最终结果。engine_data
:ASR引擎原生返回的数据,适用于可以进一步获取更多信息。 error
:错误,如no_input
、speech_timeout
等。type
:结果类型,有DTMF
、Speech.Begin
、Speech.Partial
、Speech.End
,Speech.Merged
、ERROR
等。offset
:如果是文件播放过程中被打断,则该值为打断时的偏移量。可以在打断位置继续播放。本接口将在第一时间检测到语音后打断当前的播放。可以加一个参数控制是否打断,暂未实现。
正常情况下,本接口会在检测到结果后返回,以便进行下一轮交互。
本接口也可以支持连续识别。如果在调用时不传任何media
参数,则会启动后台识别,识别到的结果以事件(Event.DetectedData
)形式发出。在连续识别过程中,可以继续进行放音操作。(本操作需要相应的后台设置)。
分段录音:
分段录音在ASR引擎上实现,因此做为引擎的参数(params
)传入。
segment-recording-prefix
:分段录音路径前缀,将与真正的文件名拼接。如果前缀是一个目录,必须以/
结尾,如/tmp/
,建议使用带时间戳的前缀,如/tmp/20200202-121212-
。文件名后半部分暂时使用引擎相关的唯一值,如阿里云的引擎中我们使用header.message_id
作为文件名。人脸识别接口。本接口将在后台启动一个Media Bug。并将通话置于Echo状态(即客户会看到当前自己的人脸)。
在人脸识别过程中,每隔1.5
秒返回一张图片(PNG格式经过Base64算法转成data-url
)。业务侧可以将图片传到外部人脸识别系统中进行比对。
相关接口参考:
https://cn.aliyun.com/product/bigdata/product/face
https://cloud.tencent.com/document/product/867/44987
uuid
:当前通话UUID,必填。
mask
:PNG图片作为人脸型的框架,一般为透明图片,默认为/usr/local/freeswitch/images/mask.png
。
action
:字符串。START
:开始。STOP
:结束。TEXT
:写文字。CLEAR
:清除文字。
如果action
为TEXT,则有以下参数:
text
:文本font
:字体文件路径font_size
:字体大小(像素)fg_color
:前景色bg_color
:背景色结果:
uuid
:UUIDpicture
:data-url格式的字符串width
:图像宽度height
:图像高度本接口为非阻塞接口,调用者在首次调用action = START
后,将收到code = 202
消息。然后调用者应该循环待,后续的结果中将有picture
返回,然后可以将picture
传给第三方接口进行比对或识别。识别完毕可以STOP
结束识别。
识别过程中,可以调用action = TEXT
在图片上添提提示文本(如请转头、请眨眼等)。
uuid
:当前通话UUID,必填。dtmf
:DTMF字符串,必须是合法的DTMF。合法的DTMF如下:
0~9
:数字A~F
:特殊按键w
:停顿500毫秒W
:停顿1秒uuid
:当前通话UUID,必填。content_type
:可选,字符串,默认为text/plain
。data
:可选,字符串,默认为空字符串。使用该功能需要设置通道变量fs_send_unsupported_info=true
,或直接开启全局的变量。
外呼。直接同时发起多个呼叫,但其中任意一个接听后,其它的呼叫都将挂机。
ringall
:true/false。同振,默认为顺振。global_params
:FreeSWITCH 呼叫字符串{}
里的参数,作用于所有的腿,具体请参见FreeSWITCH中相关定义。call_params
:呼叫参数,是一个数组,数组中的每个对象都代表一条腿。所有的键和值都必须是字符串。呼叫参数定义:
uuid
:新Channel的UUID,必须由Ctrl生成,并且所有Channel生存周期内不能重复(建议永远不要重复),建议使用相关UUID算法生成。cid_number
:主叫号码。cid_name
:主叫名称。dial_string
:FreeSWITCH原生呼叫字符串,如user/xxx
、sofia/gateway/gwx/xxxx
等。以后的版本中可以增加相关的对象代替FreeSWITCH原生的字符串。详情参阅附录中「呼叫字符串」部分。max_duration
: 最长通话时间,单位是秒。最小60秒,最长24 * 3600秒,即24个小时,缺省值为24 * 3600秒。params
:新Channel的相关参数,对应FreeSWITCH中呼叫字符串的[]
里面的定义,仅作用于这一条腿。此外的参数会被global_params
中的同名参数覆盖。外呼成功后,如果是同步请求,则可以直接返回结果,否则可以异步的获取结果,并等待有Event.Channel(state = READY
)消息后开始下一步操作。
注意: 该命令会在成功收到媒体后返回,比如在SIP消息中,收到183
时就返回,以便进行后续的操作。如果需要在对方接听后返回,则可以使用ignore_early_media=true
参数。
呼叫过程中相应的呼叫进展后有相关的Event.Channel
事件发送到当前Ctrl上,如cn.xswitch.ctrl.<ctrl_uuid
>。
外呼的呼叫会自动接管,不需要发送Accept
。
外呼的结果中cause
为成功或失败原因,列表如下:
SUCCESS
:成功,可以进行下一步操作。USER_BUDY
:被叫忙。CALL_REJECTED
:被叫拒接。NO_ROUTE_DESTINATION
:找不到路由。更多定义参见第节常用挂机原因说明。
JSON示例:
{
"jsonrpc": "2.0",
"id": "request-uuid",
"method": "XNode.Dial",
"params": {
"ctrl_uuid": "some-uuid",
"ringall": true
"global_params": {
"ignore_early_media": "true -- must be a string",
"caller_id_number": "110"
},
"call_params": [{
"uuid": "uuid",
"cid_number": "7777",
"dial_string" = "user/1000"
}, {
"uuid": "uuid"
"dial_string" = "sofia/gateway/test/1000",
"params": {
"leg_timeout": "20 -- must be a string",
"ignore_early_media": "true -- must be a string"
}
}]
}
}
日志打印。
通过调用本接口可实现在XSwitch内打印想要输出的日志信息。该接口不依赖于通话,可在任意处进行调用。
level
:日志级别。字符串类型。可从以下级别中任选其一。默认为DEBUG
。
function
:调用该接口代码所在函数名称。
file
:调用该接口代码所在文件名称。
line
:调用该接口代码行数。整型。
log_uuid
:任意字符串。可选。建议为当前通话UUID
。
data
:想要打印的日志信息。
注意,以上六个参数中,只有log_uuid
为可选参数,其他均为必填
JSON示例:
{
"jsonrpc": "2.0",
"method": "XNode.Log",
"params": {
"ctrl_uuid": "f8a02eed-3ea6-42a2-838d-856f529d3fbc",
"level": "ALERT",
"function": "xnode_status",
"file": "log.js",
"log_uuid": "",
"line": 69,
"data": "Hello, this is a test"
},
"id": "fake-log"
}
XSwitch内日志打印如下: 2022-07-14 13:04:07.427643 98.80% [ALERT] log.js:69 Hello, this is a test
开启带内DTMF检测。
force
:可选参数,是否强制开启带内DTMF检测。本接口将可能或者强制开启带内DTMF检测。使用本接口时,如果不带force
参数或者其值为flase
的情况下,只有实际通话协商DTMF不是2833的情况下,才会开启带内DTMF检测,因为对于大部分通话来说,DTMF都是2833,这种情况是没必要开启带内检测的,如果开启会增加相应的CPU开销。当参数force
值为true
时,将强制开启带内DTMF的检测,因此一般情况下不建议开启此参数。
将当前通话桥接(发起)另一个呼叫。
该功能会发起一个新的呼叫(在a-leg
的基础上创建b-leg
)。呼叫参数与Dial
相同。a-leg
会听到b-leg
的回铃音。也可以通过ignore_early_media=true
参数抵制回铃音。
flow_control
:呼叫控制,跟程控交换机中的控制方式类似,略有不同。
NONE
:无控制,任意方挂机不影响其它一方CALLER
:主叫控制,a-leg
挂机后b-leg
自动挂机CALLEE
:被叫控制,b-leg
挂机后a-leg
自动挂机ANY
:互不控制,任一方挂机后另一方也挂机contine_on_fail
:字符串类型。该参数控制在Bridge失败的情况下的逻辑。如果Bridge失败(如空号、被叫忙等),则是否继续:
true
:继续,继续保持PARK状态false
:挂机USER_BUSY,NO_ANSWER...
:逗号分隔的字符串列表,如果遇到列表中的挂机原因则继续(即白名单),否则会自动挂机。桥接两个uuid
,两个Channel必须为未Bridge状态,且处于PARK状态(没有执行其它App)。 当unbridge之后返回这个api调用的回复消息。
uuid
:当前uuid
,为a-leg
。peer_uuid
:对方uuid
,为b-leg
。flow_control
:桥接成功后是否自动挂机,参见Bridge中相关定义。桥接两个uuid
,两个Channel必须为未Bridge
状态,且处于PARK
状态(没有执行其它App)。
当桥接完成后马上返回api调用的回复消息
A与B通话中。将A与B分离,分别park。
将通话置于保持状态。
内部要在controller
中记住状态。
解保持,清除保持状态。
转移。如果当前uuid
只有一路通话,则直接Bridge destination
,跟Bridge一样处理。
如果当前uuid
处于Bridge状态,则挂断peer_uuid,然后执行Bridge。
message TransferRequest {
Ctrl ctrl_uuid = 1;
string uuid = 2;
string ringback = 3;
Destination destination = 4;
}
三方。A与B通话,用Dial呼叫C或C呼入,然后通过该API建立三方。其中uuid
为C,target_uuid
为A或B。
direction
有以下取值(下文中,C为uuid
,A为target_uuid
,B为target_uuid
的对端):
LISTEN
:监听模式,C能听到A和B,A和B听不到CABC
:三方模式,三方互相能听到其它两方AC
:A与C通,B听MOHBC
:B与C通,A听MOHTOA
:C听AB,A听BC,B听A,即A与B正常通话,但C可以单独跟A讲话(耳语)TOB
:C听AB,B听AC,A听B,即A与B正常通话,但C可以单独跟B讲话(耳语)STOP
:停止三方message ThreeWayRequest {
Ctrl ctrl_uuid = 1;
string uuid = 2;
string target_uuid = 3;
string direction = 4;
}
在ThreeWay的过程中,如果初始化状态为LISTEN
或ABC
模式(其它状态无法自由转换),可以传入不同的direction
并改变通话逻辑。如,最开始使用LISTEN
模式兼听,然后改成使用TOA
模式跟A耳语,然后改成ABC
模式三方,最后STOP
停止。
强插。A与B通话中。C呼入或者通过Dial呼通C,uuid
为C,然后target_uuid
为A或B,建立通话后挂掉另一方。
message InterceptRequest {
Ctrl ctrl_uuid = 1;
string uuid = 2;
string target_uuid = 3;
}
在通话中听到自己发出的声音。
action
:START
(默认) | STOP
,开始或停止direction
:SELF
(默认)| OTHER
,听到自己的声音,或让对方听到自己的声音。静音,在通话中静音
参数:
uuid
: 当前session ALEG的uuiddirectionn
:静音方向。WRITE
:静音ALEG发送出去的声音;READ
:静音BLEG发送过来的声音;BOTH
:静音ALEG和BLEG。level
: integer
,0
非静音,1
静音,大于1 静音并发送舒适噪声数值代表舒适噪音的音量。flag
: media bug插入顺序, FIRST
代表插入到最头上,LAST
代表插入到最尾。通话过程中添加水印。
参数:
action
:START
| STOP
| RESTART
,增加水印,移除水印及覆盖水印marked_text
:文字水印,显示在左上角marked_with_time
:时间水印,显示在左上角marked_fsize
:水印字体大小marked_fcolor
:水印字体颜色marked_fface
:水印字体,默认为FreeMonomarked_file
:文件水印,通常是图片marked_file_mode
:文件水印显示模式 - logo
:显示在右下角,长宽占比不超过1/4 - bottom
:靠下侧居中显示 - top
:靠上侧居中显示 - custom
:自定义marked_file_alpha
:true
| false
,水印显示是否使用透明模式marked_abs_x
:文件水印自定义模式的横坐标marked_abs_y
:文件水印自定义模式的纵坐标设置通道变量(随路路数据),变量一经设置后在Channel生命周期内会一直有效。
参数:
uuid
:当前Channel的的UUIDdata
:数据,是一个对象,可以同时设置多个属性。channel_params
:字符串数组,同时订阅这些变量,以便以后的Event.Channel事件中携带。如:
{
"method": "XNode.SetVar",
"params": {
"data": {
"var1": "value1",
"var2": "value2"
},
"channel_params": [
"var1",
"var2",
"source",
"context"
]
}
}
其中、data
和channel_params
参数都是可选的这样设计的原因是可以在设置通道变量的同时进行订阅,而不需要再使用一个API订阅一次。如果只想订阅通道变量而不设置,则可以省略data
参数。
获取通道变量(随路路数据)。如果想临时获取一些通道变量,可以使用本函数。
系统提供执行FreeSWITCH原生API的能力,用于在XCC API不够用的情况下使用。如果某NativeApi使用频繁,建议包装成XCC API。
message NativeRequest {
string ctrl_uuid = 1;
string cmd = 2;
string args = 3;
}
cmd
:FreeSWITCH原生命令,如status
、sofia
等args
:FreeSWITCH命令参数,如status
命令没有参数,可以为空,sofia
命令的参数status
等。message NativeRequest {
string ctrl_uuid = 1;
string cmd = 2;
string args = 3;
string uuid = 4;
}
cmd
:FreeSWITCH原生App,如answer
、echo
、conference
等args
:FreeSWITCH命令参数,如echo
命令没有参数,可以为空,conference
的参数可以是一个会议室名称,如3000
。message NativeJSRequest {
string ctrl_uuid = 1;
message NativeJsData {
string command = 1;
// a string or a native JSON struct to google.protobuf.Any or .Struct
string data = 2;
}
NativeJsData data = 2;
}
原生的JSAPI。
command
:JSAPI命令,如conferenceInfo
data
:JSAPI命令参数,可以是字符串形式,也可以是JSON形式,必须是一个Object,如:{}
示例请求:
{
"jsonrpc": "2.0",
"id":"status",
"method": "XNode.NativeJSAPI",
"params": {
"ctrl_uuid":"test-nodejs-controller",
"data": {
"command": "status",
"data":{}
}
}
}
响应:
{
"jsonrpc": "2.0",
"id": "status",
"result": {
"code": 200,
"message": "OK",
"node_uuid": "xcc-node-1",
"data": {
"systemStatus": "ready",
"uptime": {
"years": 0,
"days": 0,
"hours": 2,
"minutes": 54,
"seconds": 23,
"milliseconds": 607,
"microseconds": 581
},
"version": "1.10.7-dev git b208d55 2021-06-15 03:52:14Z 64bit",
"sessions": {
"count": {
"total": 5,
"active": 0,
"peak": 1,
"peak5Min": 0,
"limit": 20000
},
"rate": {
"current": 0,
"max": 20000,
"peak": 1,
"peak5Min": 0
}
},
"idleCPU": {
"used": 0,
"allowed": 95.36666666666666
},
"stackSizeKB": {
"current": 240,
"max": 8192
}
}
}
}
XSwitch的mod_xcc
模块中实现一个XCC
Dialplan接口,使用该Dialplan后,XSwitch在每次查询Dialplan时都会向Ctrl发送request
请求。
cn.xswitch.ctrl
XCtrl.Dialplan
Ctrl侧返回一个数组:
[
{"app": "answer", "data":""}
{"app": "echo"}
]
XSwitch会顺序执行数组中的app
,这些app
都是XSwitch原生的Application。
cn.xswitch.ctrl
XCtrl.FetchXML
XSwitch通过XML Binding实现动态XML。相关概念可参考mod_xml_curl
。
https://freeswitch.org/confluence/display/FREESWITCH/mod_xml_curl
在XSwitch内部实现一个绑定回调函数,XSwitch在每次需要XML时,都会回调该函数。
在mod_xcc
中,每次绑定的回调都会发送request
请求。
请求示例如下:
{
"jsonrpc": "2.0",
"method": "XCtrl.FetchXML",
"id": "5ca1bf44-1f80-4845-b538-12ca29086e0e",
"params": {
"node_uuid": "5cc473e8-ad01-481e-9f28-9291b1bad9d5",
"section": "configuration",
"tag_name": "configuration",
"key_name": "name",
"key_value": "sofia.conf",
"params": {
}
}
}
可以绑定如下Section:
dialplan
:拨号计划,路由。不推荐使用,推荐使用XCC Dialplan代替。configuration
:配置,模块的配置等。directory
:用户目录。channels
:动态Channel,在转接的时候有用。chatplan
:即时消息的路由。languages
:多语言支持。XSwitch内部支持多个绑定,多个绑定之间的执行顺序不固定,因而可能会有冲突,慎用。
服务方收到XSwitch的请求后,返回正常的XML文档即可:(其中$
相关的变量可以从请求中获取)
<document type="freeswitch/xml">
<section name="${params.section}">
<${params.tag_name} ${params.key_name}="${params.key_value}"/>
... 实际的内容</section>
</document>
如果“实际的内容”为空,则FreeSWITCH找不到相关的数据。FreeSWITCH也不会Fallback到其它数据。
如果服务方不提供数据,但允许FreeSWITCH去查询其它绑定,则返回not found
消息:
<document type="freeswitch/xml">
<section name="result">
<result type="not found"/>
</section>
</document>
如果没有其它绑定,或者绑定没有找到数据,FreeSWITCH会继续查询本地的静态XML配置。
如果希望FreeSWITCH跳过所有绑定直接查询本地静态XML,则可以返回一个空的XML,如:
<document type="freeswitch/xml"/>
Fallback规则在实际使用时会增加复杂性,有时候不知道哪个配置是生效的,因此,慎用。
当用户注册时或作为被叫时发送请求,请求示例如下:
{
"jsonrpc": "2.0",
"method": "XCtrl.FetchXML",
"id": "e54b7f58-2edc-4a13-a8d8-8c74b854cb39",
"params": {
"node_uuid": "f52310ba-12c0-40a0-b0d3-f28a8deed5ab",
"section": "directory",
"tag_name": "domain",
"key_name": "name",
"key_value": "seven.local",
"params": {
"key": "id",
"user": "1007",
"domain": "seven.local",
"ip": "172.22.0.1"
}
}
}
服务方应该返回XML字符串。
请求示例如下:
{
"jsonrpc": "2.0",
"method": "XCtrl.FetchXML",
"id": "5ca1bf44-1f80-4845-b538-12ca29086e0e",
"params": {
"node_uuid": "5cc473e8-ad01-481e-9f28-9291b1bad9d5",
"section": "configuration",
"tag_name": "configuration",
"key_name": "name",
"key_value": "sofia.conf",
"params": {
}
}
}
服务方应该返回XML字符串。
详细的录音解决方案超出了本文档的范围,下面仅就实现方式做相关探讨和建议。
.wav
格式,由外部程序延时转成mp3
可以节省存储空间。/usr/local/freeswitch/storage/recordings/
,文件名建议有日期前缀,如20200202-121212-
。http://localhost/recordings/20200202-121212-xxxxxx.wav
。inotify
接口获取录音完成,然后将录音移到或上传到其它云存储上。ali
引擎实现了分段录音。注意事项:
START
消息后应该在10秒内调用Accept
或Answer
接口接管呼叫。ignore_early_media=true
参数。cn.xswitch.ctrl
Event.Channel(state = START)
Accept
或Answer
ReadDTMF
或DetectSpeech
检测按键或语音桥接即将两条独立的腿接到一起,使双方可以通话。
这种模式最初只有一条腿,执行Bridge
接口创建另一条腿。
a-leg
呼入,或者使用Dial
外呼建立,在a-leg
上调用Bridge
接口发起另一路呼叫,呼叫成功后。使用如下控制逻辑:
NONE
:默认。在这种模式下,不管任意一方挂机,都不影响另外一方。另一方仍处于PARK
状态,可以执行后续的逻辑,如转“评价”等。ANY
:互不控制。在这种模式下,只要Bridge
成功,双方开始通话后,任一方挂机另一方自动挂机。控制比较简单。CALLER
:主叫挂机另一方自动挂机,但被叫挂机不影响主叫侧。CALLEE
:被叫挂机另一方自动挂机,但主叫挂机不影响被叫侧。如果任一方不能自动挂机(在另一方挂机后己方回到PARK
状态),则应该在Bridge
成功后,等待UNBRIDGE
事件到来后,再执行下一步操作。
在上述的简单桥接的情况下,如果呼叫不成功,则flow_control
不起作用。需要使用contine_on_fail
参数控制。
true
:默认,这种情况下a-leg
会继续处于PARK状态,这时候可以重试呼叫(Bridge),或者使用一个备用号码或备用网关进行呼叫(Bridge) false
:自动挂机。 原因列表:以逗号分隔的FreeSWITCH原因列表,如USER_BUSY,NO_ANSWER
。
使用该参数可以实现遇忙转移、无应答转移、通过第二路由或第二网关重新呼叫等。
如果呼叫不成功,可以根据Bridge
返回结果中的原因值(cause
)决定下一步该怎么做。
适用于两条腿已经存在的情况。也可以通过flow_control
参数控制是否自动挂机。
ignore_early_media=true
ignore_early_media=false
,以便A能听到回铃音state = READY
后,调用ThreeWay
:uuid = A, target_uuid = B, direction=LISTEN
,即A监听B,此时A能听到B的回铃音,A能收到state = BRIDGE
消息state = ANSWERED
后,调用DetectSpeech
与客户交互ThreeWay
:uuid = A, direction=STOP
state = UNBRIDGE
后,表明监听停止,然后调用ChannelBridge
:uuid=A, peer_uuid=B
state = BRIDGE
,A与B通话建立成功。配置文件为xcc.conf
,包含以下几个部分。
settings
参数设置。
debug
:调试日志级别,0~7
。nats-url
:NATS的地址,如nats://127.0.0.1:4222
,如果不配置则不连接NATS。publish-events-subject-prefix
:事件的Subject前缀,如.event
,将会变为cn.xswitch.ctrl.event.CHANNEL_ANSWER
之类。cdr-format
:CDR的格式,可以在CDR中选一种格式,见CDR部分。cdr-subject
:CDR专门的Subject,用于将CDR送到专门的Subject上,如果不配置默认送到Channel对应的Controller上。xml-handler-bindings
:XML绑定,不同的Section以|
分隔,如directory|config
。loki-push-url
:loki接受log push的地址,如http://127.0.0.1:3100/loki/api/v1/push
,配置该参数则表示日志消息直接推送到loki。如不配置则日志使用消息队列的方式向loki推送日志。 注意:/api/prom/push
已经弃用,使用/loki/api/v1/push
loki文档loki-send-gap
: integer
推送时间间隔,单位毫秒。超过这个时间间隔会推送所有缓存的日志 默认值1000毫秒loki-max-log-cache-count
: integer
缓存日志最大的条数,超过这个数量会将缓存日志推送到loki 默认值20loki-max-log-cache-mem-size
: integer
缓存日志最大内存占用值,单位是kb
,超过这个内存占用会将缓存的日志推送到Loki,默认值为4loki-cache-queue-size
: integer
缓存队列容量 默认值300bindings
事件绑定。绑定的事件会向NATS发送,ALL
会绑定所有事件。
发送的Topic可以由publish-events-subject-prefix
参数改变,如:
cn.xswitch.ctrl.event.DTMF
cn.xswitch.ctrl.event.channel_answer
cn.xswitch.ctrl.event.custom.sofia::register
cn.xswitch.ctrl.event.custom.conference::maintenance
通话相关事件可绑定如下事件:
CHANNEL_CREATE
//通话创建事件CHANNEL_STATE
//状态CHANNEL_ANSWER
//应答CHANNEL_HANGUP_COMPLETE
//挂机结束subs
向NATS订阅。
name
:Topic,可以是任意合法的NATS Subject或Kafka Topic。其中NATS支持queue
订阅。queue
:如果参数存在则在Queue方式订阅,即在集群订阅时,同一个Queue的订阅只会有一个订阅者收到。cdr
CDR参数。CDR格式。
leg
:a | b | ab
,记录哪条腿的话单。name
:出现在JSON中的字段名。value
:对应FreeSWITCH中通道变量的字段名。如:
<cdr leg="a">
<param name="caller_id_name" value="caller_id_name"/>
<param name="caller_id_number" value="caller_id_number"/>
<param name="cid_number" value="caller_id_number"/> <!-- 别名 -->
</cdr>
channel-params
通道相关参数。为了节省带宽,XSwitch仅在Channel事件中携带很少的必要的信息,但如果在特定的场景中需要更多的信息,则可以通过配置从通道变量中获取。这些信息将显示在Channel事件的params
对象中,所有数据均为字符串。
name
:显示在JSON对象params
字段中的名称,建议与value
一致value
:XSwitch通道变量名称,可以通过“|
”指定另一个变量,当“|
”前面的不存在时,尝试从第二个变量获取default_value
:可以指定一个默认值。特殊情况下使用,一般没有什么意义,不推荐使用。如:
<channel-params>
<param name="user_uuid" value="user_uuid"/>
<param name="user_domain" value="user_domin"/>
<param name="context" value="context|first_dialplan_context"/>
</channel-params>
以下链接中包含一个常用通道变量列表,其中,上述配置中的value
字段对应表格中的“channel variable name”列:
https://freeswitch.org/confluence/display/FREESWITCH/Channel+Variables
dialplan-params
Dialplan请求参数,附加在Dialplan请求中。name
和value
的含义与channel-params
中相同。如:
<dialplan-params>
<param name="cid_name" value="caller_id_name"/>
<param name="cid_number" value="caller_id_number"/>
<param name="dst_number" value="destination_number"/>
<param name="context" value="context"/>
<param name="direction" value="direction"/>
</dialplan-params>
本章讨论XSwitch集群组网。
来话处理如下图所示。假设中继侧来话可以以一定算法分配置到3个不同的XSwitch Node节点。XNode收到呼叫后,向NATS广播来话消息(Event.Channel(state = START)
),Ctrl收到后进行处理。
所有XNode都订阅至少两个Subject:
cn.xswitch.node
:所有XNode都以同一个Queue订阅,发往该Subject的消息只有一个Node能收到,也就是说接收消息是互斥的。cn.xswitch.node.n
:其中n = 1,2,3 ...
,每个Node独立订阅,发往该Subject的消息只有一个能收到,也就是说,可以通过它定向发消息。同理,所有Ctrl都订阅两个Subject:
cn.xswitch.ctrl
:以同一个Queue订阅,互斥接收。cn.xswitch.ctrl.n
:其中n = 1,2,3 ...
,每个Ctrl独立订阅。当有来话时,不失一般性,假设该来话分发到了node.1
,则它会构造一个消息,通过NATS发送到cn.xswitch.ctrl
上,右侧两个Ctrl都有可能收到该消息,但只有一个收到。不失一般性,假设ctrl.1
收到了这个消息,这时候,它从消息的内容中取出node_uuid
,知道了该消息是来自node.1
,因此,它往node.1
上回复一个XNode.Accept
指令,表示它想接管这一路呼叫。node.1
收到后,向ctrl.1
回复200 OK
,表示指令执行成功。至此,Node和Ctrl通过NATS建立了一个“虚连接”(一对一对应关系),在这路通话生存期间都是如此。
时序图如下所示:
去话逻辑与来话差不多,区别只是控制首先从Ctrl发起。假设ctrl.1
发起呼叫,它首先将XNode.Dial
请求发到cn.xswitch.node
这个Subject上,不失一般性,假设node.1
收到了这个请求,在后续的返回结果中,它会告诉ctrl.1
这个呼叫是在node.1
上处理了,因而也可以建议虚连接。
如果Ctrl想将Dial请求发到指定的Node上,也可以直接指定Node对应的Subject,如cn.xswitch.node.1
。至于Ctrl如何知道有哪几个Node,参见下一节节点管理。
Node.Register
消息,Ctrl侧可以订阅该消息以便感知节点上线。Node.Unregister
消息,请求注销。Node.Update
消息,该消息可以做为心跳保活消息使用。此外,该消息还携带节点当前的活动Channel数以及负载信息。Ctrl可以根据该信息向“负载最轻”的Node发送新任务。注意:防止优先级反转。分布式系统的一个难点或者说一个误区就是无条件地往“负载最轻”的节点上发消息。假设我们做了一个自动外呼系统,有三个Ctrl和三个Node。在某一时间,三个Ctrl都发现node.1
负载最轻,然后分别向它发送了10
个外呼任务,node.1
就会在同一时刻收到30个外呼任务,有可能立即成为“负载最重”的节点,甚至会过载。
总之,分布式系统从来都不是很简单就可以实现的,在实际使用时需要考虑各种边界情况。在实际使用时,简单的轮循算法基于就可以做到“足够好”,如果能配合一些反馈补偿机制,就能做到“更好”。
会议使用XSwitch原生会议实现。必须将会议锁定到同一台XSwitch。
command
:conferenceInfo
data
:数据
conferenceName
:必填,会议室名称showMembers
:可选,布尔值,是否显示会议室成员,默认为否。示例:
{
"jsonrpc":"2.0",
"id":"conference-request-1",
"method":"XNode.NativeJSAPI",
"params":{
"ctrl_uuid":"54d0d141-a748-406a-8a63-992f195789cc",
"data": {
"command":"conferenceInfo",
"data":{
"conferenceName":"3000",
"showMembers":true
}
}
}
}
{
"jsonrpc": "2.0",
"id": "conference",
"result": {
"code": 200,
"message": "OK",
"node_uuid": "xcc-node-1",
"data": {
"conference": {
"conference_name": "3000",
"member_count": 1,
"ghost_count": 0,
"rate": 8000,
"run_time": 381,
"conference_uuid": "41d33e47-881e-483f-bef9-8174a8516f2e",
"canvas_count": 0,
"max_bw_in": 0,
"force_bw_in": 0,
"video_floor_packets": 0,
"locked": false,
"destruct": false,
"wait_mod": false,
"audio_always": false,
"running": true,
"answered": true,
"enforce_min": true,
"bridge_to": false,
"dynamic": true,
"exit_sound": true,
"enter_sound": true,
"recording": false,
"video_bridge": false,
"video_floor_only": false,
"video_rfc4579": false,
"variables": {
},
"members": [{
"type": "caller",
"id": 2,
"flags": {
"can_hear": true,
"can_see": true,
"can_speak": true,
"hold": false,
"mute_detect": false,
"talking": true,
"has_video": false,
"video_bridge": false,
"has_floor": true,
"is_moderator": false,
"end_conference": false
},
"uuid": "a3c2df89-b6c9-4fb3-a0a4-c1934b21f5a9",
"caller_id_name": "Seven Du",
"caller_id_number": "1001",
"join_time": 381,
"last_talking": 0,
"energy": 100,
"volume_in": 0,
"volume_out": 0,
"output-volume": 0,
"input-volume": 0
}]
}
}
}
}
可以使用NativeAPI
控制会议,如:
{
"jsonrpc": "2.0",
"id": "...",
"method": "XNode.NativeAPI",
"params": {
"ctrl_uuid": "ctrl_uuid ... ",
"cmd": "conference",
"args": "list",
}
}
常用的命令有:(以下假定会议名称为3000
)
list
:列出该节点上所有会议json_list
:同上,返回JSON字符串xml_list
:同上,返回XML字符串3000 list
:仅列出名称为3000的会议3000 mute 1
:对member_id
为1
的成员静音3000 unmute 1
:对member_id
为1
的成员取消静音3000 mute all
:将所有人静音3000 mute uuid=5bb13395-acc8-4dbc-9e90-2c6de0a27f98
:将Channel UUID对应的成员静音conference
查看帮助可以监听原生的CUSTOM conference::maintenance
事件。
XCC支持WebRTC呼叫。
node_uuid
和ctrl_uuid
映射保证消息能正常路由。verto.invite
消息,此时,XCtrl查找可用节点,建立callID
与node_uuid
的映射,并通过verto.trying
消息通知客户端,客户端应该记住这个node_uuid
并在后续的消息中发送。callID
和node_uuid
的映射关系将在通话结束后解除。后续的通话可能会路由到其它节点。callID
和node_uuid
的对应关系,以便路由后续的消息(如Bye)。node_uuid
。node_uuid
,以便路由到原来的FreeSWITCH。node_uuid
是在客户端携带,因此能正确路由到相应的Node节点。cn.xswitch.ctrl.rtc
上,XCtrl查询到用户对应的Websocket,然后呼叫用户。Verto支持如下呼叫字符串:
xrtc/profile_name/user@domain
:FreeSWITCH将查找配置文件中名字为profile_name
的profile,并将呼叫发送到profile配置中的xrtc-ctrl-subject
节点上。由XCtrl负责后续寻址。xrtc/user@domain
:如果不带profile_name则默认为default
profile。{xcc_ctrl_uuid=xxxx}xrtc/profile_name/user@domain
:FreeSWITCH将忽略profile中的xrtc-ctrl-subject
,将消息发送到呼叫字符串携带的通道变量xcc_ctrl_uuid
上xrtc/user@domain;ctrl_uuid=xxxx
:XCtrl在发起呼叫时,首先查询用户Websocket对应的ctrl_uuid
,然后生成呼叫字符串。{ctrl_uuid=xxxx}xrtc/user@domain
:同上。配置文件名字为xcc-rtc.conf.xml
,大部分配置与mod_verto模块的配置一致。
debug
类型 integer
调试级别profile
属性 name
为profile的名称参数(xcc独有的参数):
xrtc-ctrl-subject
类型 string
XCtrl的nats通信topic,使用这个profile的verto消息都会发送到这个topicmedia-timeout
类型 integer
媒体超时,单位为毫秒。 当通话时超过设定时间没有媒体通信的话freeswitch会将通话挂断客户端建议还是使用现有的verto.js
协议,稍加改动(往来消息中增加params.node_uuid
)。
如下代码,系统支持以下连接鉴权方式:
login/passwd
参数xui_sessid
,具体获取方法参见XUI相关说明xcc_token
参数传入。具体获取方法参见XCC系统相关说明.connect({
vertologin: username + "@" + domain,
passwd: password, // optional, when use xui_sessid or xcc_token
socketUrl: socketUrl,
loginParams: {
xui_sessid: xuiToken, // optional
xcc_token: xccToken, // optional
,
}; })
Verto会议,对于同一个会议,统一路由到一个Node节点上。
第一个成员呼入时,Node将发送一个Event Channel消息,ConfMan收到后,会订阅相关的事件。需要保证所有后续消息都路由到同一个Node节点上。
消息如下:
confman-liveArray.3000-xswitch.cn@xswitch.cn
:与会成员列表confman-chat.3000-xswitch.cn@xswitch.cn
:Chat Channelconfman-mod.3000-xswitch.cn@xswitch.cn
:Moderator Channel,可以对会议进行控制如果后续有人加入会议,则向Node发送bootstrap
消息,Node收到后回复liveArray,同步所有已经参会的与会者列表。
XSwitch支持与腾讯TRTC互联互通。
TRTC需要以下参数:
app_id
:TRTC SDK App ID,字符串,不同AppID之间的数据不互通room_id
:房间ID,字符串,加入同一个room的成员可以媒体互通user_id
:用户ID,字符串,TRTC不支持同一个UserID (除非 SDKAppID 不同)在两个设备同时使用user_sig
:动态签名,详见 https://github.com/tencentyun/tls-sig-api 下面的README.mdTRTC在XSwitch内是一个Endpoint,与SIP类似,呼叫字符串格式为:
{trtc_user_id=$user_id,trtc_user_sig=$user_sig}/trtc/$app_id/$room/$dest_number
room
room
,在XSwitch中生成一个Channel有以下两种方式:
{
"jsonrpc": "2.0",
"id": "0",
"method": "XNode.NativeAPI",
"params": {
"ctrl_uuid": "ctrl_uuid",
"cmd": "trtc",
"args": "call $app_id $user_id $user_sig $dest_number $cid_name $cid_number"
}
}
其中:
app_id
:TRTC App ID,必选user_id
:TRTC 用户 ID,必选user_sig
:TRTC签名,必选dest_nubmer
:被叫号码,必选cid_name
:主叫名称,可选cid_number
:主叫号码,可选该方法使用Native API发起呼叫,会到系统Dialplan中查找$dest_number
,具体的Context可以在模块配置文件中配置。
此外,也可以直接使用Dial发起外呼(其中$
相关的变量在真正使用时要以实际的值代替):
{
"jsonrpc": "2.0",
"id": "0",
"method": "XNode.Dial",
"params": {
"ctrl_uuid": "ctrl_uuid",
"destination": {
"global_params": {
},
"call_params": [{
"uuid": "uuid",
"dial_string": "trtc/$app_id/$room/$dest_number",
"params": {
"trtc_user_id": "$user_id",
"trtc_user_sig": "$user_sig"
}
}]
}
}
}
其中:
dest_nubmer
:被叫号码,如果为auto_answer
,则channel会自动应答呼通后,TRTC Channel将会自动接听,然后就可以调用XNode.Bridge
去Bridge SIP侧的Channel了。
上述两种方式都需要先将TRTC SDK加入room
,如果想省钱,可以先呼叫SIP,等SIP接通后再加入TRTC room,如果SIP侧呼不通,就无需加入TRTC room
。呼叫流程如下:
trtc
App加入TRTC room
,或通过Bridge加入TRTC room
room
SIP呼叫TRTC也有两种方式,当SIP Channel到来后,可以先Accept,然后Bridge到trtc/$app_id/$room/$dest_number
即可,注意需要有trtc_user_id
和trtc_user_sig
参数。
另一种方式是直接调用一个NativeApp
,如:
{
"jsonrpc": "2.0",
"id": "0",
"method": "XNode.NativeApp",
"params": {
"ctrl_uuid": "ctrl_uuid",
"cmd": "trtc",
"args": "$appid $room $user_id $user_sig"
}
}
注意:不管从哪个方向呼,TRTC只有媒体层的协议支持,没有信令,因此,具体的信令由开发者自行实现,如:
room
(具体先后顺序可以自行决定)。暂未实现。
在会议模式下,Linux SDK要以主播模式进入会议,FreeSWITCH侧如果有多个用户,则先在FreeSWITCH侧混流,然后以一路流的方式跟TRTC Cloud交互。
由于客户端会有多个主播加入,所以收到的多路流需要适当混流。
参见:https://cloud.tencent.com/document/product/647/35429
XSwitch支持与Agora互联互通。
Agora需要以下参数:
appid
: Agora App ID,字符串,同一个ID下的媒体才有可能互通token
: App ID泄漏后后果严重,因此使用Token来代替App IDchannel
: Agora Channel(频道),字符串,加入同一个Channel中的所有成员都可以媒体互通Agora在XSwitch内也是一个Endpoint,与SIP类似,呼叫字符串格式为:agora/$appid-or-token/$channel/$dest_number
。
目前仅支持一个SIP Channel加入一个Agora Channel,不支持多个SIP端加入同一个Agora Channel(会议模式)。
在XSwitch中,也有Channel概念,一个Channel代表一个通道,也就是一路通话,与Agora概念区分,我们把XSwitch中的Channel叫通道,Agora中的Channel叫频道。
appid/channel
appid/channel
,在XSwitch中生成一个Channel(注意,与Agora Channel字符串不同)有以下两种方式:
{
"jsonrpc": "2.0",
"id": "0",
"method": "XNode.NativeAPI",
"params": {
"ctrl_uuid": "ctrl_uuid",
"cmd": "agora",
"args": "call $key $channel $dest_number $cid_name $cid_number"
}
}
其中:
key
:Agora App ID或Token,必选channel
:Agora Channel,必选dest_nubmer
:被叫号码,必选cid_name
:主叫名称,可选cid_number
:主叫号码,可选该方法使用Native API发起呼叫,会到系统Dialplan中查找$dest_number
,具体的Context可以在模块配置文件中配置。
此外,也可以直接使用Dial发起外呼:
{
"jsonrpc": "2.0",
"id": "0",
"method": "XNode.Dial",
"params": {
"ctrl_uuid": "ctrl_uuid",
"destination": {
"global_params": {
},
"call_params": [{
"uuid": "uuid",
"dial_string": "agora/$key/$channel/$dest_number",
}]
}
}
}
其中:
dest_nubmer
:被叫号码,如果指定为auto_answer
,则Agora channel会自动应答呼通后,Agora Channel将会自动接听,然后就可以调用XNode.Bridge
去Bridge SIP侧的Channel了。
上述两种方式都需要先将Agora SDK加入Channel,如果想省钱,可以先呼叫SIP,等SIP接通后再加入Agora Channel,如果SIP侧呼不通,就无需加入Agora Channel。呼叫流程如下:
agora
App加入Agora Channel,或通过Bridge加入Agora ChannelSIP呼叫Agora也有两种方式,当SIP Channel到来后,可以先Accept,然后Bridge到agora/$key/$channel/$dest_number
即可。
另一种方式是直接调用一个NativeApp
,如:
{
"jsonrpc": "2.0",
"id": "0",
"method": "XNode.NativeApp",
"params": {
"ctrl_uuid": "ctrl_uuid",
"cmd": "agora",
"args": "$key $channel"
}
}
注意:不管从哪个方向呼,Agora只有媒体层的协议支持,没有信令,因此,具体的信令由开发者自行实现,如:
appid/channel
(具体先后顺序可以自行决定)。agora
App在XSwitch中,一个Channel指一路通话,典型地,SIP呼Agora以及Agora呼SIP都有两个Channel组成,一个SIP Channel和一个Agora Channel,它们之间使用Bridge进行桥接,组成一个桥接的呼叫。
在XSwitch中,SIP与Agora通信的另一种实现方式是使用agora
App,该App会执行join
操作调用Agora Linux SDK加入一个Agora频道,但不会产生一个Agora通道,在XSwitch中,它是一个单腿呼叫。
当有多个SIP Channel需要加入Agora Channel时,就形成一个会议,可以使用agora
Application实现。
agora
Application有一个全局的Agora Channel引用计数,多个SIP Channel可以加入同一个Agora Channel(增加一个uid
)。
具体示例可以参考xswitch/xcc-examples
中的例子。
通过微信小程序呼叫
Todo.
XSwitch XCC API功能强大,使用起来非常灵活,可以很简单的使用任何语言写出一些Demo IVR。但是,XCC API是基于RPC调用的,不管API设计多完善,由于网络和系统的复杂性,写出一个生产级别、高可靠的系统也不是一件容易的事。本章,我们就来看一下在面向生产环境开发应用程序时应该注意的问题。
简单的回答是:使用你(以及你的团队)最擅长的语言。
XCC API通过NATS承载,NATS有各种语言的客户端库,因此,你几乎肯定可以找到你使用的语言的客户端库。
NATS支持同步调用和异步调用,XCC也是。NATS的API设计的很好,因此我们没有在NATS API上又包装一层XCC SDK,主要是因为不是太有必要,而且,我们不一定能包装好。如果只是进行简单封装,那么,你无法用到所有的参数和特性;如果彻底封装、精心检查每一个参数和返回值,那么,最终就会是包装的特别臃肿,而你的应用层还是免不了检查所有可能的返回值以打印特定的日志或者进行对应的处理。比如我们使用Play
播放一个媒体文件,最简单的用法只有两个参数:当前Channel的UUID以及待播放文件的路径,但是当你播放TTS时,就需要传入更多的参数。哪些参数(比如TTS引擎)合法与不合法,可能只有到了XSwitch侧才能知道,或者在应用层(通过事先配置)知道,而处于中间Play
函数却不容易知道。另外,对于Play的返回值,在XSwitch侧,通常有以下情况:
但在控制侧(你的应用程序侧),却又多了很多情况,如网络中断、消息超时、XSwitch过载或崩溃、NATS过载丢消息或崩溃等。当发生这些情况时,你的应用就会异常,你必须小心地检查各种异常以便清除缓存、打印相关的日志用于问题追踪等。当然,你写的程序也会崩溃,如果很不幸,你的程序崩溃了,那么,失去控制的XSwitch何去何从、你的程序恢复后又怎么收拾前面的烂摊子,也是需要考虑的问题。
所幸,NATS已经帮我们Cover了很多问题,NATS各种语言实现的客户端也都尽量发挥了相关的语言的特性,并能提供一致的调用逻辑。
因此,我们并没有再在NATS基础上包装一层客户端SDK。除了上述原因,还有就是我们并不是对所有语言都非常擅长,而且,不同的用户有不同的应用场景,即使对同一种语言也有不同的偏好,不同的调用习惯,因此,我们决定把这些自由留给开发者。
当然,我们本身也是开发者,在使用XCC API的过程中也逐渐形成了自己的Go语言SDK,我们也开源出来供大家参考,这些SDK我们会一直维护,因而大家也可以直接使用,并欢迎提出各种改进建议。关于该SDK会再后文提到。
不管是本地函数调用还是RPC,同步调用都会比较简单。但是RPC由于需要经过网络传输的特殊性,实际上所有的RPC都是异步的,客户端发出一个消息,服务端收到并进行处理,然后将内容返回,客户端再根据服务端返回的数据返回给调用者。
NATS使用Reqeust机制做同步调用。同步订阅一个一次性的Subject,在发送请求的同时,带上这个Subject,对方在处理完后就可以直接将消息回复到这个Subject上,客户端收到回复后调用完成,在此之前,客户端就一直阻塞等待。如图:
同步调用同阻塞的,因而需要超时机制,一般在Request请求中都有一个timeout
参数用于超时。在C语言中,是使用pthread/condition
同步机制实现的,它会阻塞掉整个线程。还是以Play函数为例,为了方便调用者使用,它是阻塞的,只能等文件播放完成后才能返回。如果文件的长度是20秒,则必须保证timeout
大于20才行,这样才能防止调用超时。如果对方提前挂机或Play被API人为中断,则该函数也会提前返回,状态码为410
(Gone,表示Channel主体已不存在)或206
(表示文件只播放了一部分)。
timeout
在满足播放条件的情况下肯定是越短越好。为了能更精确的使用timeout
,需要提前知道文件的播放长度。在播放时长不可预知的情况下,只能使用经验参数,保证不能太长,又不能太短。但无论如何,正常情况下,该函数或者返回服务端的结果,或者超时。
在网络发生问题、过载等导致消息丢失的情况下,发出的请求无法得到响应,调用者就会一直阻塞直到超时。这在timeout
比较长的情况下,可能会导致过多的资源占用。所以使用同步机制要求网络比较可靠,XSwitch和NATS也不要过载(导致丢消息)。
同步调用的好处是可以很方便的写连续控制的代码,下面是一个调用方法的伪代码:
Play(file1)
Play(file2)
Play(file2)
但是,对方可能会提前挂机,Play
也可能出错,所以,在真正应用中,都需要检查返回值:
code, message = Play(file1)
if code != 200
// 打印日志,错误处理代码 ...
return
end
code, message = Play(file2)
...
由于同步调用是阻塞的,一般来说,都需要在独立的线程中调用。线程在不同的语言中有不同的表现方式,比如,在C语言中是线程,在Go语言中是协程(Goroutine),而在Javascript中,所有调用都是异步非阻塞的。
值一得的是,Javascript的NATS客户端库也提供了Request和Publish两种调用方法,区别是前者可以指定超时时间,并可以在收到结果后调用回调函数。
异步调用使用起来更复杂一些,但对于调用者有更大的灵活性。异步调用通过使用NATS的Publish方法发送请求,Publish会立即返回而不等待执行结果,因此,要想获得结果,就必须订阅一个Subject用于接收返回值。同时。为了知道请求是哪个函数发出的,还需要“记住”请求的ID,如:
nats.sub('cn.xswitch.ctrl.ivr', onMessage) // 收到消息会执行onMessage回调函数
request_id = 0
requests = {} // 记录所有请求
function onMessage(msg) {
if (msg.method == 'Event.Channel') { // Channel 事件
if (state == 'START') { // 来话的第一个消息
request_id++ // 请求id
requests[request_id] = 'play file1'
Publish(Play, request_id, file1) // 发送请求,播放file1
} else if (state == 'DESTROY') { // 挂机了
delete(requests[request_id]) // 清理现场
}
} else if msg.result { 结果
state = requests[msg.result.id] // 哪个请求的返回结果
if (state == 'play file1') {
// file1 播放完成
request_id++ // 请求id
requests[request_id] = 'play file2'
Publish(Play, request_id, file2) // 发送请求,播放file2
} else if (state == 'play file2') {
// file2播放完成...
}
}
}
从上述伪代码中可以看出,异步执行需要异步的等待执行结果,然后进行下一步。为了能匹配请求和返回值,还需要将请求id
存起来。当然,挂机后需要清除缓存中的内容,或者,需要实现一种垃圾回收机制(以面对各种异步情况,如收不到DESTROY
消息的情况,严谨的网络编程会认为网络永远会有不可靠的情况的)。其实,同步调用也是这么实现的,只不过将这些复杂性隐藏到了阻塞等待之后。
XSwitch会发出事件通知,在JSON-RPC中,事件就是一个不带id
的请求,且不需要回复。
在控制侧的事件接收通常是在独立的线程中执行的。控制侧可以根据接收到的事件执行一些控制,如收到语音识别的内容后打断放音操作。注意在收到事件后,不要阻塞当前的事件接收进程(比如不要执行阻塞的Play调用)。
在实际应用中,可以使用独立的线程或协程处理每一路通话,收到的事件也可以推到当前线程中处理,也可以在专门的线程中处理。在多线程(或协程)编程中,不可避免的要考虑对同一资源的竞争性访问,这一般可以通过Mutex或其它机制实现,具体的实现因程序语言而异(在Javascript中只有一个线程,因而不需要考虑这种情况)。
XSwitch有原生的事件,通常字段比较多,不建议使用。如果有可能,就建议使用Event.Channel
事件。
Event.Channel
Channel事件,在Channel状态发生变化时发出。参见第节Channel State。
Channel事件会发送到被Accept
或Answer
接管的Controller上。
Event.CDR
CDR事件,在通话完毕后发出。每一个Channel都会有一个CDR事件,如果参与通话的是两条腿(aleg
、bleg
),则会有两个CDR事件,并分别有leg
标志。CDR事件一般会在Event.Destroy
之前发出。
CDR事件默认会送到与Event.Channel
相同的Subject上,但也可以通过全局配置参数cdr-subject
配置单独的Subject。
参见第节CDR相关说明。
在实际应用中,通常可以根据情况使用同步和异步结合使用。
一般来说,简单的应用不需要对Channel信息进行缓存。todo
如果需要缓存,
Protobuf是Google推出的消息序列化格式,配合gRPC使用更能发挥使用。XCC使用JSON而不是并没有使用Protobuf进行对象的序列化。这主要是因为Protobuf相对来说更重一些,而且,XCC传输的消息内容大部分是字符串,Protobuf的优势不是特别明显。另外,gPRC更重,且C语言没有官方的实现,C++语言的实现在XSwitch中表现不是很好(reload
模块会有问题,一些网络资源无法释放干净),因此我们在XCC API中没有使用gRPC和Protobuf。
不过,XCC API有一个Protobuf描述(xctrl.proto
,参见xswitch/xctrl
),这主要是因为很多语言(如Go或Java)可以通过它转换成目标语言的对象,并序列化成JSON。该xctrl.proto
是会一直维护的,可以在项目中使用。
XCtrl Proto参见:
XCC使用JSON-RPC消息承载。各种语言都有JSON-RPC的实现,但是,它们通常都耦合了传输层相关的代码实现(如HTTP),比较重。如果你需要自己实现JSON-RPC的消息序列化和反序列化,本节给出一些语言的参考。
在Javascript语言中,JSON-RPC消息的序列化和反序列化都很简单,因为它几乎可以跟Javascript的对象一对一的转换,相当于Javascript中的一等公民,因而,无需特殊的处理。
值得一提的是,我们提倡在序列化JSON时使用“Pretty”格式,也就是有正常的缩进和换行,这样便于阅读和调试,对字节数的增加引起的影响也可以忽略不计。如果你特别在意在生产环境中的效率,那么可以使用条件编译技术仅在生产环境中使用“紧凑”格式。在Javascript中使用“Pretty”格式的将对象序列化的方法如下:
var str = JSON.stringify(obj, null, 2); // spacing level = 2
反序列化也很简单:
var rpc = JSON.parse(str);
在Go语言中,习惯将JSON与Go语言的结构体相对应。由于JSON-RPC只是一个“信封”,在收到JSON-RPC消息时并不知道里面的内容应该对应哪个对象,因而一般需要两步或多步解析。以Event.Channel
消息为例:
{
"jsonrpc": "2.0",
"method": "Event.Channel",
"params": {
"state": "START",
"...": "..."
}
}
当收到上述消息时,我们先解析“信封”,因为有method
,所以是一个请求,没有id
,说明是一个事件请求,不需要回复。根据method
才知道params
的结构,可以将params
中的内容反序列化成ChannelEvent
结构体对象。
import (
"encoding/json"
)
type RPC struct {
string `json:"jsonrpc"`
Version `json:"id"`
ID *json.RawMessage string `json:"method"`
Method `json:"params"`
Params *json.RawMessage `json:"result,omitempty"`
Result *json.RawMessage `json:"error,omitempty"`
Error *json.RawMessage
}
type ChannelEvent struct {
string `json:"node_uuid"`
NodeUUID string `json:"uuid"`
UUID string `json:"state"`
State string `json:"cid_name"`
CidName string `json:"cid_number"`
CidNumber string `json:"dest_number"`
DestNumber
}
var rpc RPC // 定义`rpc`变量为RPC类型
// 反序列化为RPC对象
err := json.Unmarshal(msg.Data, &rpc) if err != nil { // 错误处理
}switch rpc.Method {
case "Event.Channel": // 根据`method`决定使用什么对象反序列化
{var channelEvent ChannelEvent
err := json.Unmarshal(*(rpc.Params), &channelEvent)
} }
在上述代码中,我们定义RPC
为一个宽泛的类型,即可以接收JSON-RPC请求消息,也可以接收响应消息,它的的Params
为json.RawMessage
类型,因而它会保存原来JSON的内容而不深度解析,直到我们根据method
知道params
的类型后再进行解析。
下列代码是Ctrl侧发送应答请求的序列化代码示例:
type RPCRequest struct {
string `json:"jsonrpc"`
Version string `json:"id"`
ID string `json:"method"`
Method interface{} `json:"params"`
Params
}
type AnswerParams struct {
string `json:"ctrl_uuid"`
CtrlUUID string `json:"uuid"`
UUID
}
rpc := RPCRequest{"2.0",
Version: "1",
ID: "XNode.Answer",
Method:
Params: AnswerParams{"ctrl_uuid",
CtrlUUID: "uuid",
UUID:
},
}
bytes, err := json.Marshal(rpc)
在上述代码中,RPCRequest
比RPC
更具体,它只是一个请求,同时,它也比较宽泛,使用interface{}
可以接受任何类型的参数,因而在构造请求结构体时我们可以传入AnswerParams
类型的对象,序列化后的对象类型是byte[]
,可以直接通过网络函数发送,也可以转换成字符串打印输出。
由于Go语言允许在结构体定义时加入“json:
”相关的注释,因而序列化后的字段名称可以根据情况指定。
除此之外,Go语言对Protobuf的支持非常完善,我们也提供xctrl.proto
转换生成的Go语言对象和函数,这样就不需要自定义各种函数的结构体,使用起来就方便些。
当然,前面也提到,我们也有更深度的包装,做成了Go语言SDK,更方便使用,但也有更多的规矩和约束,这些约束适用于我们的代码架构,供大家参考。这些SDK会一直维护,如果也适合你使用,也可以直接拿来用。
Java中有json-simple
和gson
等可以直接序列化和反序列化JSON。与上一节中Go语言示例中对应的Java示例代码如下:
import org.json.simple.JSONObject;
import org.json.simple.parser.JSONParser;
String rpc = new String(json_request_string, StandardCharsets.UTF_8);
parse(rpc); // 将JSON字符串解析成Java对象
JSONObject m = (JSONObject) parser.String method = (String) m.get("method");
get("params");
JSONObject params = (JSONObject)m.String state = (String) params.get("state");
if (method != null && method.equals("Event.Channel") &&
null && state.equals("START")) { // 新来话事件
state != String node_uuid = (String) params.get("node_uuid");
// 构造应答请求
new JSONObject();
JSONObject rpc = put("jsonrpc", "2.0");
rpc.put("id", "test-id");
rpc.put("method", "XNode.Answer");
rpc.new JSONObject();
JSONObject p = put("uuid", (String) params.get("uuid"));
p.put("ctrl_uuid", "ctrl_uuid");
p.put("params", p);
rpc.StringWriter request = new StringWriter();
writeJSONString(request);
rpc.System.out.println(request);
// 通过NATS发送请求 ...
}
从上述代码可以看出,直接操作JSON通用对象的代码也不复杂,但与序列化成具体的对象类相比,显得不太直观,而且如果JSON中的字符比较多的情况下,代码就比较冗长了。
Java中不支持类似Go语言中的json.RawMessage
和interface{}
机制,因而使用起来要复杂些。gson
支持对象的序列化和反序列化,可以配合使用。xctrl.proto
也可以直接生成Java类(Xctrl.java
),只是Protobuf对JSON的支持没有对Protobuf原生协议支持的好。下面是使用gson
和Protobuf的示例:
import com.google.protobuf.util.JsonFormat;
import com.google.protobuf.*;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
import xctrl.Xctrl.AcceptRequest;
import xctrl.Xctrl.Request;
import xctrl.Xctrl.PlayRequest;
import xctrl.Xctrl.Media;
import xctrl.Xctrl.ChannelEvent;
import xctrl.Rpc.*;
private static RPCRequest.Builder rpc(String method, String id) {
return RPCRequest.newBuilder()
setJsonrpc("2.0")
.setMethod(method)
.setId(id);
.
}
// 请求字符串
String event = new String(json_request_string, StandardCharsets.UTF_8);
System.out.println(event);
parse(event); // 将请求字符串解析成`gson`对象
JsonElement e = parser.getAsJsonObject();// 找到根对象
JsonObject root = e.String method = root.get("method").getAsString(); // 找到`method`
get("params").getAsJsonObject(); // 找到`params`
JsonObject params = root.if (method.equals("Event.Channel")) {
// 将params重新变成string,注意这一点与Go语言中不同
// gson中应该有方法直接将gson对象直接转换成Protobuf对像,但未找到怎么用
String sparams = params.toString();
Builder cevent = ChannelEvent.newBuilder();
ChannelEvent.// 反序列化成`ChannelEvent`类
parser().ignoringUnknownFields().merge(sparams, cevent);
JsonFormat.if (cevent.getState().equals("START")) { // 来话请求
// 下列代码应试在新线程中执行(因为是阻塞的),但简单起见,我们直接写在这里
Printer printer = JsonFormat.printer().preservingProtoFieldNames();
JsonFormat.String node_uuid = cevent.getNodeUuid();
newBuilder()
AcceptRequest accept = AcceptRequest.setUuid(params.get("uuid").getAsString())
.setCtrlUuid("ctrl_uuid")
.build();
.
rpc("XNode.Accept", "0") // 创建RPC请求
RPCRequest xrpc = setParams(Any.pack(accept)) // params被定义为Protobuf的Any类型
.build();
.
// Any类型必须提供一个TypeRegistry才能正常序列化,它会生成一个额外的`@type`字段
newBuilder()
TypeRegistry registry = TypeRegistry.add(accept.getDescriptorForType()).build();
.String rpc_accept = printer.usingTypeRegistry(registry).print(xrpc);
System.out.println(rpc_accept);
// 通过NATS发送请求 ...
} }
在上述示例中,我们通过JsonFormat
将对像序列化,它是Protobuf中标准的序列化方法,但对于Any
类型的请求会生成额外的@type
字段,理论上对方通过@type
能正确的反序列化。但XSwitch不支持@type
,因而生成的消息无法在Ctrl侧用Java正确的反序列化,因此上述代码中我们仍然使用了gson
。
我们相信Java语言如此成熟且应用广泛,一定有更好的方法。
使用XCC开发时,会遇到好多Subject、ID及UUID,虽然我们在本文档中给出了解释,但是初学者还是容易混,因此,在此我们再详细解释一下。
id
RPC中的id
是JSON-RPC要求的,它代表一个请求。XSwitch要求该id
必须是一个字符串类型。XSwitch本身不关心该ID,会在Response中原样返回。所以,该id
可以是任意字符串。但是,为了便于跟踪消息和排错,建议使用真正的UUID字符串,每个请求一个,保证唯一。
对于NATS,如果使用异步消息(Publish而不是Request),则需要根据JSON-RPC的语义匹配返回结果。
每个XSwitch媒体节点又称为一个Node,因此有一个唯一的Node UUID,在事件消息中写为node_uuid
。该node_uuid
必须加上cn.xswitch.node
前缀,才是媒体节点订阅的Subject。一般来说,根据业务属性,媒体节点也会以队列方式订阅一些其它的业务Subject,如cn.xswitch.node.ivr
、cn.xswitch.node.test
等,这样在有很多媒体节点的情况下便于按业务分组。
与Node UUID类似,每一个Controller都需要有一个唯一的Ctrl UUID,用于与Node UUID建立虚拟对应关系。Ctrl UUID在JSON消息中以ctrl_uuid
表示,每个Controller对需要加上cn.xswitch.ctrl.
前缀订阅自己的主题,如:cn.xswitch.ctrl.f76f6931-38c1-123b-4786-0242ac120003
。
多个Ctrl可以以Queue订阅的方式订阅一个业务Subject,如cn.xswitch.ctrl
、或cn.xswitch.ctrl.ivr
等,多个Ctrl之间会竞争的接收发到这个Subject上的消息。
在一些高级应用中,同一个Ctroller也可以订阅很多UUID。如,在批量外呼应用中,可以对每路通话产生一个Channel UUID,进而订阅cn.xswitch.ctrl.$channel_uuid
,当呼叫完成后取消订阅。
每个呼叫(Channel)都有一个UUID,唯一表示这路呼叫。对于来话,Channel UUID是由XNode产生的。对于去话,Channel UUID可以由Controller产生,或者由XNode产生。
XNode侧的Subject必须以cn.xswitch.node
前缀开头。对于从Controller侧发起的外呼或者命令(如status
命令)而言,XNode属于服务的一方,Controller属于客户端,因而Controller会向cn.xswitch.node
发Request。
Controller侧的Subject必须以cn.xswitch.ctrl
前缀开头。多个Controller可以以Queue的方式竞争订阅cn.xswitch.ctrl
主题。对于从XNode侧的来话而言,XNode是客户端,XCtrl是服务器。但是,在XCtrl接收到来话消息以后,它要命令XNode做事情,如Answer
、Play
等,则XCtrl又是客户端,而XNode是服务器,此时所有命令应该发到cn.xswitch.node.$node_uuid
上。
请参考 https://git.xswitch.cn/xswitch/xcc-examples 中的相关示例,示例大部分以Node.js(Javascript)语言提供,因为Node.js可以比较方便的描述JSON,做前后端的也都熟悉。
示例中也有Go、Java等语言的参考,其中的README也能提供更多的信息。
Q:XSwitch是否可以支持单机版使用?
A:带XUI的XSwitch本身设计是简单易用,支持单机版单租户
Q:XSwitch是否支持集群?
A:支持,只是带XUI的版本在界面上有些限制。
Q:XSwitch是否支持Lua?
A:XSWitch支持原生FreeSWITCH支持的所有能力,包括Lua。事实上,XSwitch的后台的Rest API全部使用Lua开发的。
Q:XSwitch是否支持ESL?
A:XSWitch支持原生FreeSWITCH支持的所有能力,包括ESL。
Q:XSwitch底层使用了XML动态绑定,是否支持还其它的绑定?
A:XSwitch同时支持多种XML绑定,但是顺序不好控制,必要时,可以禁用XSwitch内置的绑定。
Q:在使用XCC API进行开发时,最好使用哪种语言?
A:我们推荐使用Go语言,但实际上你应该使用你最擅长的语言,详见第节我该使用什么语言开发。
Q:为什么不提供Java SDK?
A:每个人的应用场景和调用习惯不同,我们相信直接使用NATS接口能最大限度的给你自由,详见第节我该使用什么语言开发。。
FreeSWITCH原生的呼叫字符串。
注册到FreeSWITCH,FreeSWITCH可以直接呼叫。
user/分机号
直接通过IP白名单方式对接。对方需要对FreeSWITCH开通IP白名单。具体的IP和端口号跟FreeSWITCH侧的SIP配置有关。
sofia/public/号码@ip:port
如果使用TCP或TLS链路,可以添加参数
sofia/public/号码@ip:port;transport=tcp
在FreeSWITCH侧可以配置一个网关,网关可以以注册或非注册方式与对端对接。对端需要提供至少以下信息:
realm
: 域名或IP地址username
: 用户名password
: 密码具体网关名称需要在FreeSWITCH侧配置。
格式:
sofia/gateway/网关名称、被叫号码
user/1000
呼叫本地用户user/1001
sofia/public/1860535xxxx@192.168.1.1
默认端口为5060
sofia/public/1860535xxxx@192.168.1.1:2345
指定端口sofia/public/1860535xxxx@192.168.1.1:2345;transport=tcp
TCPsofia/public/1860535xxxx@192.168.1.1:2345;transport=tls
TLSTTS和ASR是人工智能必要的组成部分。XCC集成了市面上大部分的公有云接口。
engine
:引擎名称。跟实现有关,目前可用的有ali
、huawei
、baidu
、ifly
等。
voice
:嗓音,跟具体引擎有关,可传入default
,或查找相关引擎提供的嗓音值。
阿里Voice相关配置:https://help.aliyun.com/document_detail/84435.html
华为相关参数配置:https://support.huaweicloud.com/api-sis/sis_03_0111.html
ASR检测一个很关键的部分是VAD(Voice Activity Detection),即首先要检测是否有人讲话,再去识别内容。以下图为例,横向为时间轴t
,纵向为语音波形。其中thresh
为语音检测阈值,即小于该值认为没有声音。该值在公有云连续识别时一般不用设置。如果使用XSwitch原生的VAD,则需要设置。
_-_ .. ~~.
thresh _-/ \_-^-_/ __ / \ thresh
---------/ \ / \ ----------------------
/ \__/ \
---------------------------|----------|----------|---------> t
no-input| | | |
|~~| | | |
voice-ms_/ |Begin Speaking | | |
|Speaking ... Partial ... Partial ... |End Speaking
| | |silence-ms|silence-ms|End Speaking
| |
| | speech-timeout |End Speaking
no-input-timeout
内用户一直没有讲话,则会返回no-input
。voice-ms
时间检测到语音活动(~~
波浪线部分),则为认讲话开始。silence-ms
计数,超时后认为讲话结束。如果在超时前又检测到强度回升,则继续检测。silence-ms
时,认为讲话结束,返回最终结果。默认情况下,ASR返回结果会“打断”当前的放音(如果还没有播放完的话),nobreak
参数可以控制是否打断。
有时候,用户程序希望能根据识别的结果进行条件打断,可以使用offset
参数实现。识别结果中返回的offset
为当前播放的文件被打断的位置。下一次调用时使用该offset
可以从该offset
位置开始播放。目前仅对文件类型的播放有效,尚不支持TTS。
以下参数在所有引擎中通用。注意,这些是引擎原始参数,值为字符串类型,如在DetectSpeech接口中应该放到params
中。
no-input-timeout
:毫秒,未检测到语音超时,通常为2000-10000
。speech-timeout
:毫秒,讲话超时。从检测到讲话开始算,或者从放完音开始算。通常大于2000
,小于60000
。voice-ms
:毫秒,连续检测到语音时长,通常为20-100
。silence-ms
:毫秒,尾部静音时长,通常为800-2000
。engine
:引擎名称partial
:是否产生中间结果add-punct
:是否加标点,字符串true
或false
。vad-mode
:VAD模式,VENDOR
、NATIVE
、0 ~ 3
等。ali
相关参数segment-recording-prefix
:分段录音前缀(路径),如果不设置则禁用分段录音。disable-engine-data
:不发送ASR引擎返回的原始数据。huawei
相关参数文档:https://support.huaweicloud.com/api-sis/sis_03_0111.html
原生FreeSWITCH实现的接口中,每个检测都需要起单独的线程,而且通过Event方式传递文本的消息,不利于控制。另外,原生的FreeSWITCH使用轮循、忙等待的方式检查有没有识别结果,效率不高,状态机特别复杂。
XCC重新实现了语言检测接口,基本上还是使用原来的模块接口,但不再需要忙等待方式提供检测结果,而是直接将检测到的结果推到当前Session的消息处理队列中,实现更高效。
新接口接受JSON格式的参数输入,比FreeSWITCH原生接口更清晰、方便。
XCC接口与原来的接口兼容,由于历史原因,在mod_huawei
和mod_ali
中还保留了FreeSWITCH原生接口的状态机。新的模块中无需再实现这一复杂的状态机。有利于简化代码,提高效率。
在使用Play播放文件时,data
参数支持如下文件类型:
/tmp/test.wav
。http://example.com/test.wav
,支持http
和https
。silence_stream://3000
,其中3000为表单的毫秒数。tone_stream://%(1000,4000,450)
为中国回铃音。详情参见: https://freeswitch.org/confluence/display/FREESWITCH/TGML!
分隔(文件名中不能有!
),如file_string:///tmp/1.wav!/tmp/2.wav
系统支持以下文件类型:
.wav
.mp3
.mp4
.mkv
.m4a
.mov
.webm
支持以下协议:
http
rtmp
rtmps
rtsp
可以把PNG文件当成视频文件用。如:/tmp/test.png
。文件路径前可以加参数,如{png_ms=10000}/tmp/test.png
表示播放10秒。支持的参数列表如下:
png_ms
:时长,毫秒audio_file
:音频文件路径png_fps
:帧率,默认为5
alpha
:是否支持Alpha通道text
:文本,可以以TTS方式播放,但需要提供下列参数tts_engine
:TTS引擎tts_voice
:TTS噪音除PNG外,还支持以下扩展名的文件:
*.jpg
*.jpeg
*.bmp
话单字段 | 中文说明 |
---|---|
caller_id_name | 主叫名称 |
start_stamp | 开始时间 |
billsec | 计费时长 |
account_code | 计费号码 |
network_addr | 网络地址 |
abs_path | 录音文件绝对路径 |
answer_stamp | 应答时间 |
sip_hangup_disposition | SIP相关 |
context | 呼叫源 |
duration | 总时长 |
end_stamp | 结束时间 |
uuid | 通话UUID |
bleg_uuid | B腿UUID |
direction | 方向,inbound为向内,outbound向外 |
destination_number | 被叫号码 |
network_port | 网络端口 |
realm | 域 |
caller_id_number | 主叫号码 |
hangup_cause | 挂机原因 |
挂机原因 | 中文说明 |
---|---|
ORIGINATOR_CANCEL | 主叫挂机 |
NORMAL_CLEARING | 正常释放 |
NORMAL_TEMPORARY_FAILURE | 临时故障 |
WRONG_CALL_STATE | 呼叫状态异常 |
USER_BUSY | 用户忙 |
LOSE_RACE | 别处应答 |
MEDIA_TIMEOUT | 媒体超时 |
CALL_REJECTED | 拒绝呼叫 |
UNALLOCATED_NUMBER | 空号 |
NO_ROUTE_DESTINATION | 无法路由 |
NO_USER_RESPONSE | 久叫不应 |
NO_ANSWER | 无应答 |
NORMAL_UNSPECIFIED | 未定义 |
NETWORK_OUT_OF_ORDER | 网络异常 |
RECOVERY_ON_TIMER_EXPIRE | 呼叫超时 |
USER_NOT_REGISTERED | 用户未注册 |
SUBSCRIBER_ABSENT | 用户缺席 |
GATEWAY_DOWN | 网关故障 |
MANDATORY_IE_MISSING | SIP消息不全 |
SYSTEM_SHUTDOWN | 系统关机 |
INCOMPATIBLE_DESTINATION | 目的地不兼容 |
EXCHANGE_ROUTING_ERROR | 交换路由错误 |
MANAGER_REQUEST | 强制挂机 |
DESTINATION_OUT_OF_ORDER | 目的地异常 |
SERVICE_NOT_IMPLEMENTED | 服务未实现 |
参见 https://nats.io/ 。↩︎
ANI II (OLI - Originating Line Information),参见:http://www.nanpa.com/number_resource_info/ani_ii_digits.html↩︎
https://freeswitch.org/confluence/display/FREESWITCH/Hangup+Cause+Code+Table↩︎