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
}

其中,loginParamsuserVariables在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在运行过程中会出出事件,事件的methodverto.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:消息,如DTMF
  • conference-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回调中收到该消息,消息类型msgVerto.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 UUID

  • data[1]:成员数组,它又是一个数组,内容如下:

    • 0:Member ID
    • 1:Caller ID Number
    • 2:Caller ID Name
    • 3:Codec
    • 4:音频状态,是一个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后,按正常的逻辑触发bootstrapbootObj)。

这种方式在浏览器端不需要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
	}
}
SDK