OWT Server信令分析 (下) [Open WebRTC Toolkit]

目录

  1. 信令分析因为包含一些代码和格式,文章很长,所以分成上下两篇记录,OWT(Open WebRTC Toolkit)相关文章。

相关文章:

  1. Ubuntu环境安装OWT Server[Open WebRTC Toolkit]
  2. Docker环境安装OWT Server[Open WebRTC Toolkit]
  3. OWT Server整体架构分析 [Open WebRTC Toolkit]
  4. OWT Server信令分析 (上) [Open WebRTC Toolkit]
  5. OWT Server信令分析 (下) [Open WebRTC Toolkit]
  6. OWT Server进程结构和JS代码处理流程 [Open WebRTC Toolkit]
  7. OWT Server REST API

1. OWT Server信令分析

  1. OWT信令协议分为RESTful API和SocketIO长连接两部分,RESTful API由Management API提供,SocketIO长连接则由WebRTC Portal提供。
  2. 不过不是直接调用Management API,而是调用Conference Sample Server的接口,它是对Management API的一个封装,源码在owt-client-javascript https://github.com/open-webrtc-toolkit/owt-client-javascript/tree/master/src/samples/conference项目中。
  3. OWT Server WebRTC的信令交互过程如下:
A POST /tokens/
A SocketIO connect
A SocketIO login
A SocketIO publish
A SocketIO soac offer
A SocketIO soac candidate
Portal SocketIO soac answerB POST /tokens/
B SocketIO connect
B SocketIO login
B SocketIO subscribe
B SocketIO soac offer
B SocketIO soac candidate
Portal SocketIO soac answerSocketIO logout

2. OWT Server信令交互过程解析

  1. 以owt-client-javascript作为客户端,owt-server作为服务端为例。
  2. owt server安装完成后,可以在https://localhost:3004或者https://ip:3004看到通话界面。
  3. owt-client-javascript入口函数为src/samples/conference/public/scripts/index.js的window.onload(通话界面按F12可见),简化代码如下:
    window.onload = function() {var simulcast = getParameterByName('simulcast') || false;var shareScreen = getParameterByName('screen') || false;myRoom = getParameterByName('room');var isHttps = (location.protocol === 'https:');var mediaUrl = getParameterByName('url');var isPublish = getParameterByName('publish');createToken(myRoom, 'user', 'presenter', function(response) {var token = response;conference.join(token).then(resp => {...}, function(err) {...});}, serverUrlBase);};
  1. 可以看到,客户端首先会创建token,拿到token后再加入会议室。
  2. 信令分析上篇解析了获取token过程(OWT(Open WebRTC Toolkit) Server信令分析 (上)),接下来分析socketio信令交互过程。

1. Http POST创建token

  1. 见:OWT(Open WebRTC Toolkit) Server信令分析 (上)

2. SocketIO登录

  1. SocketIO连接成功后,客户端需要主动发送login事件,携带的数据格式为:
{"token":"eyJ0b2tlbklkIjoiNjM2OWMyNGI1MTI2YWMzZTM0NjFiMzdjIiwiaG9zdCI6IjUyLjgxLjIwMS4xOTA6ODA4MCIsInNlY3VyZSI6dHJ1ZSwic2lnbmF0dXJlIjoiWW1JeU56SXdaakJsWkdJMU16VmlPV1ExT0RSa1l6Um1NREk0WTJWak1tTmlZbU14TnpZME1ETmtZVEV3TkRrek9ERXhZMlJoTkdReFpqRTJNR0UxTXc9PSJ9","userAgent":{"sdk":{"version":"5.0","type":"JavaScript"}},"protocol":"1.2"
}
  1. token字段是创建token接口返回的base64编码字符串,其他的字段用来描述客户端的属性,以便服务端在需要时进行检查和区分处理。
  2. 登录成功后,SocketIO的ACK格式为:
{"user":"user","role":"presenter","permission":{"publish":{"video":true,"audio":true},"subscribe":{"video":true,"audio":true}},"room":{"id":"62b95bd27ff8054480cbb73a","views":["common"],"participants":[{"id":"IqCKvfpG402ooz_oAAAZ","user":"user","role":"presenter"}],"streams":[{"id":"62b95bd27ff8054480cbb73a-common","type":"mixed","media":{"tracks":[{"type":"audio","format":{"codec":"opus","sampleRate":48000,"channelNum":2},"optional":{"format":[{"codec":"isac","sampleRate":16000},{"codec":"isac","sampleRate":32000},{"codec":"g722","sampleRate":16000,"channelNum":1},{"codec":"pcma"},{"codec":"pcmu"},{"codec":"aac","sampleRate":48000,"channelNum":2},{"codec":"ac3"},{"codec":"nellymoser"},{"codec":"ilbc"}]},"status":"active"},{"type":"video","format":{"codec":"vp8"},"parameters":{"resolution":{"width":640,"height":480},"framerate":24,"keyFrameInterval":100},"optional":{"format":[{"codec":"h264","profile":"CB"},{"codec":"h264","profile":"B"},{"codec":"vp9"}],"parameters":{"resolution":[{"width":480,"height":360},{"width":426,"height":320},{"width":320,"height":240},{"width":212,"height":160},{"width":160,"height":120},{"width":352,"height":288}],"bitrate":["x0.8","x0.6","x0.4","x0.2"],"framerate":[6,12,15],"keyFrameInterval":[100,30,5,2,1]}},"status":"active"}]},"data":null,"info":{"label":"common","activeInput":"unknown","layout":[],"origin":{"isp":"isp","region":"region"}}}]},"id":"IqCKvfpG402ooz_oAAAZ","reconnectionTicket":"eyJwYXJ0aWNpcGFudElkIjoiSXFDS3ZmcEc0MDJvb3pfb0FBQVoiLCJ0aWNrZXRJZCI6IjJmdmdqYm52NW82Iiwibm90QmVmb3JlIjoxNjY3ODc1NDAzNjkwLCJub3RBZnRlciI6MTY2Nzg3NjAwMzY5MCwic2lnbmF0dXJlIjoiTWpZeVlqbGpNMk5sWXpkalpERmxOV0k1TkdNNFlXVmpPREpsWW1FM09UQTNZemcxWW1Sak9EWmpaR1E0TnpCalpERXdZV0l4TW1ZMlpUTTFNV0l4TUE9PSJ9"}
  1. ACK里room字段,包含了房间内的用户和流的信息,分别对应participants字段和streams字段。
  2. ACK里的user、role、id字段含义和participants的子字段含义一致,分别是用户id、角色、服务器分配的唯一标识。之后用户离开的消息只包含id字段,因此如果要用user字段标识用户,就需要维护id到user的映射关系。
  3. streams:
    1. type用来表明流的类型:

      1. mixed为MCU合成的流。
      2. forward为SFU转发的流。
    2. id为流的id,订阅时需要用到。
    3. media和info字段表明流的属性。
  4. reconnectionTicket字段用于重连时的重新登录。

