選択できるのは25トピックまでです。 トピックは、先頭が英数字で、英数字とダッシュ('-')を使用した35文字以内のものにしてください。

ProxyConnectionService.js 11KB

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