XRTC开发文档
Verto 协议
Verto 协议
本文描述 Verto 的底层协议,大多数情况下你不需要使用它,仅供参考。
Verto 协议基于 JSON-RPC 实现。关于 JSON RPC 协议规范,参见:https://www.jsonrpc.org/specification
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 } }