1. 客户端代码

  1. SocketIO连接成功后,客户端需要主动发送login事件:

    1. 位置:src/sdk/conference/signaling.js
  /*** @function connect* @instance* @desc Connect to a portal.* @memberof Oms.Conference.SioSignaling* @return {Promise<Object, Error>} Return a promise resolved with the data returned by portal if successfully logged in. Or return a promise rejected with a newly created Oms.Error if failed.* @param {string} host Host of the portal.* @param {string} isSecured Is secure connection or not.* @param {string} loginInfo Information required for logging in.* @private.*/connect(host, isSecured, loginInfo) {return new Promise((resolve, reject) => {const opts = {'reconnection': true,'reconnectionAttempts': reconnectionAttempts,'force new connection': true,};this._socket = io(host, opts);...//SocketIO连接成功后,客户端需要主动发送login事件this._socket.emit('login', loginInfo, (status, data) => {if (status === 'ok') {this._loggedIn = true;this._onReconnectionTicket(data.reconnectionTicket);this._socket.on('connect', () => {// re-login with reconnection ticket.this._socket.emit('relogin', this._reconnectionTicket, (status,data) => {if (status === 'ok') {this._reconnectTimes = 0;this._onReconnectionTicket(data);} else {this.dispatchEvent(new EventModule.OwtEvent('disconnect'));}});});}handleResponse(status, data, resolve, reject);});});}

2. 服务端代码

  1. 位置:socket.on函数在source/portal/socketIOServer.js中。

  2. SocketIO服务器的逻辑在socketIOServer.js和v11Client.js(除了v11Client,还有v10Client和legacyClient,用于老版本的兼容)中。

    1. v11Client监听了其他所有OWT Server信令协议的事件,比如publish、subscribe等
  3. socketIOServer监听login、relogin、refreshReconnectionTicket、logout、disconnect和connection这6个事件。

    1. 前4个是OWT Server信令协议定义的。
    2. 最后两个由长连接客户端的断开和接入触发。
  4. 在处理login和relogin事件时,会根据protocol字段的取值来决定使用的Client版本(V10和V11)

socket.on('login', function(login_info, callback) {...client_id = socket.id + '';var client;if (login_info.protocol === undefined) {protocol_version = 'legacy';client = new LegacyClient(client_id, that, portal);} else if (login_info.protocol === '1.0' ||login_info.protocol === '1.1' ||login_info.protocol === '1.2') {//FIXME: Reject connection from 3.5 clientif (login_info.userAgent && login_info.userAgent.sdk &&login_info.userAgent.sdk.version === '3.5') {safeCall(callback, 'error', 'Deprecated client version');return socket.disconnect();}protocol_version = login_info.protocol;client = new Client(client_id, that, portal, protocol_version);} else {safeCall(callback, 'error', 'Unknown client protocol');return socket.disconnect();}return validateUserAgent(login_info.userAgent).then((reconnEnabled) => {reconnection.enabled = reconnEnabled;return new Promise(function(resolve){resolve(JSON.parse((Buffer.from(login_info.token, 'base64')).toString()));});}).then((token) => {return client.join(token);}).then((result) => {...});
});
  1. socketIOServer监听login事件,根据protocol字段创建对应客户端,然后会进行校验token以及完成其他操作,涉及代码较多,这篇主要为信令交互内容,后续再补上相关。
  2. 调用栈为:
source/portal/socketIOServer.js socket.on('login', function(login_info, ... =>
source/portal/v11Client.js that.join = (token) =>
source/portal/portal.js that.join = function(participantId, token) =>
source/portal/rpcRequest.js that.join = function(controller, roomId, participant) =>
source/agent/conference/conference.js that.join = function(roomId, participantInfo, callback) =>
source/agent/conference/conference.js  initRoom = function(roomId, origin) => //保存房间信息,创建roomController、accessController
source/agent/conference/conference.js  addParticipant = function(participantInfo, permission) => //保存用户信息
  1. 其中:

    1. accessController负责接入逻辑,比如WebRTC的信令处理等。
    2. roomController负责房间内各种状态的维护和功能的控制,比如流、订阅关系等,以及发布(publish)、订阅(subscribe)、混流(mix)、设置混流布局(setLayout)等。
    3. 也有很多处理工作是向其他组件的node进程发起RPC完成的。

3. SocketIO发布流

  1. 登录成功后,就可以发布音视频流了,发布时客户端发送publish事件,携带的数据格式为:
{"media":{"tracks":[{"type":"audio","mid":"2","source":"mic"},{"type":"video","mid":"3","source":"camera"}]},"transport":{"id":"cbeae5478b684b5e8f639fa84aafde9b","type":"webrtc"}
}
  1. 用来指明音视频数据的格式。
  2. 发布成功的ACK格式为:
    1. 其中id为streamid
{"id":"55aa9331e2b741338639d282f0bffa0d","transportId":"eceda0bafe3e4a1eab1a75056ede3cd8"
}

1. 客户端代码

  1. 部分代码如下:
  async publish(stream, options, videoCodecs) {...return this._signaling.sendSignalingMessage('publish', {media: {tracks: trackOptions},attributes: stream.attributes,transport: {id: this._id, type: 'webrtc'},}).catch((e) => {// Send SDP even when failed to get Answer.this._signaling.sendSignalingMessage('soac', {id: this._id,signaling: localDesc,});throw e;});}).then((data) => {...this._signaling.sendSignalingMessage('soac', {id: this._id,signaling: localDesc,});}).catch((e) => {...}
  1. publish客户端代码调用栈为:
src/samples/conference/public/scripts/index.js conference.publish(localStream, ... =>
src/sdk/conference/client.js this.publish =>
src/sdk/conference/channel.js async publish =>
src/sdk/conference/channel.js _signaling.sendSignalingMessage('publish', =>

2. 服务端代码

  1. 服务端会接收publish信令,部分代码如下:
socket.on('publish', function(pubReq, callback) {if(!that.inRoom){return safeCall(callback, 'error', 'Illegal request');}//FIXME: move the id assignment to conferencevar stream_id = uuidWithoutDash();var transportId;return adapter.translateReq(ReqType.Pub, pubReq).then((req) => {if (req.transport && req.transport.type == 'quic') {req.type = 'quic';if (!req.transport.id) {req.transport.id = uuidWithoutDash();}transportId = req.transport.id;} else {req.type = 'webrtc'; //FIXME: For backend compatibility with v3.4 clients.if (!req.transport || !req.transport.id) {req.transport = { type : 'webrtc', id : stream_id };}}transportId = req.transport.id;return portal.publish(clientId, stream_id, req);}).then((result) => {safeCall(callback, 'ok', {id: stream_id, transportId});}).catch(onError('publish', callback));
});
  1. 调用栈:
source/portal/v11Client.js socket.on('publish', function(pubReq,
source/portal/portal.js that.publish =  = function(participantId, streamId, pubInfo) =>
source/portal/rpcRequest.js that.publish = function(controller, participantId, streamId, Options) =>
source/agent/conference/conference.js that.publish = function(participantId, streamId, pubInfo, callback) =>
source/agent/conference/accessController.js that.initiate = function(participantId, sessionId, direction, origin, sessionOptions, ... =>
source/agent/conference/rpcRequest.js that.getWorkerNode = function(clusterManager, purpose, ... => //发起RPC,拿到WebRTC Agent的nodeId
source/agent/conference/rpcRequest.js that.initiate = function(accessNode, sessionId, ... => //向WebRTC Agent的node发起publish RPC
  1. WebRTC Agent的RPC服务定义在webrtc/index.js中。
source/agent/conference/rpcRequest.js that.initiate = function(accessNode, sessionId, ... => //向WebRTC Agent的node发起publish RPC
source/agent/webrtc/index.js that.publish = function (operationId, connectionType, ... =>
source/agent/webrtc/index.js createWebRTCConnection = function (transportId, ... => //创建WrtcConnection(`webrtc/wrtcConnection.js`),它负责和C++ 代码进行交互
  1. 在创建WrtcConnection时会执行webrtc/wrtcConnection.js的initWebRtcConnection函数,其中会监听C++代码发送的事件(由webrtc/connection.js做了一层翻译,变成status_event事件),收到事件后会回调给webrtc/index.js的notifyStatus函数,向Conference Agent的node发起onSessionProgress RPC。
  /** Given a WebRtcConnection waits for the state CANDIDATES_GATHERED for set remote SDP.*/var initWebRtcConnection = function (wrtc) {wrtc.on('status_event', (evt, status) => {if (evt.type === 'answer') {processAnswer(evt.sdp);const message = localSdp.toString();log.debug('Answer SDP', message);on_status({type: 'answer', sdp: message});} else if (evt.type === 'candidate') {let message = evt.candidate;networkInterfaces.forEach((i) => {if (i.ip_address && i.replaced_ip_address) {message = message.replace(new RegExp(i.ip_address, 'g'), i.replaced_ip_address);}});on_status({type: 'candidate', candidate: message});} else if (evt.type === 'failed') {log.warn('ICE failed, ', status, wrtc.id);on_status({type: 'failed', reason: 'Ice procedure failed.'});} else if (evt.type === 'ready') {log.debug('Connection ready, ', wrtc.wrtcId);on_status({type: 'ready'});}});wrtc.init(wrtcId);};

4. SocketIO发送Offer和ICE Candidate

  1. 对接OWT Server时,无论是发布还是订阅,都需要客户端创建Offer。拿到Offer后发送soac事件,携带的数据格式为:

    1. id是发布流成功ACK里返回的streamid。
    2. signaling的"type":"offer"表示发送的是Offer消息。
    3. 剩下sdp字段为Offer内容。
{"id":"eceda0bafe3e4a1eab1a75056ede3cd8","signaling":{"type":"offer","sdp":"v=0o=- 7416128173695703170 3 IN IP4 127.0.0.1s=-t=0 0a=group:BUNDLE 0 1 2 3a=extmap-allow-mixeda=msid-semantic: WMSm=audio 60916 UDP/TLS/RTP/SAVPF 111 63 103 104 9 0 8 106 105 13 110 112 113 126c=IN IP4 10.110.133.203a=rtcp:9 IN IP4 0.0.0.0a=candidate:2722325311 1 udp 2122265343 fc00:2::e19b:dc32:443d:dd84 55478 typ host generation 0 network-id 2a=candidate:3962993534 1 udp 2122194687 10.110.133.203 60916 typ host generation 0 network-id 1a=ice-ufrag:gdMSa=ice-pwd:1yWra8IAyw4YqqXE6JLbrfDIa=ice-options:tricklea=fingerprint:sha-256 07:95:5D:7A:A8:FD:53:D3:B9:8B:CC:8C:29:54:08:89:83:63:5E:CE:A6:DE:01:EF:5F:0A:06:96:ED:E1:8A:9Fa=setup:actpassa=mid:0a=extmap:1 urn:ietf:params:rtp-hdrext:ssrc-audio-levela=extmap:2 http://www.webrtc.org/experiments/rtp-hdrext/abs-send-timea=extmap:3 http://www.ietf.org/id/draft-holmer-rmcat-transport-wide-cc-extensions-01a=extmap:4 urn:ietf:params:rtp-hdrext:sdes:mida=recvonlya=rtcp-muxa=rtpmap:111 opus/48000/2a=rtcp-fb:111 transport-cca=fmtp:111 minptime=10;useinbandfec=1a=rtpmap:63 red/48000/2a=fmtp:63 111/111a=rtpmap:103 ISAC/16000a=rtpmap:104 ISAC/32000a=rtpmap:9 G722/8000a=rtpmap:0 PCMU/8000a=rtpmap:8 PCMA/8000a=rtpmap:106 CN/32000a=rtpmap:105 CN/16000a=rtpmap:13 CN/8000a=rtpmap:110 telephone-event/48000a=rtpmap:112 telephone-event/32000a=rtpmap:113 telephone-event/16000a=rtpmap:126 telephone-event/8000m=video 9 UDP/TLS/RTP/SAVPF 96 97 98 99 100 101 35 36 37 38 102 122 127 121 125 107 108 109 124 120 39 40 41 42 43 44 45 46 47 48 123 119 114 115 116 49c=IN IP4 0.0.0.0a=rtcp:9 IN IP4 0.0.0.0a=ice-ufrag:gdMSa=ice-pwd:1yWra8IAyw4YqqXE6JLbrfDIa=ice-options:tricklea=fingerprint:sha-256 07:95:5D:7A:A8:FD:53:D3:B9:8B:CC:8C:29:54:08:89:83:63:5E:CE:A6:DE:01:EF:5F:0A:06:96:ED:E1:8A:9Fa=setup:actpassa=mid:1a=extmap:14 urn:ietf:params:rtp-hdrext:toffseta=extmap:2 http://www.webrtc.org/experiments/rtp-hdrext/abs-send-timea=extmap:13 urn:3gpp:video-orientationa=extmap:3 http://www.ietf.org/id/draft-holmer-rmcat-transport-wide-cc-extensions-01a=extmap:5 http://www.webrtc.org/experiments/rtp-hdrext/playout-delaya=extmap:6 http://www.webrtc.org/experiments/rtp-hdrext/video-content-typea=extmap:7 http://www.webrtc.org/experiments/rtp-hdrext/video-timinga=extmap:8 http://www.webrtc.org/experiments/rtp-hdrext/color-spacea=extmap:4 urn:ietf:params:rtp-hdrext:sdes:mida=extmap:10 urn:ietf:params:rtp-hdrext:sdes:rtp-stream-ida=extmap:11 urn:ietf:params:rtp-hdrext:sdes:repaired-rtp-stream-ida=recvonlya=rtcp-muxa=rtcp-rsizea=rtpmap:96 VP8/90000a=rtcp-fb:96 goog-remba=rtcp-fb:96 transport-cca=rtcp-fb:96 ccm fira=rtcp-fb:96 nacka=rtcp-fb:96 nack plia=rtpmap:97 rtx/90000a=fmtp:97 apt=96a=rtpmap:98 VP9/90000a=rtcp-fb:98 goog-remba=rtcp-fb:98 transport-cca=rtcp-fb:98 ccm fira=rtcp-fb:98 nacka=rtcp-fb:98 nack plia=fmtp:98 profile-id=0a=rtpmap:99 rtx/90000a=fmtp:99 apt=98a=rtpmap:100 VP9/90000a=rtcp-fb:100 goog-remba=rtcp-fb:100 transport-cca=rtcp-fb:100 ccm fira=rtcp-fb:100 nacka=rtcp-fb:100 nack plia=fmtp:100 profile-id=2a=rtpmap:101 rtx/90000a=fmtp:101 apt=100a=rtpmap:35 VP9/90000a=rtcp-fb:35 goog-remba=rtcp-fb:35 transport-cca=rtcp-fb:35 ccm fira=rtcp-fb:35 nacka=rtcp-fb:35 nack plia=fmtp:35 profile-id=1a=rtpmap:36 rtx/90000a=fmtp:36 apt=35a=rtpmap:37 VP9/90000a=rtcp-fb:37 goog-remba=rtcp-fb:37 transport-cca=rtcp-fb:37 ccm fira=rtcp-fb:37 nacka=rtcp-fb:37 nack plia=fmtp:37 profile-id=3a=rtpmap:38 rtx/90000a=fmtp:38 apt=37a=rtpmap:102 H264/90000a=rtcp-fb:102 goog-remba=rtcp-fb:102 transport-cca=rtcp-fb:102 ccm fira=rtcp-fb:102 nacka=rtcp-fb:102 nack plia=fmtp:102 level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42001fa=rtpmap:122 rtx/90000a=fmtp:122 apt=102a=rtpmap:127 H264/90000a=rtcp-fb:127 goog-remba=rtcp-fb:127 transport-cca=rtcp-fb:127 ccm fira=rtcp-fb:127 nacka=rtcp-fb:127 nack plia=fmtp:127 level-asymmetry-allowed=1;packetization-mode=0;profile-level-id=42001fa=rtpmap:121 rtx/90000a=fmtp:121 apt=127a=rtpmap:125 H264/90000a=rtcp-fb:125 goog-remba=rtcp-fb:125 transport-cca=rtcp-fb:125 ccm fira=rtcp-fb:125 nacka=rtcp-fb:125 nack plia=fmtp:125 level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42e01fa=rtpmap:107 rtx/90000a=fmtp:107 apt=125a=rtpmap:108 H264/90000a=rtcp-fb:108 goog-remba=rtcp-fb:108 transport-cca=rtcp-fb:108 ccm fira=rtcp-fb:108 nacka=rtcp-fb:108 nack plia=fmtp:108 level-asymmetry-allowed=1;packetization-mode=0;profile-level-id=42e01fa=rtpmap:109 rtx/90000a=fmtp:109 apt=108a=rtpmap:124 H264/90000a=rtcp-fb:124 goog-remba=rtcp-fb:124 transport-cca=rtcp-fb:124 ccm fira=rtcp-fb:124 nacka=rtcp-fb:124 nack plia=fmtp:124 level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=4d001fa=rtpmap:120 rtx/90000a=fmtp:120 apt=124a=rtpmap:39 H264/90000a=rtcp-fb:39 goog-remba=rtcp-fb:39 transport-cca=rtcp-fb:39 ccm fira=rtcp-fb:39 nacka=rtcp-fb:39 nack plia=fmtp:39 level-asymmetry-allowed=1;packetization-mode=0;profile-level-id=4d001fa=rtpmap:40 rtx/90000a=fmtp:40 apt=39a=rtpmap:41 H264/90000a=rtcp-fb:41 goog-remba=rtcp-fb:41 transport-cca=rtcp-fb:41 ccm fira=rtcp-fb:41 nacka=rtcp-fb:41 nack plia=fmtp:41 level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=f4001fa=rtpmap:42 rtx/90000a=fmtp:42 apt=41a=rtpmap:43 H264/90000a=rtcp-fb:43 goog-remba=rtcp-fb:43 transport-cca=rtcp-fb:43 ccm fira=rtcp-fb:43 nacka=rtcp-fb:43 nack plia=fmtp:43 level-asymmetry-allowed=1;packetization-mode=0;profile-level-id=f4001fa=rtpmap:44 rtx/90000a=fmtp:44 apt=43a=rtpmap:45 AV1/90000a=rtcp-fb:45 goog-remba=rtcp-fb:45 transport-cca=rtcp-fb:45 ccm fira=rtcp-fb:45 nacka=rtcp-fb:45 nack plia=rtpmap:46 rtx/90000a=fmtp:46 apt=45a=rtpmap:47 AV1/90000a=rtcp-fb:47 goog-remba=rtcp-fb:47 transport-cca=rtcp-fb:47 ccm fira=rtcp-fb:47 nacka=rtcp-fb:47 nack plia=fmtp:47 profile=1a=rtpmap:48 rtx/90000a=fmtp:48 apt=47a=rtpmap:123 H264/90000a=rtcp-fb:123 goog-remba=rtcp-fb:123 transport-cca=rtcp-fb:123 ccm fira=rtcp-fb:123 nacka=rtcp-fb:123 nack plia=fmtp:123 level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=64001fa=rtpmap:119 rtx/90000a=fmtp:119 apt=123a=rtpmap:114 red/90000a=rtpmap:115 rtx/90000a=fmtp:115 apt=114a=rtpmap:116 ulpfec/90000a=rtpmap:49 flexfec-03/90000a=rtcp-fb:49 goog-remba=rtcp-fb:49 transport-cca=fmtp:49 repair-window=10000000m=audio 9 UDP/TLS/RTP/SAVPF 111 63 103 104 9 0 8 106 105 13 110 112 113 126c=IN IP4 0.0.0.0a=rtcp:9 IN IP4 0.0.0.0a=ice-ufrag:gdMSa=ice-pwd:1yWra8IAyw4YqqXE6JLbrfDIa=ice-options:tricklea=fingerprint:sha-256 07:95:5D:7A:A8:FD:53:D3:B9:8B:CC:8C:29:54:08:89:83:63:5E:CE:A6:DE:01:EF:5F:0A:06:96:ED:E1:8A:9Fa=setup:actpassa=mid:2a=extmap:1 urn:ietf:params:rtp-hdrext:ssrc-audio-levela=extmap:2 http://www.webrtc.org/experiments/rtp-hdrext/abs-send-timea=extmap:3 http://www.ietf.org/id/draft-holmer-rmcat-transport-wide-cc-extensions-01a=extmap:4 urn:ietf:params:rtp-hdrext:sdes:mida=sendonlya=msid:fbiSmx6n3xm4yA1FQtKj8zH9yGBv7vmGoVai b2bbec5a-1452-4840-9171-30a10d300741a=rtcp-muxa=rtpmap:111 opus/48000/2a=rtcp-fb:111 transport-cca=fmtp:111 minptime=10;useinbandfec=1a=rtpmap:63 red/48000/2a=fmtp:63 111/111a=rtpmap:103 ISAC/16000a=rtpmap:104 ISAC/32000a=rtpmap:9 G722/8000a=rtpmap:0 PCMU/8000a=rtpmap:8 PCMA/8000a=rtpmap:106 CN/32000a=rtpmap:105 CN/16000a=rtpmap:13 CN/8000a=rtpmap:110 telephone-event/48000a=rtpmap:112 telephone-event/32000a=rtpmap:113 telephone-event/16000a=rtpmap:126 telephone-event/8000a=ssrc:3856090643 cname:L8DTZMOJip3IXAHsa=ssrc:3856090643 msid:fbiSmx6n3xm4yA1FQtKj8zH9yGBv7vmGoVai b2bbec5a-1452-4840-9171-30a10d300741m=video 9 UDP/TLS/RTP/SAVPF 96 97 102 122 127 121 125 107 108 109 124 120 39 40 45 46 98 99 100 101 123 119 114 115 116c=IN IP4 0.0.0.0a=rtcp:9 IN IP4 0.0.0.0a=ice-ufrag:gdMSa=ice-pwd:1yWra8IAyw4YqqXE6JLbrfDIa=ice-options:tricklea=fingerprint:sha-256 07:95:5D:7A:A8:FD:53:D3:B9:8B:CC:8C:29:54:08:89:83:63:5E:CE:A6:DE:01:EF:5F:0A:06:96:ED:E1:8A:9Fa=setup:actpassa=mid:3a=extmap:14 urn:ietf:params:rtp-hdrext:toffseta=extmap:2 http://www.webrtc.org/experiments/rtp-hdrext/abs-send-timea=extmap:13 urn:3gpp:video-orientationa=extmap:3 http://www.ietf.org/id/draft-holmer-rmcat-transport-wide-cc-extensions-01a=extmap:5 http://www.webrtc.org/experiments/rtp-hdrext/playout-delaya=extmap:6 http://www.webrtc.org/experiments/rtp-hdrext/video-content-typea=extmap:7 http://www.webrtc.org/experiments/rtp-hdrext/video-timinga=extmap:8 http://www.webrtc.org/experiments/rtp-hdrext/color-spacea=extmap:4 urn:ietf:params:rtp-hdrext:sdes:mida=extmap:10 urn:ietf:params:rtp-hdrext:sdes:rtp-stream-ida=extmap:11 urn:ietf:params:rtp-hdrext:sdes:repaired-rtp-stream-ida=sendonlya=msid:fbiSmx6n3xm4yA1FQtKj8zH9yGBv7vmGoVai 1327213e-5d74-461f-be50-ed78ede6a453a=rtcp-muxa=rtcp-rsizea=rtpmap:96 VP8/90000a=rtcp-fb:96 goog-remba=rtcp-fb:96 transport-cca=rtcp-fb:96 ccm fira=rtcp-fb:96 nacka=rtcp-fb:96 nack plia=rtpmap:97 rtx/90000a=fmtp:97 apt=96a=rtpmap:102 H264/90000a=rtcp-fb:102 goog-remba=rtcp-fb:102 transport-cca=rtcp-fb:102 ccm fira=rtcp-fb:102 nacka=rtcp-fb:102 nack plia=fmtp:102 level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42001fa=rtpmap:122 rtx/90000a=fmtp:122 apt=102a=rtpmap:127 H264/90000a=rtcp-fb:127 goog-remba=rtcp-fb:127 transport-cca=rtcp-fb:127 ccm fira=rtcp-fb:127 nacka=rtcp-fb:127 nack plia=fmtp:127 level-asymmetry-allowed=1;packetization-mode=0;profile-level-id=42001fa=rtpmap:121 rtx/90000a=fmtp:121 apt=127a=rtpmap:125 H264/90000a=rtcp-fb:125 goog-remba=rtcp-fb:125 transport-cca=rtcp-fb:125 ccm fira=rtcp-fb:125 nacka=rtcp-fb:125 nack plia=fmtp:125 level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42e01fa=rtpmap:107 rtx/90000a=fmtp:107 apt=125a=rtpmap:108 H264/90000a=rtcp-fb:108 goog-remba=rtcp-fb:108 transport-cca=rtcp-fb:108 ccm fira=rtcp-fb:108 nacka=rtcp-fb:108 nack plia=fmtp:108 level-asymmetry-allowed=1;packetization-mode=0;profile-level-id=42e01fa=rtpmap:109 rtx/90000a=fmtp:109 apt=108a=rtpmap:124 H264/90000a=rtcp-fb:124 goog-remba=rtcp-fb:124 transport-cca=rtcp-fb:124 ccm fira=rtcp-fb:124 nacka=rtcp-fb:124 nack plia=fmtp:124 level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=4d001fa=rtpmap:120 rtx/90000a=fmtp:120 apt=124a=rtpmap:39 H264/90000a=rtcp-fb:39 goog-remba=rtcp-fb:39 transport-cca=rtcp-fb:39 ccm fira=rtcp-fb:39 nacka=rtcp-fb:39 nack plia=fmtp:39 level-asymmetry-allowed=1;packetization-mode=0;profile-level-id=4d001fa=rtpmap:40 rtx/90000a=fmtp:40 apt=39a=rtpmap:45 AV1/90000a=rtcp-fb:45 goog-remba=rtcp-fb:45 transport-cca=rtcp-fb:45 ccm fira=rtcp-fb:45 nacka=rtcp-fb:45 nack plia=rtpmap:46 rtx/90000a=fmtp:46 apt=45a=rtpmap:98 VP9/90000a=rtcp-fb:98 goog-remba=rtcp-fb:98 transport-cca=rtcp-fb:98 ccm fira=rtcp-fb:98 nacka=rtcp-fb:98 nack plia=fmtp:98 profile-id=0a=rtpmap:99 rtx/90000a=fmtp:99 apt=98a=rtpmap:100 VP9/90000a=rtcp-fb:100 goog-remba=rtcp-fb:100 transport-cca=rtcp-fb:100 ccm fira=rtcp-fb:100 nacka=rtcp-fb:100 nack plia=fmtp:100 profile-id=2a=rtpmap:101 rtx/90000a=fmtp:101 apt=100a=rtpmap:123 H264/90000a=rtcp-fb:123 goog-remba=rtcp-fb:123 transport-cca=rtcp-fb:123 ccm fira=rtcp-fb:123 nacka=rtcp-fb:123 nack plia=fmtp:123 level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=64001fa=rtpmap:119 rtx/90000a=fmtp:119 apt=123a=rtpmap:114 red/90000a=rtpmap:115 rtx/90000a=fmtp:115 apt=114a=rtpmap:116 ulpfec/90000a=ssrc-group:FID 3686128283 1675289484a=ssrc:3686128283 cname:L8DTZMOJip3IXAHsa=ssrc:3686128283 msid:fbiSmx6n3xm4yA1FQtKj8zH9yGBv7vmGoVai 1327213e-5d74-461f-be50-ed78ede6a453a=ssrc:1675289484 cname:L8DTZMOJip3IXAHsa=ssrc:1675289484 msid:fbiSmx6n3xm4yA1FQtKj8zH9yGBv7vmGoVai 1327213e-5d74-461f-be50-ed78ede6a453"
}
}
  1. 拿到ICE Candidate后也发送soac事件,携带的数据格式为:

    1. id是发布流成功ACK里返回的id。
    2. signaling的"type":"candidate"表示发送的是ICE Candidate消息。
    3. sdpMLineIndex、sdpMid和candidate字段分别是IceCandidate类的sdpMLineIndex、sdpMid和sdp成员(在前面拼接a=)。
{"id":"eceda0bafe3e4a1eab1a75056ede3cd8","signaling":{"type":"candidate","candidate":{"candidate":"a=candidate:2722325311 1 udp 2122265343 fc00:2::e19b:dc32:443d:dd84 55478 typ host generation 0 ufrag gdMS network-id 2","sdpMid":"0","sdpMLineIndex":0}}
}

1. 客户端代码

  1. 部分代码如下:
  async publish(stream, options, videoCodecs) {...return this._signaling.sendSignalingMessage('publish', {media: {tracks: trackOptions},attributes: stream.attributes,transport: {id: this._id, type: 'webrtc'},}).catch((e) => {// Send SDP even when failed to get Answer.this._signaling.sendSignalingMessage('soac', {id: this._id,signaling: localDesc,});throw e;});}).then((data) => {...this._signaling.sendSignalingMessage('soac', {id: this._id,signaling: localDesc,});}).catch((e) => {...}
  1. 客户端调用栈:
src/samples/conference/public/scripts/index.js conference.publish(localStream, ... =>
src/sdk/conference/client.js this.publish =>
src/sdk/conference/channel.js async publish =>
src/sdk/conference/channel.js _signaling.sendSignalingMessage('soac', =>

2. 服务端代码

  1. 服务端会接收soac信令,接收代码:
    socket.on('soac', function(SOAC, callback) {if(!that.inRoom){return safeCall(callback, 'error', 'Illegal request');}return validateSOAC(SOAC).then((soac) => {return portal.onSessionSignaling(clientId, soac.id, soac.signaling);}).then((result) => {safeCall(callback, 'ok');}).catch(onError('soac', callback));});};
  1. 相关调用栈:
source/portal/v11Client.js socket.on('soac', function(SOAC, callback) =>
source/portal/portal.js that.onSessionSignaling = function(participantId, ... =>
source/portal/rpcRequest.js that.onSessionSignaling = function(controller, sessionId, ... =>
source/agent/conference/conference.js that.onSessionSignaling = function(sessionId, signaling, ... =>
source/agent/conference/rtcController.js onClientTransportSignaling(transportId, signaling) =>
source/agent/conference/rpcRequest.js that.onTransportSignaling = function(accessNode, ... =>
source/agent/webrtc/index.js that.onTransportSignaling = function (connectionId, msg, callback) =>
source/agent/webrtc/wrtcConnection.js that.onSignalling = function (msg, operationId) =>
  1. source/agent/webrtc/wrtcConnection.js的that.onSignalling函数中,会根据msg的type是offer/candidate/removed-candidates进行不同操作。

    1. 如果type是offer,会处理Offer SDP,最后发送answer类型的on_status消息,在initWebRtcConnection函数中接收。
  that.onSignalling = function (msg, operationId) {var processSignalling = function () {if (msg.type === 'offer') {log.debug('on offer:', msg.sdp);processOffer(msg.sdp);} else if (msg.type === 'candidate') {wrtc.addRemoteCandidate(msg.candidate);} else if (msg.type === 'removed-candidates') {wrtc.removeRemoteCandidates(msg.candidates);}};if (wrtc) {processSignalling();} else {// should not reach herelog.error('wrtc is not ready');}};

5. SocketIO接收Answer

  1. OWT Server收到客户端发送的Offer后,会通过progress事件返回Answer,携带的数据格式为:

    1. id是发布流成功ACK里返回的id。
    2. “status”:"soac"表明是返回的SDP消息,还有其他类型的消息,比如流发布或订阅成功的ready消息、错误error消息等。
    3. data的"type":"answer"表示发送的是Answer消息。
    4. sdp字段为Answer内容,内容较长,这里省略了。
[{"id":"62b95bd27ff8054480cbb73a","status":"soac","data":{"type":"answer","sdp":"v=0\ro=- 7416128173695703170 3 IN IP4 127.0.0.1s=\r-t=0 0...省略"}}
]

6. SocketIO订阅流

  1. 客户端B创建token、登录的过程和客户端A一样,但是B登录ACK返回的room字段的participants子字段会包含A、B两个用户的信息,streams子字段除了MCU混合的流,还会有SFU转发A的流。拿到A用户的这两个信息之后,我们就可以订阅A的流了。
  2. 订阅流需要客户端发送subscribe事件,携带的数据格式为:
    1. audio和video的from都是想要订阅的流的id,即登录ACK返回的room.streams的id字段。
{"media":{"tracks":[{"type":"audio","mid":"0","from":"62b95bd27ff8054480cbb73a-common"},{"type":"video","mid":"1","from":"62b95bd27ff8054480cbb73a-common"}]},"transport":{"type":"webrtc"}
}
  1. 订阅成功的ACK的格式为:
{"id":"55aa9331e2b741338639d282f0bffa0d"
}
  1. id是订阅(subscription)的id。
  2. 之后发送Offer和ICE Candidate,以及接收Answer,格式和发布流时的格式都一样,但使用的id值都是这里订阅ACK的id。
  3. 客户端B如果要发布流,那么它的信令流程和A的流程完全一致。发布成功后,A会收到消息通知,这样A也就可以订阅B发布的流。A订阅B的过程和上面描述的B订阅A的过程完全一致。

1. 客户端代码

  1. 部分代码如下:
    window.onload = function() {var simulcast = getParameterByName('simulcast') || false;var shareScreen = getParameterByName('screen') || false;myRoom = getParameterByName('room');var isHttps = (location.protocol === 'https:');var mediaUrl = getParameterByName('url');var isPublish = getParameterByName('publish');createToken(myRoom, 'user', 'presenter', function(response) {var token = response;conference.join(token).then(resp => {...var streams = resp.remoteStreams;for (const stream of streams) {if(!subscribeForward){if (stream.source.audio === 'mixed' || stream.source.video ==='mixed') {subscribeAndRenderVideo(stream);}} else if (stream.source.audio !== 'mixed') {subscribeAndRenderVideo(stream);}}console.log('Streams in conference:', streams.length);var participants = resp.participants;console.log('Participants in conference: ' + participants.length);});}, serverUrlBase);};
  1. publish客户端代码调用栈为:
src/samples/conference/public/scripts/index.js subscribeAndRenderVideo(stream) =>
src/samples/conference/public/scripts/index.js conference.subscribe(stream) =>
src/sdk/conference/client.js this.subscribe =>
src/sdk/conference/channel.js async subscribe(stream, options)
src/sdk/conference/channel.js _signaling.sendSignalingMessage('subscribe', =>

2. 服务端代码

  1. 服务端接收消息代码如下:
    socket.on('subscribe', function(subReq, callback) {if(!that.inRoom){return safeCall(callback, 'error', 'Illegal request');}var subscription_id = Math.round(Math.random() * 1000000000000000000) + '';return validateSubReq(subReq).then((req) => {req.type = 'webrtc';//FIXME: For backend compatibility with v3.4 clients.return portal.subscribe(clientId, subscription_id, req);}).then((result) => {safeCall(callback, 'ok', {id: subscription_id});}).catch(onError('subscribe', callback));});
  1. 调用栈:
source/portal/v11Client.js socket.on('subscribe', function(subReq, =>
source/portal/portal.js that.subscribe = function(participantId, ... =>
source/portal/rpcRequest.js that.publish = function(controller, participantId, streamId, Options) =>
source/agent/conference/conference.js that.subscribe = function(controller, participantId, ... =>
source/agent/conference/rtcController.js initiate(ownerId, sessionId, direction, origin, ... =>
source/agent/conference/rpcRequest.js that.initiate = function(accessNode, sessionId, ... => //向WebRTC Agent的node发起publish RPC
  1. WebRTC Agent的RPC服务定义在webrtc/index.js中。
source/agent/conference/rpcRequest.js that.initiate = function(accessNode, sessionId, ... => //向WebRTC Agent的node发起publish RPC
source/agent/webrtc/index.js that.subscribe = function (operationId, connectionType, ... =>
source/agent/webrtc/index.js createWebRTCConnection = function (transportId, ... => //创建WrtcConnection(`webrtc/wrtcConnection.js`),它负责和C++ 代码进行交互
  1. 在创建WrtcConnection时会执行webrtc/wrtcConnection.js的initWebRtcConnection函数,其中会监听C++代码发送的事件(由webrtc/connection.js做了一层翻译,变成status_event事件),收到事件后会回调给webrtc/index.js的notifyStatus函数,向Conference Agent的node发起onSessionProgress RPC。
  /** Given a WebRtcConnection waits for the state CANDIDATES_GATHERED for set remote SDP.*/var initWebRtcConnection = function (wrtc) {wrtc.on('status_event', (evt, status) => {if (evt.type === 'answer') {processAnswer(evt.sdp);const message = localSdp.toString();log.debug('Answer SDP', message);on_status({type: 'answer', sdp: message});} else if (evt.type === 'candidate') {let message = evt.candidate;networkInterfaces.forEach((i) => {if (i.ip_address && i.replaced_ip_address) {message = message.replace(new RegExp(i.ip_address, 'g'), i.replaced_ip_address);}});on_status({type: 'candidate', candidate: message});} else if (evt.type === 'failed') {log.warn('ICE failed, ', status, wrtc.id);on_status({type: 'failed', reason: 'Ice procedure failed.'});} else if (evt.type === 'ready') {log.debug('Connection ready, ', wrtc.wrtcId);on_status({type: 'ready'});}});wrtc.init(wrtcId);};

7. SocketIO接收其他用户的通知

1. 其他用户登录通知

  1. 其他用户登录后,服务端会通过participant事件发布通知,携带的数据格式为:
{"action":"join","data":{"id":"1d6pEzr67tkUhEJeAAAn","user":"user","role":"presenter"}
}
  1. 调用栈:
source/portal/socketIOServer.js socket.on('login', function(login_info, ... =>
source/portal/v11Client.js that.join = (token) =>
source/portal/portal.js that.join = function(participantId, token) =>
source/portal/rpcRequest.js that.join = function(controller, roomId, participant) =>
source/agent/conference/conference.js that.join = function(roomId, participantInfo, callback) =>
source/agent/conference/conference.js  initRoom = function(roomId, origin) => //保存房间信息,创建roomController、accessController
source/agent/conference/conference.js  addParticipant = function(participantInfo, permission) => //保存用户信息
  1. addParticipant函数在保存用户信息后会向其他参与者发布通知。

2. 其他用户发布stream通知

  1. 其他用户发布流后,服务端会通过stream事件发布通知,携带的数据格式为:
{"id":"55aa9331e2b741338639d282f0bffa0d","status":"add","data":{"id":"55aa9331e2b741338639d282f0bffa0d","type":"forward","media":{},"info":{"owner":"kYmJHa8XsioNXhKUAAAJ","type":"webrtc","inViews":[]}}
}
  1. “status”:"add"表明是其他用户新发布了流。data字段的各个子字段、含义和登录ACK的streams的各个子字段含义一致。

3. 用户退出登录通知

  1. 其他用户退出登录后,服务端也会通过participant事件发布通知,携带的数据格式为:
{"action":"leave","data":"I-Fv7fQ8Qe9_9qa2AAAG"
}
  1. “action”:"leave"表明是其他用户退出登录了。
  2. data字段为登录ACK的participants的id子字段。
  3. 如果是用user字段标识用户,就需要维护id到user的映射关系。

8. SocketIO退出登录

  1. 如果需要停止发布和订阅所有的流,就可以发送logout事件退出登录,携带的数据为空字符串。退出登录后,其他客户端将会收到通知。通知携带的数据为:
{"action":"leave","data":"kYmJHa8XsioNXhKUAAAJ"
}

1. 客户端代码

  1. 客户端部分代码:
  /*** @function leave* @memberOf Owt.Conference.ConferenceClient* @instance* @desc Leave a conference.* @return {Promise<void, Error>} Returned promise will be resolved with undefined once the connection is disconnected.*/this.leave = function() {return signaling.disconnect().then(() => {clean();signalingState = SignalingState.READY;});};
  1. 当离开页面时会触发,然后调用leave函数,最终会向owt server发送logout事件。
  /*** @function disconnect* @instance* @desc Disconnect from a portal.* @memberof Oms.Conference.SioSignaling* @return {Promise<Object, Error>} Return a promise resolved with the data returned by portal if successfully disconnected. Or return a promise rejected with a newly created Oms.Error if failed.* @private.*/disconnect() {if (!this._socket || this._socket.disconnected) {return Promise.reject(new ConferenceError('Portal is not connected.'));}return new Promise((resolve, reject) => {this._socket.emit('logout', (status, data) => {// Maximize the reconnect times to disable reconnection.this._reconnectTimes = reconnectionAttempts;this._socket.disconnect();handleResponse(status, data, resolve, reject);});});}
  1. 调用栈:
src/samples/conference/public/scripts/index.js window.onbeforeunload = function(... =>
src/sdk/conference/client.js this.leave = function() =>
src/sdk/conference/signaling.js disconnect()

2. 服务端代码

  1. owt server会接收logout事件,代码如下:
    socket.on('logout', function(callback){reconnection.enabled = false;state = 'initialized';if (client_id) {forceClientLeave();safeCall(callback, okWord());} else {return safeCall(callback, 'error', 'Illegal request');}});
  1. 调用栈为:
source/portal/socketIOServer.js socket.on('logout', function(callback) =>
source/portal/socketIOServer.js forceClientLeave = () =>
source/portal/v11Client.js that.leave = () =>
source/portal/portal.js that.leave = function(participantId) =>
source/portal/rpcRequest.js that.leave = function(controller, participantId) =>
source/agent/conference/conference.js that.leave = function(participantId, callback) =>

9. Offer和ICE Candidate顺序

  1. OWT Server服务端也没有处理Offer和ICE Candidate的先后顺序,所以需要客户端确保先发送Offer再发送ICE Candidate,否则先收到的ICE Candidate会被丢弃,进而可能导致连接失败。
  2. 当网络连接发生错误时,可能SocketIO会断开连接,或者PC会断开连接,比较简单的做法是在断开连接时自动关闭所有PC,重新建立SocketIO连接,连接成功后发送relogin事件并带上reconnectionTicket的内容。relogin的ACK和login的ACK格式相同,这样之后的流程就可以按照login处理了。
  3. reconnectionTicket只有一定的有效期,我们需要在过期之前进行刷新。发送refreshReconnectionTicket事件,携带的数据为空字符串,返回的ACK格式为:
["ok","<新的reconnectionTicket>"
]
  1. 重连逻辑可以进一步优化,比如没有发生错误的连接就无须断开重连了。

HTTP GET用户和流列表

  1. 除了通过SocketIO接收其他用户加入或离开的事件,还可以通过HTTP接口主动获取完整的用户和流列表,获取用户列表的接口为GET /rooms//participants,响应的格式为:
[{"id": "XHDK7_8QXJjazcwuAAAx","user": "user","role": "presenter","permission": {"publish": {"video": true,"audio": true},"subscribe": {"video": true,"audio": true}}},{"id": "nOqTKNJrCgHEUAlaAAAz","user": "user","role": "presenter","permission": {"publish": {"video": true,"audio": true},"subscribe": {"video": true,"audio": true}}}
]
  1. 获取流列表的接口为GET /rooms//streams,响应的格式为:
[{"id":"62b95bd27ff8054480cbb73a-common","type":"mixed","media":{"audio":{"status":"active","format":{"codec":"opus","sampleRate":48000,"channelNum":2},"optional":{"format":[{"codec":"isac","sampleRate":16000},{"codec":"isac","sampleRate":32000},{"codec":"g722","sampleRate":16000,"channelNum":1},{"codec":"pcma"},{"codec":"pcmu"},{"codec":"aac","sampleRate":48000,"channelNum":2},{"codec":"ac3"},{"codec":"nellymoser"},{"codec":"ilbc"}]}},"video":{"status":"active","optional":{"format":[{"codec":"h264","profile":"CB"},{"codec":"h264","profile":"B"},{"codec":"vp9"}],"parameters":{"resolution":[{"width":480,"height":360},{"width":426,"height":320},{"width":320,"height":240},{"width":212,"height":160},{"width":160,"height":120},{"width":352,"height":288}],"bitrate":["x0.8","x0.6","x0.4","x0.2"],"framerate":[6,12,15],"keyFrameInterval":[100,30,5,2,1]}},"format":{"codec":"vp8"},"parameters":{"resolution":{"width":640,"height":480},"framerate":24,"keyFrameInterval":100}}},"data":null,"info":{"label":"common","activeInput":"961f75a4fac8435f9a54506d25150784","layout":[{"stream":"961f75a4fac8435f9a54506d25150784","region":{"id":"1","shape":"rectangle","area":{"left":"0/1","top":"0/1","width":"1/1","height":"1/1"}}}],"origin":{"isp":"isp","region":"region"}}},{"id":"961f75a4fac8435f9a54506d25150784","type":"forward","media":{"audio":{"status":"active","source":"mic","format":{"codec":"opus","sampleRate":48000,"channelNum":2},"optional":{"format":[{"codec":"isac","sampleRate":16000},{"codec":"isac","sampleRate":32000},{"codec":"g722","sampleRate":16000,"channelNum":1},{"codec":"pcma"},{"codec":"pcmu"},{"codec":"aac","sampleRate":48000,"channelNum":2},{"codec":"ac3"},{"codec":"nellymoser"},{"codec":"ilbc"}]}},"video":{"status":"active","source":"camera","optional":{"format":[{"codec":"h264","profile":"CB"},{"codec":"h264","profile":"B"},{"codec":"vp9"}],"parameters":{"resolution":[{"width":480,"height":360},{"width":426,"height":320},{"width":320,"height":240},{"width":212,"height":160},{"width":160,"height":120},{"width":352,"height":288}],"bitrate":["x0.8","x0.6","x0.4","x0.2"],"framerate":[6,12,15,24],"keyFrameInterval":[100,30,5,2,1]}},"format":{"codec":"vp8"},"parameters":{"resolution":{"width":640,"height":480}}}},"info":{"owner":"XHDK7_8QXJjazcwuAAAx","type":"webrtc","inViews":["common"]}}
]
  1. 其他REST API接口请看README:OWT REST API

OWT Server信令分析 (下) [Open WebRTC Toolkit]相关推荐

  1. Open WebRTC Toolkit实时视频分析系统

    随着物联网技术的发展,实时视频分析技术已应用于智能物联网的各个领域.英特尔基于与GStreamer以及OpenVINO构建了整套实时视频分析方案,为用户提供更加灵活.便捷的实时视频分析服务.本文由英特 ...

  2. 基于Open WebRTC Toolkit(OWT)的8K全景视频低延时直播系统

    photo from Ready Player One 随着5G技术的发展,其高带宽.超低延时的特性为高分辨率全景视频的实现带来了更多的可能.本文来自Open WebRTC Toolkit (OWT) ...

  3. OWT Server 整体架构分析

    基础模块架构图 按自己的理解用 visio 大体画了一下. 前一篇博客讲过OWT Server是模块化的设计,不同模块可以分开部署,便于新手入门,下面画了OWT Server的一些基础模块(支持Web ...

  4. OWT (Open WebRTC Toolkit) 5.0 初体验与开发环境搭建

    介绍 OWT是Intel前些年开源的基于互联网的视频会议解决方案,可以支持WebRTC和SIP终端.这几年WebRTC应用的特别广泛,使用OWT可以快速搭建一个WebRTC视频会议系统.OWT最初仅支 ...

  5. Janus流媒体服务器信令分析

    Janus流媒体服务器信令分析 目录 房间配置文件 API分类 Video Room房间管理 VideoRoom Publishers VideoRoom Subscribers video room ...

  6. CentOS下配置webrtc服务器

    主要参考:Ubuntu下配置webrtc服务器 ------------------------------------------------ 原理介绍:<以下文字copy自上文> 房间 ...

  7. Ubuntu 14.04下编译WebRTC

    阿里云  >  教程中心   >  python教程  >  Ubuntu 14.04下编译WebRTC Ubuntu 14.04下编译WebRTC 发布时间:2018-02-28 ...

  8. ubuntu server 10.4下LAMP的安装

    前言: Apache,Mysql,php结合在一起常用在三种环境,一种是微软的windows server 下的AMP,就把它叫WAMP:另一种当然就是GNU/LINUX下的,都叫它LAMP:还可以用 ...

  9. SQL Server Performance 分析

    对网络上的一篇博客做下笔记,适当扩展下对 Performance 各个涉及到的要素.这篇文章讲的是分析性能,老外写的: How to analyse SQL Server performance 主要 ...

最新文章

  1. Ubuntu11.04软件源--增强版
  2. 北京赛区总结,以及。。。
  3. stm32F105的can2问题
  4. 区块链BaaS云服务(2)亚马逊 Amazon Managed Blockchain
  5. 音频处理七:(极坐标转换)
  6. getordefault java_Java map.getOrDefault()方法的用法详解
  7. (部分转载,部分原创)java大数类(2)
  8. 我的世界基岩版种子和java版种子_我的世界:对萌新最友好的种子,基岩版通用,对老玩家也很适合!...
  9. java 反射抽象_Java实现抽象工厂模式+java的反射的机制
  10. POI操作Excel表格相关API说明
  11. 网易有道最新力作 有道词典笔3 结构拆解
  12. java 视频截图_获取视频截图
  13. win10装机之天涯若比邻长时间卡死
  14. mysql的填充因子_SQL Server表索引:调整填充因子
  15. 身份证校验码程序c#
  16. 令人头痛的WH_CBT钩子,使窗口前置——泪水+汗水的赞歌
  17. java中的字符retry: 是什么?
  18. arduino使用晶联讯jlx12864
  19. golang多版本管理工具g(gvm)使用(windows)
  20. linux boost库安装

热门文章

  1. 车间制造管理系统(上)
  2. easyexcel操作遇到的坑
  3. android 聊天气泡背景图片,关于实现微信聊天气泡里显示图片
  4. css解决图片缩小变模糊问题
  5. eCharts——柱状图中的柱体颜色渐变
  6. CSS 样式属性大全
  7. 腾讯位置服务---->(小程序简单使用+显示附近WC步行路线)
  8. ux和ui_UI和UX设计人员的47个关键课程
  9. 读书笔记:《狼图腾》
  10. 使用 Abp.Zero 搭建第三方登录模块(四):微信小程序开发