您最多选择25个主题 主题必须以字母或数字开头,可以包含连字符 (-),并且长度不得超过35个字符

BridgeChannel.js 15KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436
  1. import { getLogger } from '@jitsi/logger';
  2. import RTCEvents from '../../service/RTC/RTCEvents';
  3. import { createBridgeChannelClosedEvent } from '../../service/statistics/AnalyticsEvents';
  4. import Statistics from '../statistics/statistics';
  5. import GlobalOnErrorHandler from '../util/GlobalOnErrorHandler';
  6. const logger = getLogger(__filename);
  7. /**
  8. * Handles a WebRTC RTCPeerConnection or a WebSocket instance to communicate
  9. * with the videobridge.
  10. */
  11. export default class BridgeChannel {
  12. /**
  13. * Binds "ondatachannel" event listener on the given RTCPeerConnection
  14. * instance, or creates a WebSocket connection with the videobridge.
  15. * At least one of both, peerconnection or wsUrl parameters, must be
  16. * given.
  17. * @param {RTCPeerConnection} [peerconnection] WebRTC peer connection
  18. * instance.
  19. * @param {string} [wsUrl] WebSocket URL.
  20. * @param {EventEmitter} emitter the EventEmitter instance to use for event emission.
  21. * @param {JitsiConference} conference the conference instance.
  22. */
  23. constructor(peerconnection, wsUrl, emitter, conference) {
  24. if (!peerconnection && !wsUrl) {
  25. throw new TypeError('At least peerconnection or wsUrl must be given');
  26. } else if (peerconnection && wsUrl) {
  27. throw new TypeError('Just one of peerconnection or wsUrl must be given');
  28. }
  29. if (peerconnection) {
  30. logger.debug('constructor() with peerconnection');
  31. } else {
  32. logger.debug(`constructor() with wsUrl:"${wsUrl}"`);
  33. }
  34. // The underlying WebRTC RTCDataChannel or WebSocket instance.
  35. // @type {RTCDataChannel|WebSocket}
  36. this._channel = null;
  37. // The conference that uses this bridge channel.
  38. this._conference = conference;
  39. // Whether the channel is connected or not. It will start as undefined
  40. // for the first connection attempt. Then transition to either true or false.
  41. this._connected = undefined;
  42. // @type {EventEmitter}
  43. this._eventEmitter = emitter;
  44. // Whether a RTCDataChannel or WebSocket is internally used.
  45. // @type {string} "datachannel" / "websocket"
  46. this._mode = null;
  47. // Indicates whether the connection retries are enabled or not.
  48. this._areRetriesEnabled = false;
  49. // Indicates whether the connection was closed from the client or not.
  50. this._closedFromClient = false;
  51. // If a RTCPeerConnection is given, listen for new RTCDataChannel
  52. // event.
  53. if (peerconnection) {
  54. const datachannel
  55. = peerconnection.createDataChannel(
  56. 'JVB data channel', {
  57. protocol: 'http://jitsi.org/protocols/colibri'
  58. });
  59. // Handle the RTCDataChannel.
  60. this._handleChannel(datachannel);
  61. this._mode = 'datachannel';
  62. // Otherwise create a WebSocket connection.
  63. } else if (wsUrl) {
  64. this._areRetriesEnabled = true;
  65. this._wsUrl = wsUrl;
  66. this._initWebSocket();
  67. }
  68. }
  69. /**
  70. * Initializes the web socket channel.
  71. *
  72. * @returns {void}
  73. */
  74. _initWebSocket() {
  75. // Create a WebSocket instance.
  76. const ws = new WebSocket(this._wsUrl);
  77. // Handle the WebSocket.
  78. this._handleChannel(ws);
  79. this._mode = 'websocket';
  80. }
  81. /**
  82. * Starts the websocket connection retries.
  83. *
  84. * @returns {void}
  85. */
  86. _startConnectionRetries() {
  87. let timeoutS = 1;
  88. const reload = () => {
  89. const isConnecting = this._channel && (this._channel.readyState === 'connecting'
  90. || this._channel.readyState === WebSocket.CONNECTING);
  91. // Should not spawn new websockets while one is already trying to connect.
  92. if (isConnecting) {
  93. // Timeout is still required as there is flag `_areRetriesEnabled` that
  94. // blocks new retrying cycles until any channel opens in current cycle.
  95. this._retryTimeout = setTimeout(reload, timeoutS * 1000);
  96. return;
  97. }
  98. if (this.isOpen()) {
  99. return;
  100. }
  101. this._initWebSocket(this._wsUrl);
  102. timeoutS = Math.min(timeoutS * 2, 60);
  103. this._retryTimeout = setTimeout(reload, timeoutS * 1000);
  104. };
  105. this._retryTimeout = setTimeout(reload, timeoutS * 1000);
  106. }
  107. /**
  108. * Stops the websocket connection retries.
  109. *
  110. * @returns {void}
  111. */
  112. _stopConnectionRetries() {
  113. if (this._retryTimeout) {
  114. clearTimeout(this._retryTimeout);
  115. this._retryTimeout = undefined;
  116. }
  117. }
  118. /**
  119. * Retries to establish the websocket connection after the connection was closed by the server.
  120. *
  121. * @param {CloseEvent} closeEvent - The close event that triggered the retries.
  122. * @returns {void}
  123. */
  124. _retryWebSocketConnection(closeEvent) {
  125. if (!this._areRetriesEnabled) {
  126. return;
  127. }
  128. const { code, reason } = closeEvent;
  129. Statistics.sendAnalytics(createBridgeChannelClosedEvent(code, reason));
  130. this._areRetriesEnabled = false;
  131. this._eventEmitter.once(RTCEvents.DATA_CHANNEL_OPEN, () => {
  132. this._stopConnectionRetries();
  133. this._areRetriesEnabled = true;
  134. });
  135. this._startConnectionRetries();
  136. }
  137. /**
  138. * The channel mode.
  139. * @return {string} "datachannel" or "websocket" (or null if not yet set).
  140. */
  141. get mode() {
  142. return this._mode;
  143. }
  144. /**
  145. * Closes the currently opened channel.
  146. */
  147. close() {
  148. this._closedFromClient = true;
  149. this._stopConnectionRetries();
  150. this._areRetriesEnabled = false;
  151. if (this._channel) {
  152. try {
  153. this._channel.close();
  154. } catch (error) {} // eslint-disable-line no-empty
  155. this._channel = null;
  156. }
  157. }
  158. /**
  159. * Whether there is an underlying RTCDataChannel or WebSocket and it's
  160. * open.
  161. * @return {boolean}
  162. */
  163. isOpen() {
  164. return this._channel && (this._channel.readyState === 'open'
  165. || this._channel.readyState === WebSocket.OPEN);
  166. }
  167. /**
  168. * Sends local stats via the bridge channel.
  169. * @param {Object} payload The payload of the message.
  170. * @throws NetworkError/InvalidStateError/Error if the operation fails or if there is no data channel created.
  171. */
  172. sendEndpointStatsMessage(payload) {
  173. this._send({
  174. colibriClass: 'EndpointStats',
  175. ...payload
  176. });
  177. }
  178. /**
  179. * Sends message via the channel.
  180. * @param {string} to The id of the endpoint that should receive the
  181. * message. If "" the message will be sent to all participants.
  182. * @param {object} payload The payload of the message.
  183. * @throws NetworkError or InvalidStateError from RTCDataChannel#send (@see
  184. * {@link https://developer.mozilla.org/docs/Web/API/RTCDataChannel/send})
  185. * or from WebSocket#send or Error with "No opened channel" message.
  186. */
  187. sendMessage(to, payload) {
  188. this._send({
  189. colibriClass: 'EndpointMessage',
  190. msgPayload: payload,
  191. to
  192. });
  193. }
  194. /**
  195. * Sends a "lastN value changed" message via the channel.
  196. * @param {number} value The new value for lastN. -1 means unlimited.
  197. */
  198. sendSetLastNMessage(value) {
  199. logger.log(`Sending lastN=${value}.`);
  200. this._send({
  201. colibriClass: 'LastNChangedEvent',
  202. lastN: value
  203. });
  204. }
  205. /**
  206. * Sends a 'ReceiverVideoConstraints' message via the bridge channel.
  207. *
  208. * @param {ReceiverVideoConstraints} constraints video constraints.
  209. */
  210. sendReceiverVideoConstraintsMessage(constraints) {
  211. logger.log(`Sending ReceiverVideoConstraints with ${JSON.stringify(constraints)}`);
  212. this._send({
  213. colibriClass: 'ReceiverVideoConstraints',
  214. ...constraints
  215. });
  216. }
  217. /**
  218. * Sends a 'SourceVideoTypeMessage' message via the bridge channel.
  219. *
  220. * @param {BridgeVideoType} videoType - the video type.
  221. * @param {SourceName} sourceName - the source name of the video track.
  222. * @returns {void}
  223. */
  224. sendSourceVideoTypeMessage(sourceName, videoType) {
  225. logger.info(`Sending SourceVideoTypeMessage with video type ${sourceName}: ${videoType}`);
  226. this._send({
  227. colibriClass: 'SourceVideoTypeMessage',
  228. sourceName,
  229. videoType
  230. });
  231. }
  232. /**
  233. * Set events on the given RTCDataChannel or WebSocket instance.
  234. */
  235. _handleChannel(channel) {
  236. const emitter = this._eventEmitter;
  237. channel.onopen = () => {
  238. logger.info(`${this._mode} channel opened`);
  239. this._connected = true;
  240. emitter.emit(RTCEvents.DATA_CHANNEL_OPEN);
  241. };
  242. channel.onerror = event => {
  243. // WS error events contain no information about the failure (this is available in the onclose event) and
  244. // the event references the WS object itself, which causes hangs on mobile.
  245. if (this._mode !== 'websocket') {
  246. logger.error(`Channel error: ${event.message}`);
  247. }
  248. };
  249. channel.onmessage = ({ data }) => {
  250. // JSON object.
  251. let obj;
  252. try {
  253. obj = JSON.parse(data);
  254. } catch (error) {
  255. GlobalOnErrorHandler.callErrorHandler(error);
  256. logger.error('Failed to parse channel message as JSON: ', data, error);
  257. return;
  258. }
  259. const colibriClass = obj.colibriClass;
  260. switch (colibriClass) {
  261. case 'DominantSpeakerEndpointChangeEvent': {
  262. const { dominantSpeakerEndpoint, previousSpeakers = [], silence } = obj;
  263. logger.debug(`Dominant speaker: ${dominantSpeakerEndpoint}, previous speakers: ${previousSpeakers}`);
  264. emitter.emit(RTCEvents.DOMINANT_SPEAKER_CHANGED, dominantSpeakerEndpoint, previousSpeakers, silence);
  265. break;
  266. }
  267. case 'EndpointConnectivityStatusChangeEvent': {
  268. const endpoint = obj.endpoint;
  269. const isActive = obj.active === 'true';
  270. logger.info(`Endpoint connection status changed: ${endpoint} active=${isActive}`);
  271. emitter.emit(RTCEvents.ENDPOINT_CONN_STATUS_CHANGED, endpoint, isActive);
  272. break;
  273. }
  274. case 'EndpointMessage': {
  275. emitter.emit(RTCEvents.ENDPOINT_MESSAGE_RECEIVED, obj.from, obj.msgPayload);
  276. break;
  277. }
  278. case 'EndpointStats': {
  279. emitter.emit(RTCEvents.ENDPOINT_STATS_RECEIVED, obj.from, obj);
  280. break;
  281. }
  282. case 'ForwardedSources': {
  283. const forwardedSources = obj.forwardedSources;
  284. logger.info(`New forwarded sources: ${forwardedSources}`);
  285. emitter.emit(RTCEvents.FORWARDED_SOURCES_CHANGED, forwardedSources);
  286. break;
  287. }
  288. case 'SenderSourceConstraints': {
  289. if (typeof obj.sourceName === 'string' && typeof obj.maxHeight === 'number') {
  290. logger.info(`SenderSourceConstraints: ${obj.sourceName} - ${obj.maxHeight}`);
  291. emitter.emit(RTCEvents.SENDER_VIDEO_CONSTRAINTS_CHANGED, obj);
  292. } else {
  293. logger.error(`Invalid SenderSourceConstraints: ${obj.sourceName} - ${obj.maxHeight}`);
  294. }
  295. break;
  296. }
  297. case 'ServerHello': {
  298. logger.info(`Received ServerHello, version=${obj.version}.`);
  299. break;
  300. }
  301. case 'VideoSourcesMap': {
  302. logger.info(`Received VideoSourcesMap: ${JSON.stringify(obj.mappedSources)}`);
  303. emitter.emit(RTCEvents.VIDEO_SSRCS_REMAPPED, obj);
  304. break;
  305. }
  306. case 'AudioSourcesMap': {
  307. logger.info(`Received AudioSourcesMap: ${JSON.stringify(obj.mappedSources)}`);
  308. emitter.emit(RTCEvents.AUDIO_SSRCS_REMAPPED, obj);
  309. break;
  310. }
  311. default: {
  312. logger.debug('Channel JSON-formatted message: ', obj);
  313. // The received message appears to be appropriately formatted
  314. // (i.e. is a JSON object which assigns a value to the
  315. // mandatory property colibriClass) so don't just swallow it,
  316. // expose it to public consumption.
  317. emitter.emit(`rtc.datachannel.${colibriClass}`, obj);
  318. }
  319. }
  320. };
  321. channel.onclose = event => {
  322. logger.debug(`Channel closed by ${this._closedFromClient ? 'client' : 'server'}`);
  323. if (channel !== this._channel) {
  324. logger.debug('Skip close handler, channel instance is not equal to stored one');
  325. return;
  326. }
  327. // When the JVB closes the connection gracefully due to the participant being alone in the meeting it uses
  328. // code 1001. However, the same code is also used by Cloudflare when it terminates the ws. Therefore, check
  329. // for the number of remote participants in the call and abort retries only when the endpoint is the only
  330. // endpoint in the call.
  331. const isGracefulClose = this._closedFromClient
  332. || (event.code === 1001 && this._conference.getParticipantCount() === 1);
  333. if (!isGracefulClose) {
  334. const { code, reason } = event;
  335. logger.error(`Channel closed: ${code} ${reason}`);
  336. if (this._mode === 'websocket') {
  337. this._retryWebSocketConnection(event);
  338. // We only want to send this event the first time the failure happens.
  339. if (this._connected !== false) {
  340. emitter.emit(RTCEvents.DATA_CHANNEL_CLOSED, {
  341. code,
  342. reason
  343. });
  344. }
  345. }
  346. }
  347. this._connected = false;
  348. // Remove the channel.
  349. this._channel = null;
  350. };
  351. // Store the channel.
  352. this._channel = channel;
  353. }
  354. /**
  355. * Sends passed object via the channel.
  356. * @param {object} jsonObject The object that will be sent.
  357. * @throws NetworkError or InvalidStateError from RTCDataChannel#send (@see
  358. * {@link https://developer.mozilla.org/docs/Web/API/RTCDataChannel/send})
  359. * or from WebSocket#send or Error with "No opened channel" message.
  360. */
  361. _send(jsonObject) {
  362. const channel = this._channel;
  363. if (!this.isOpen()) {
  364. logger.error('Bridge Channel send: no opened channel.');
  365. throw new Error('No opened channel');
  366. }
  367. channel.send(JSON.stringify(jsonObject));
  368. }
  369. }