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 11KB

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