You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

ProxyConnectionService.js 10KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317
  1. /* globals $ */
  2. import { getLogger } from 'jitsi-meet-logger';
  3. import { $iq } from 'strophe.js';
  4. import * as MediaType from '../../service/RTC/MediaType';
  5. import VideoType from '../../service/RTC/VideoType';
  6. import RTC from '../RTC/RTC';
  7. import ProxyConnectionPC from './ProxyConnectionPC';
  8. import { ACTIONS } from './constants';
  9. const logger = getLogger(__filename);
  10. /**
  11. * Instantiates a new ProxyConnectionPC and ensures only one exists at a given
  12. * time. Currently it assumes ProxyConnectionPC is used only for screensharing
  13. * and assumes IQs to be used for communication.
  14. */
  15. export default class ProxyConnectionService {
  16. /**
  17. * Initializes a new {@code ProxyConnectionService} instance.
  18. *
  19. * @param {Object} options - Values to initialize the instance with.
  20. * @param {boolean} [options.convertVideoToDesktop] - Whether or not proxied
  21. * video should be returned as a desktop stream. Defaults to false.
  22. * @param {Object} [options.iceConfig] - The {@code RTCConfiguration} to use
  23. * for the peer connection.
  24. * @param {Function} options.onRemoteStream - Callback to invoke when a
  25. * remote video stream has been received and converted to a
  26. * {@code JitsiLocakTrack}. The {@code JitsiLocakTrack} will be passed in.
  27. * @param {Function} options.onSendMessage - Callback to invoke when a
  28. * message has to be sent (signaled) out. The arguments passed in are the
  29. * jid to send the message to and the message
  30. */
  31. constructor(options = {}) {
  32. /**
  33. * Holds a reference to the collection of all callbacks.
  34. *
  35. * @type {Object}
  36. */
  37. this._options = options;
  38. /**
  39. * The active instance of {@code ProxyConnectionService}.
  40. *
  41. * @type {ProxyConnectionPC|null}
  42. */
  43. this._peerConnection = null;
  44. // Bind event handlers so they are only bound once for every instance.
  45. this._onFatalError = this._onFatalError.bind(this);
  46. this._onSendMessage = this._onSendMessage.bind(this);
  47. this._onRemoteStream = this._onRemoteStream.bind(this);
  48. }
  49. /**
  50. * Parses a message object regarding a proxy connection to create a new
  51. * proxy connection or update and existing connection.
  52. *
  53. * @param {Object} message - A message object regarding establishing or
  54. * updating a proxy connection.
  55. * @param {Object} message.data - An object containing additional message
  56. * details.
  57. * @param {string} message.data.iq - The stringified iq which explains how
  58. * and what to update regarding the proxy connection.
  59. * @param {string} message.from - The message sender's full jid. Used for
  60. * sending replies.
  61. * @returns {void}
  62. */
  63. processMessage(message) {
  64. const peerJid = message.from;
  65. if (!peerJid) {
  66. return;
  67. }
  68. // If a proxy connection has already been established and messages come
  69. // from another peer jid then those messages should be replied to with
  70. // a rejection.
  71. if (this._peerConnection
  72. && this._peerConnection.getPeerJid() !== peerJid) {
  73. this._onFatalError(
  74. peerJid,
  75. ACTIONS.CONNECTION_ERROR,
  76. 'rejected'
  77. );
  78. return;
  79. }
  80. const iq = this._convertStringToXML(message.data.iq);
  81. const $jingle = iq && iq.find('jingle');
  82. const action = $jingle && $jingle.attr('action');
  83. if (action === ACTIONS.INITIATE) {
  84. this._peerConnection = this._createPeerConnection(peerJid, {
  85. isInitiator: false,
  86. receiveVideo: true
  87. });
  88. }
  89. // Truthy check for peer connection added to protect against possibly
  90. // receiving actions before an ACTIONS.INITIATE.
  91. if (this._peerConnection) {
  92. this._peerConnection.processMessage($jingle);
  93. }
  94. // Take additional steps to ensure the peer connection is cleaned up
  95. // if it is to be closed.
  96. if (action === ACTIONS.CONNECTION_ERROR
  97. || action === ACTIONS.UNAVAILABLE
  98. || action === ACTIONS.TERMINATE) {
  99. this._selfCloseConnection();
  100. }
  101. return;
  102. }
  103. /**
  104. * Instantiates and initiates a proxy peer connection.
  105. *
  106. * @param {string} peerJid - The jid of the remote client that should
  107. * receive messages.
  108. * @param {Array<JitsiLocalTrack>} localTracks - Initial media tracks to
  109. * send through to the peer.
  110. * @returns {void}
  111. */
  112. start(peerJid, localTracks = []) {
  113. this._peerConnection = this._createPeerConnection(peerJid, {
  114. isInitiator: true,
  115. receiveVideo: false
  116. });
  117. this._peerConnection.start(localTracks);
  118. }
  119. /**
  120. * Terminates any active proxy peer connection.
  121. *
  122. * @returns {void}
  123. */
  124. stop() {
  125. if (this._peerConnection) {
  126. this._peerConnection.stop();
  127. }
  128. this._peerConnection = null;
  129. }
  130. /**
  131. * Transforms a stringified xML into a XML wrapped in jQuery.
  132. *
  133. * @param {string} xml - The XML in string form.
  134. * @private
  135. * @returns {Object|null} A jQuery version of the xml. Null will be returned
  136. * if an error is encountered during transformation.
  137. */
  138. _convertStringToXML(xml) {
  139. try {
  140. const xmlDom = new DOMParser().parseFromString(xml, 'text/xml');
  141. return $(xmlDom);
  142. } catch (e) {
  143. logger.error('Attempted to convert incorrectly formatted xml');
  144. return null;
  145. }
  146. }
  147. /**
  148. * Helper for creating an instance of {@code ProxyConnectionPC}.
  149. *
  150. * @param {string} peerJid - The jid of the remote peer with which the
  151. * {@code ProxyConnectionPC} will be established with.
  152. * @param {Object} options - Additional defaults to instantiate the
  153. * {@code ProxyConnectionPC} with. See the constructor of ProxyConnectionPC
  154. * for more details.
  155. * @private
  156. * @returns {ProxyConnectionPC}
  157. */
  158. _createPeerConnection(peerJid, options = {}) {
  159. if (!peerJid) {
  160. throw new Error('Cannot create ProxyConnectionPC without a peer.');
  161. }
  162. const pcOptions = {
  163. iceConfig: this._options.iceConfig,
  164. onError: this._onFatalError,
  165. onRemoteStream: this._onRemoteStream,
  166. onSendMessage: this._onSendMessage,
  167. peerJid,
  168. ...options
  169. };
  170. return new ProxyConnectionPC(pcOptions);
  171. }
  172. /**
  173. * Callback invoked when an error occurs that should cause
  174. * {@code ProxyConnectionPC} to be closed if the peer is currently
  175. * connected. Sends an error message/reply back to the peer.
  176. *
  177. * @param {string} peerJid - The peer jid with which the connection was
  178. * attempted or started, and to which an iq with error details should be
  179. * sent.
  180. * @param {string} errorType - The constant indicating the type of the error
  181. * that occured.
  182. * @param {string} details - Optional additional data about the error.
  183. * @private
  184. * @returns {void}
  185. */
  186. _onFatalError(peerJid, errorType, details = '') {
  187. logger.error(
  188. 'Received a proxy connection error', peerJid, errorType, details);
  189. const iq = $iq({
  190. to: peerJid,
  191. type: 'set'
  192. })
  193. .c('jingle', {
  194. xmlns: 'urn:xmpp:jingle:1',
  195. action: errorType
  196. })
  197. .c('details')
  198. .t(details)
  199. .up();
  200. this._onSendMessage(peerJid, iq);
  201. if (this._peerConnection
  202. && this._peerConnection.getPeerJid() === peerJid) {
  203. this._selfCloseConnection();
  204. }
  205. }
  206. /**
  207. * Callback invoked when the remote peer of the {@code ProxyConnectionPC}
  208. * has offered a media stream. The stream is converted into a
  209. * {@code JitsiLocalTrack} for local usage if the {@code onRemoteStream}
  210. * callback is defined.
  211. *
  212. * @param {JitsiRemoteTrack} jitsiRemoteTrack - The {@code JitsiRemoteTrack}
  213. * for the peer's media stream.
  214. * @private
  215. * @returns {void}
  216. */
  217. _onRemoteStream(jitsiRemoteTrack) {
  218. if (!this._options.onRemoteStream) {
  219. logger.error('Remote track received without callback.');
  220. jitsiRemoteTrack.dispose();
  221. return;
  222. }
  223. const isVideo = jitsiRemoteTrack.isVideoTrack();
  224. let videoType;
  225. if (isVideo) {
  226. videoType = this._options.convertVideoToDesktop
  227. ? VideoType.DESKTOP : VideoType.CAMERA;
  228. }
  229. // Grab the webrtc media stream and pipe it through the same processing
  230. // that would occur for a locally obtained media stream.
  231. const mediaStream = jitsiRemoteTrack.getOriginalStream();
  232. const jitsiLocalTracks = RTC.newCreateLocalTracks(
  233. [
  234. {
  235. deviceId:
  236. `proxy:${this._peerConnection.getPeerJid()}`,
  237. mediaType: isVideo ? MediaType.VIDEO : MediaType.AUDIO,
  238. stream: mediaStream,
  239. track: mediaStream.getVideoTracks()[0],
  240. videoType
  241. }
  242. ]);
  243. this._options.onRemoteStream(jitsiLocalTracks[0]);
  244. }
  245. /**
  246. * Formats and forwards a message an iq to be sent to a peer jid.
  247. *
  248. * @param {string} peerJid - The jid the iq should be sent to.
  249. * @param {Object} iq - The iq which would be sent to the peer jid.
  250. * @private
  251. * @returns {void}
  252. */
  253. _onSendMessage(peerJid, iq) {
  254. if (!this._options.onSendMessage) {
  255. return;
  256. }
  257. try {
  258. const stringifiedIq
  259. = new XMLSerializer().serializeToString(iq.nodeTree || iq);
  260. this._options.onSendMessage(peerJid, { iq: stringifiedIq });
  261. } catch (e) {
  262. logger.error('Attempted to send an incorrectly formatted iq.');
  263. }
  264. }
  265. /**
  266. * Invoked when preemptively closing the {@code ProxyConnectionPC}.
  267. *
  268. * @private
  269. * @returns {void}
  270. */
  271. _selfCloseConnection() {
  272. this.stop();
  273. this._options.onConnectionClosed
  274. && this._options.onConnectionClosed();
  275. }
  276. }