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
}

其中,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