XRTC开发文档
Verto协议
Verto协议
本文描述Verto的底层协议,大多数情况下你不需要使用它,仅供参考。
Verto协议基于JSON-RPC实现。关于JSON RPC协议规范,参见:http://wiki.geekdream.com/Specification/json-rpc_2.0.html
RPC中,每次请求的id
应该是递增的,客户端可以根据id
匹配请求的结果。
约定
- Verto协议按约定使用JSON-RPC 2.0,但又没有严格遵守规范。
- Verto中使用整数的
id
。
登录
请求:
{
"jsonrpc": "2.0",
"method": "login",
"params": {
"login": "admin@demo.xswitch.cn",
"passwd": "xxxx",
"loginParams": {
},
"userVariables": {
},
"sessid": "8faafdd3-dc45-c333-c37d-9997320f354f"
},
"id": 1
}
其中,loginParams
和userVariables
在WebRTC呼叫的时候有用。如果只是收发消息,可以忽略。
sessid
为当前Socket的唯一ID,在整个服务器上不允许重复,推荐使用uuid-v4
算法生成。在连接存续期间,sessid
应该不变。客户端可以记住这个sessid
,在下一次建议连接时可以重用。如果是WebRTC通话,则可以用它来恢复中断的通话(如在网页刷新的情况下)。
返回:
{
"jsonrpc": "2.0",
"id": 1,
"result": {
"message": "logged in",
"sessid": "8faafdd3-dc45-c333-c37d-9997320f354f"
}
}
第一次登录会收到以下事件,可以忽略。
{
"jsonrpc": "2.0",
"id": 550,
"method": "verto.clientReady",
"params": {
"reattached_sessions": []
}
}
上面的例子是使用用户名/密码登录。在某些情况下,使用用户名,密码是不安全的,这时,可以从后台获取Token,使用Token来验证。
Token放到loginParams.xui_sessid
里,如:
{
"jsonrpc": "2.0",
"method": "login",
"params": {
"login": "admin@demo.xswitch.cn",
"passwd": "x",
"loginParams": {
"xui_sessid": "18373514-b8e0-4fba-96dc-c3a1bbf68a82"
},
"userVariables": {
},
"sessid": "c321bb23-bdcc-9620-d1ec-72608184a3ec"
},
"id": 1
}
如何获取Token请参见XSwitch认证鉴权文档。
订阅事件
可以向后台订阅事件,如:
{
"jsonrpc": "2.0",
"method": "verto.subscribe",
"params": {
"eventChannel": "FSevent.heartbeat",
"sessid": "574bd1c3-6df7-1c20-ef29-a12717d72e35"
},
"id": 6
}
其中eventChannel
参数为订阅的事件名称。可以每次只订阅一个事件,也可以是一个数组,如["event1", "event2"]
。
返回:
{
"jsonrpc": "2.0",
"id": 6,
"result": {
"subscribedChannels": ["FSevent.heartbeat"],
"sessid": "574bd1c3-6df7-1c20-ef29-a12717d72e35"
}
}
订阅vcc
(呼叫中心)事件:
{
"jsonrpc": "2.0",
"method": "verto.subscribe",
"params": {
"eventChannel": "vcc.default",
"sessid": "64cfe79c-9723-ae5e-a396-53887e109d60"
},
"id": 2
}
WebRTC
Verto支持WebRTC呼叫。
呼叫
方法:verto.invite
。
{
"jsonrpc": "2.0",
"method": "verto.invite",
"params": {
"sdp": "很长,省略",
"dialogParams": {
"useVideo": true,
"useStereo": true,
"screenShare": false,
"useCamera": "any",
"useMic": "any",
"useSpeak": "any",
"tag": "webcam",
"localTag": null,
"login": "admin@demo.xswitch.cn",
"videoParams": {
"videoDevice": "any",
"width": {
"min": 1280,
"max": 1280
},
"height": {
"min": 720,
"max": 720
},
"frameRate": {
"best": 15,
"min": 15,
"max": 15
},
"minHeight": 720,
"maxHeight": 720,
"minWidth": 1280,
"maxWidth": 1280,
"minFrameRate": 15
},
"destination_number": "9196",
"caller_id_name": "admin",
"caller_id_number": "admin",
"outgoingBandwidth": "default",
"incomingBandwidth": "default",
"videoBandwidth": {
"max": 2048,
"min": 1024,
"start": 1024
},
"deviceParams": {
"useMic": "any",
"useSpeak": "any",
"useCamera": "any"
},
"callID": "e708cee0-3c33-1624-dc8c-f1ca704664f1",
"remote_caller_id_name": "Outbound Call",
"remote_caller_id_number": "9196"
},
"sessid": "1709be87-8a9b-6f93-93f0-a3817b4aa3fa"
},
"id": 2
}
服务端会返回呼叫进展消息,相当于SIP中的183:
{
"jsonrpc": "2.0",
"id": 643,
"method": "verto.media",
"params": {
"callID": "e708cee0-3c33-1624-dc8c-f1ca704664f1",
"sdp": "略 ..."
}
}
应答
{
"jsonrpc": "2.0",
"id": 644,
"method": "verto.answer",
"params": {
"callID": "e708cee0-3c33-1624-dc8c-f1ca704664f1"
}
}
挂机
{
"jsonrpc": "2.0",
"method": "verto.bye",
"params": {
"dialogParams": {
},
"sessid": "1709be87-8a9b-6f93-93f0-a3817b4aa3fa"
},
"id": 3
}
返回:
{
"jsonrpc": "2.0",
"id": 3,
"result": {
"callID": "e708cee0-3c33-1624-dc8c-f1ca704664f1",
"message": "CALL ENDED",
"causeCode": 16,
"cause": "NORMAL_CLEARING",
"sessid": "1709be87-8a9b-6f93-93f0-a3817b4aa3fa"
}
}]
事件
XSwitch/FreeSWITCH在运行过程中会出出事件,事件的method
为verto.event
,事件类型由eventChannel
决定,具体数据(data
)因事件类型而异。如:
{
"jsonrpc": "2.0",
"id": 660,
"method": "verto.event",
"params": {
"eventChannel": "FSevent.heartbeat",
"data": {
"Event-Name": "HEARTBEAT",
"Core-UUID": "e6bc80ad-d9ec-49b8-bfcf-fb70d4c8ae0c",
"FreeSWITCH-Hostname": "0515416ed1e7",
"FreeSWITCH-Switchname": "0515416ed1e7",
"FreeSWITCH-IPv4": "172.31.0.2",
"FreeSWITCH-IPv6": "::1",
"Event-Date-Local": "2020-07-17 16:45:04",
"Event-Date-GMT": "Fri, 17 Jul 2020 08:45:04 GMT",
"Event-Date-Timestamp": "1594975504302776",
"Event-Calling-File": "switch_core.c",
"Event-Calling-Function": "send_heartbeat",
"Event-Calling-Line-Number": "81",
"Event-Sequence": "62396",
"Event-Info": "System Ready",
"Up-Time": "0 years, 1 day, 0 hours, 40 minutes, 20 seconds, 105 milliseconds, 546 microseconds",
"FreeSWITCH-Version": "1.10.4-dev+git~20200716T012008Z~13da3a0095~64bit",
"Uptime-msec": "88820105",
"Session-Count": "0",
"Max-Sessions": "1000",
"Session-Per-Sec": "30",
"Session-Per-Sec-Last": "0",
"Session-Per-Sec-Max": "2",
"Session-Per-Sec-FiveMin": "0",
"Session-Since-Startup": "59",
"Session-Peak-Max": "2",
"Session-Peak-FiveMin": "0",
"Idle-CPU": "96.133333"
},
"eventSerno": 93
}
}
会议相关
Verto会议使用WebRTC入会,使用Websocket消息控制。Websocket消息基本上使用JSON-RPC 2.0规范,但又不严格。最大的问题是,JSON-RPC协议规定,id
的有无作为请求和事件的区分,但是FreeSWITCH发送的很多事件都带有id
。
会控API采用“请求-响应”和“请求-事件”方式设计,请求和响应都是异步的。
- Websocket消息使用FreeSWITCH中的EventChannel控制
- 成员列表更新使用LiveArray协议。
EventChannel
EventChannel是一个双向协议,跟FreeSWITCH有关的EventChannel有:
conference-mod
:管理员专用,如果不是会议管理员,则在客户关收不到该消息conference-info
:消息,如DTMFconference-chat
:聊天消息
所谓双向,即浏览器可以订阅这些EventChannel上的事件,也可以向这些EventChannel发送请求,FreeSWITCH会议启动后会监听这些相关的EventChannel,响应命令请求,并可以返回结果。
EventChannel的规则如下:
conference-mod.conference-name@domain
示例数据:
- 会议名称:
3000-seven.local
- Domain:
seven.local
- EventChannel:
conference-mod.3000-seven.local@seven.local
- EventChannel:
conference-info.3000-seven.local@seven.local
- EventChannel:
conference-chat.3000-seven.local@seven.local
LiveArray
LiveArray是一个服务器/客户端数组同步方案。有以下步骤:
- 在客户端刷新页面时,调用
bootObj
,获取全量与会者列表 - 以后,有新成员加入时,使用
add
进行增量 - 如果有人退出,则使用
del
减员 - 如果成员状态有变化,使用
modify
修改 - 在客户端的LiveArray实现是一个数组和对象双实现,即可以通过JS的对象的Key快读取单个成员,又可以以数组或遍历方式获取全部列表。
- 推荐在客户端直接使用LiveArray中的数据,这样效率比较高
本质上,LiveArray跟会议无关,但客户端的VertoConfMan
在内部使用了它。
WebRTC入会步骤
WebRTC直接连接mod_verto
提供的Websocket服务。
- 浏览器连接FreeSWITCH,
sessid
由客户端生成,唯一标志本次连接 - 浏览器发起呼叫,产生Channel UUID,即
callID
- 如果呼入的是一个会议,则FreeSWITCH发送一个
verto.event
,其中,eventChannel
的值为sessid
,以便消息能返回到对应的Websocket连接上,callID
为Channel UUID。消息如下:
{
"jsonrpc": "2.0",
"id": 877,
"method": "verto.event",
"params": {
"pvtData": {
"callID": "be5d95cb-7612-801f-c642-9c4fa6b451c1",
"action": "conference-liveArray-part",
"laChannel": "conference-liveArray.723773739@seven.local",
"laName": "723773739",
"role": "moderator",
"chatID": "conf+723773739@seven.local",
"conferenceMemberID": "77",
"canvasCount": 1,
"modChannel": "conference-mod.723773739@seven.local",
"chatChannel": "conference-chat.723773739@seven.local",
"infoChannel": "conference-info.723773739@seven.local"
},
"eventChannel": "2b226216-ddaa-2279-916a-f9c9dc14ea86",
"eventType": "channelPvtData",
"eventSerno": 0
}
}
- 浏览器在收到这个消息后,由于eventChannel与自己的
sessid
相同,则触发回调,可以在Websocket的onMessage
回调中收到该消息,消息类型msg
为Verto.enum.message.pvtEvent
,处理代码如下:
onMessage: function (verto, dialog, msg, data) {
console.log("data", data);
console.log("msg", msg);
console.log('verto', verto);
switch (msg) {
case Verto.enum.message.pvtEvent:
handleVertoPvtEvent(data);
break;
}
}
- 产生一个新的
VertoConfMan
,用于跟踪状态,代码如下:
function handleVertoPvtEvent(e) {
console.log("conference pvtEvent", e);
let laData = e.pvtData;
conference_name = laData.laName;
if (laData.action === "conference-liveArray-join") {
if (cman) {
cman.destroy();
cman = null;
}
cman = new VertoConfMan(verto, {
dialog: null, // dialog,
hasVid: true, // check_vid(),
laData: laData,
// onChat: chatCallback,
onLaChange: handleConferenceEvent, // LiveArray有变化时回调
onBroadcast: handleConferenceBroadcast
});
} else if (laData.action === "conference-liveArray-part") {
if (cman) cman.destroy();
cman = null;
}
}
- VertoConfMan初始化以后,会自动启动LiveArray,向后端发送
bootstrap
请求,刷新会议列表(比如刷新页面时会议中已经有100个人了):
{
"jsonrpc": "2.0",
"id": 868,
"method": "verto.event",
"params": {
"eventChannel": "conference-liveArray.723773739@seven.local",
"data": {
"liveArray": {
"command": "bootstrap",
"context": "conference-liveArray.723773739@seven.local",
"name": "723773739",
"obj": {
}
}
},
"sessid": "2b226216-ddaa-2279-916a-f9c9dc14ea86",
"userid": "admin@seven.local",
"sessid": "2b226216-ddaa-2279-916a-f9c9dc14ea86",
"fromDisplay": "admin",
"eventSerno": 0
}
}
- FreeSWITCH返回当前会议列表
{
"jsonrpc": "2.0",
"id": 870,
"method": "verto.event",
"params": {
"data": {
"action": "bootObj",
"name": "723773739",
"wireSerno": -1,
"data": [
["be5d95cb-7612-801f-c642-9c4fa6b451c1",
["0077", "admin", "Admin", "opus@48000",
"{\"audio\":{\"muted\":false,\"deaf\":false,\"onHold\":false,\"talking\":false,\"floor\":true,\"energyScore\":0},\"video\":{\"visible\":false,\"noRecover\":false,\"avatarPresented\":false,\"mediaFlow\":\"sendRecv\",\"muted\":false,\"floor\":true,\"reservationID\":null,\"roleID\":null,\"videoLayerID\":-1,\"canvasID\":0,\"watchingCanvasID\":0,\"order\":0},\"oldStatus\":\"FLOOR VIDEO (FLOOR)\",\"isModerator\":true}",
{}, null]
]
]
},
"eventChannel": "conference-liveArray.723773739@seven.local",
"sessid": "2b226216-ddaa-2279-916a-f9c9dc14ea86",
"eventSerno": 0
}
}
其中,data
是一个二维数组,第一维,存放多个成员member
,第二维的member
也是一个数组(为了节省字节没有用对象),含义如下:
data[0]
:Key,即Channel UUIDdata[1]
:成员数组,它又是一个数组,内容如下:0
:Member ID1
:Caller ID Number2
:Caller ID Name3
:Codec4
:音频状态,是一个JSON字符串5
:一个对象,包含了一些属性,如email
等6
:视频状态,是一个JSON字符串或null
后面,就可以通过LiveArray动态刷新与会者列表了
add
{
"data": {
"action": "add",
"arrIndex": 0,
"name": "3000-seven.local",
"hashKey": "8844c7ba-1a9b-0578-ed3f-419f6829981c",
"wireSerno": 1,
"data": ["0002", "admin", "Admin", "opus@48000", "{\"audio\":{\"muted\":false,\"deaf\":false,\"onHold\":false,\"talking\":false,\"floor\":false,\"energyScore\":0},\"video\":false,\"oldStatus\":\"ACTIVE\",\"isModerator\":true}", {
}, null]
},
"eventChannel": "conference-liveArray.3000-seven.local@seven.local"
}
modify
{
"data": {
"action": "modify",
"name": "3000-seven.local",
"hashKey": "8844c7ba-1a9b-0578-ed3f-419f6829981c",
"wireSerno": 2,
"data": ["0002", "admin", "Admin", "opus@48000", "{\"audio\":{\"muted\":false,\"deaf\":false,\"onHold\":false,\"talking\":false,\"floor\":false,\"energyScore\":0},\"video\":false,\"oldStatus\":\"ACTIVE\",\"isModerator\":true}", {
}, null]
},
"eventChannel": "conference-liveArray.3000-seven.local@seven.local"
}
del
{
"data": {
"name": "3000-seven.local",
"action": "del",
"hashKey": "8844c7ba-1a9b-0578-ed3f-419f6829981c",
"wireSerno": 4,
"data": ["0002", "admin", "Admin", "opus@48000", "{\"audio\":{\"muted\":false,\"deaf\":false,\"onHold\":false,\"talking\":false,\"floor\":true,\"energyScore\":0},\"video\":false,\"oldStatus\":\"FLOOR\",\"isModerator\":true}", {
}, null]
},
"eventChannel": "conference-liveArray.3000-seven.local@seven.local"
}
没有WebRTC参与的会控
上述消息必须由pvtData
触发,而pvtData
只有在WebRTC入会时才能送达客户端。如果没有WebRTC参与的会控,使用该机制时,可以做一个假的pvtData
消息(所有数据都可由Conference Name和Domain推测出来),然后初始化VertoConfMan,并进行bootstrap
。
XCC扩展
本扩展只有后台配置了XCC CMan微服务时才可用。
XCC是集群架构,同时控制多个XSwitch。后面控制由cman
(Go语言)实现。
我们尽量使用与原FreeSWITCH一致的数据结构,并增加如下扩展:
- 将
member
数组的第4-6
个元素的字符串全转成对象 - 在第5个元素中增加
node_uuid
- 实现一个新的
VertoCMan
,使用新函数控制会议,新函数将在请求中包含node_uuid
- 有些函数使用请求响应机制,如
bootstrap
等
关于pvtData
:
在FreeSWITCH会议中,pvtData
是由FreeSWITCH主动发送的(在WebRTC呼入会议时),但在cman(Go)
中,需要有一个触发机制。为了简洁,我们在客户端通过如下步骤触发:
- Websocket连接成功后,执行
verto.cmanBootstrap(conference_name, domain)
触发,消息如下:
{
"jsonrpc": "2.0",
"method": "verto.broadcast",
"params": {
"eventChannel": "cman.bootstrap",
"data": {
"conferenceName": "3000",
"domain": "seven.local"
},
"sessid": "71006add-c123-3050-e191-e161a0338e78"
},
"id": 2
}
- FreeSWITCH返回当前会议列表
{
"jsonrpc": "2.0",
"method": "verto.event",
"params": {
"eventChannel": "conference-liveArray.716418825-xswitch.cn@xswitch.cn",
"sessid": "1a8a0e08-3fd1-edcf-147c-486271495c48",
"data": {
"action": "bootObj",
"name": "716418825-xswitch.cn",
"data": [
[
"743d92d9-6bd8-6f9b-0916-9ea325a324dc",
{
"memberID": "142",
"cidNumber": "1001",
"cidName": "1001",
"codec": "",
"status": {
"audio": {
"talking": false,
"deaf": false,
"muted": false,
"onHold": false,
"energyScore": 0,
"floor": false
}
},
"email": "",
"node_uuid": "1407beec-b94a-43c1-bf9e-a222b146d786",
"active": false,
"uuid": "743d92d9-6bd8-6f9b-0916-9ea325a324dc"
}
]
],
"wireSerno": -1
}
},
"id": 13
}
- UAS收到上述请求,通过Request(Call)方式发给
cman(Go)
,cman
返回结果后(包含pvtData),UAS通过Websocket返回给浏览器。 - 浏览器收到
pvtData
后,按正常的逻辑触发bootstrap
(bootObj
)。
这种方式在浏览器端不需要WebRTC入会。但需要注册,如果浏览器端也入会,则需要防止消息重复(todo)。
不同于单机的Verto,XCC是个多节点架构,需要UAS和后面的cman控制多个FreeSWITCH节点。因而,大部分命令都需要node_uuid
,加了verto-cman.js
,使用XCC扩展的API,而verto-confman.js
应该保持与FreeSWITCH中的兼容。
Conference Layout
layout-info
消息会在conference-info
这个EventChannel中自动发出,在ConfMan创建时订阅该事件即可(onInfo
回调)。示例如下:
{
"jsonrpc": "2.0",
"id": 13,
"method": "verto.event",
"params": {
"eventData": {
"contentType": "layout-info",
"canvasInfo": {
"canvasID": 0,
"totalLayers": 1,
"layersUsed": 0,
"layoutFloorID": 0,
"layoutName": "1x1",
"canvasLayouts": [{
"x": 0,
"y": 0,
"scale": 360,
"hscale": 360,
"scale": 360,
"zoom": 0,
"border": 0,
"floor": 1,
"overlap": 0,
"screenWidth": 1280,
"screenHeight": 720,
"xPOS": 0,
"yPOS": 0
}],
"scale": 360
}
},
"eventChannel": "conference-info.3000-seven.local@seven.local",
"eventSerno": 0
}
}