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.

ProxyConnectionPC.js 13KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408
  1. import { getLogger } from '@jitsi/logger';
  2. import RTCEvents from '../../service/RTC/RTCEvents';
  3. import XMPPEvents from '../../service/xmpp/XMPPEvents';
  4. import RTC from '../RTC/RTC';
  5. import JingleSessionPC from '../xmpp/JingleSessionPC';
  6. import SignalingLayerImpl from '../xmpp/SignalingLayerImpl';
  7. import { DEFAULT_STUN_SERVERS } from '../xmpp/xmpp';
  8. import { ACTIONS } from './constants';
  9. const logger = getLogger(__filename);
  10. /**
  11. * An adapter around {@code JingleSessionPC} so its logic can be re-used without
  12. * an XMPP connection. It is being re-used for consistency with the rest of the
  13. * codebase and to leverage existing peer connection event handling. Also
  14. * this class provides a facade to hide most of the API for
  15. * {@code JingleSessionPC}.
  16. */
  17. export default class ProxyConnectionPC {
  18. /**
  19. * Initializes a new {@code ProxyConnectionPC} instance.
  20. *
  21. * @param {Object} options - Values to initialize the instance with.
  22. * @param {Object} [options.pcConfig] - The {@code RTCConfiguration} to use for the WebRTC peer connection.
  23. * @param {boolean} [options.isInitiator] - If true, the local client should send offers. If false, the local
  24. * client should send answers. Defaults to false.
  25. * @param {Function} options.onRemoteStream - Callback to invoke when a remote media stream has been received
  26. * through the peer connection.
  27. * @param {string} options.peerJid - The jid of the remote client with which the peer connection is being establish
  28. * and which should receive direct messages regarding peer connection updates.
  29. * @param {boolean} [options.receiveVideo] - Whether or not the peer connection should accept incoming video
  30. * streams. Defaults to false.
  31. * @param {Function} options.onSendMessage - Callback to invoke when a message has to be sent (signaled) out.
  32. */
  33. constructor(options = {}) {
  34. this._options = {
  35. pcConfig: {},
  36. isInitiator: false,
  37. receiveAudio: false,
  38. receiveVideo: false,
  39. ...options
  40. };
  41. /**
  42. * Instances of {@code JitsiTrack} associated with this instance of
  43. * {@code ProxyConnectionPC}.
  44. *
  45. * @type {Array<JitsiTrack>}
  46. */
  47. this._tracks = [];
  48. /**
  49. * The active instance of {@code JingleSessionPC}.
  50. *
  51. * @type {JingleSessionPC|null}
  52. */
  53. this._peerConnection = null;
  54. // Bind event handlers so they are only bound once for every instance.
  55. this._onError = this._onError.bind(this);
  56. this._onRemoteStream = this._onRemoteStream.bind(this);
  57. this._onSendMessage = this._onSendMessage.bind(this);
  58. }
  59. /**
  60. * Returns the jid of the remote peer with which this peer connection should
  61. * be established with.
  62. *
  63. * @returns {string}
  64. */
  65. getPeerJid() {
  66. return this._options.peerJid;
  67. }
  68. /**
  69. * Updates the peer connection based on the passed in jingle.
  70. *
  71. * @param {Object} $jingle - An XML jingle element, wrapped in query,
  72. * describing how the peer connection should be updated.
  73. * @returns {void}
  74. */
  75. processMessage($jingle) {
  76. switch ($jingle.attr('action')) {
  77. case ACTIONS.ACCEPT:
  78. this._onSessionAccept($jingle);
  79. break;
  80. case ACTIONS.INITIATE:
  81. this._onSessionInitiate($jingle);
  82. break;
  83. case ACTIONS.TERMINATE:
  84. this._onSessionTerminate($jingle);
  85. break;
  86. case ACTIONS.TRANSPORT_INFO:
  87. this._onTransportInfo($jingle);
  88. break;
  89. }
  90. }
  91. /**
  92. * Instantiates a peer connection and starts the offer/answer cycle to
  93. * establish a connection with a remote peer.
  94. *
  95. * @param {Array<JitsiLocalTrack>} localTracks - Initial local tracks to add
  96. * to add to the peer connection.
  97. * @returns {void}
  98. */
  99. start(localTracks = []) {
  100. if (this._peerConnection) {
  101. return;
  102. }
  103. this._tracks = this._tracks.concat(localTracks);
  104. this._peerConnection = this._createPeerConnection();
  105. this._peerConnection.invite(localTracks);
  106. }
  107. /**
  108. * Begins the process of disconnecting from a remote peer and cleaning up
  109. * the peer connection.
  110. *
  111. * @returns {void}
  112. */
  113. stop() {
  114. if (this._peerConnection) {
  115. this._peerConnection.terminate();
  116. }
  117. this._onSessionTerminate();
  118. }
  119. /**
  120. * Instantiates a new {@code JingleSessionPC} by stubbing out the various
  121. * dependencies of {@code JingleSessionPC}.
  122. *
  123. * @private
  124. * @returns {JingleSessionPC}
  125. */
  126. _createPeerConnection() {
  127. /**
  128. * {@code JingleSessionPC} takes in the entire jitsi-meet config.js
  129. * object, which may not be accessible from the caller.
  130. *
  131. * @type {Object}
  132. */
  133. const configStub = {};
  134. /**
  135. * {@code JingleSessionPC} assumes an XMPP/Strophe connection object is
  136. * passed through, which also has the jingle plugin initialized on it.
  137. * This connection object is used to signal out peer connection updates
  138. * via iqs, and those updates need to be piped back out to the remote
  139. * peer.
  140. *
  141. * @type {Object}
  142. */
  143. const connectionStub = {
  144. // At the time this is used for Spot and it's okay to say the connection is always connected, because if
  145. // spot has no signalling it will not be in a meeting where this is used.
  146. connected: true,
  147. jingle: {
  148. terminate: () => { /** no-op */ }
  149. },
  150. sendIQ: this._onSendMessage,
  151. // Returns empty function, because it does not add any listeners for real
  152. // eslint-disable-next-line no-empty-function
  153. addEventListener: () => () => { }
  154. };
  155. /**
  156. * {@code JingleSessionPC} can take in a custom ice configuration,
  157. * depending on the peer connection type, peer-to-peer or other.
  158. * However, {@code ProxyConnectionPC} always assume a peer-to-peer
  159. * connection so the ice configuration is hard-coded with defaults.
  160. *
  161. * @type {Object}
  162. */
  163. const pcConfigStub = {
  164. iceServers: DEFAULT_STUN_SERVERS,
  165. ...this._options.pcConfig
  166. };
  167. /**
  168. * {@code JingleSessionPC} expects an instance of
  169. * {@code JitsiConference}, which has an event emitter that is used
  170. * to signal various connection updates that the local client should
  171. * act upon. The conference instance is not a dependency of a proxy
  172. * connection, but the emitted events can be relevant to the proxy
  173. * connection so the event emitter is stubbed.
  174. *
  175. * @param {string} event - The constant for the event type.
  176. * @type {Function}
  177. * @returns {void}
  178. */
  179. const emitter = event => {
  180. switch (event) {
  181. case XMPPEvents.CONNECTION_ICE_FAILED:
  182. case XMPPEvents.CONNECTION_FAILED:
  183. this._onError(ACTIONS.CONNECTION_ERROR, event);
  184. break;
  185. }
  186. };
  187. /**
  188. * {@link JingleSessionPC} expects an instance of
  189. * {@link ChatRoom} to be passed in. {@link ProxyConnectionPC}
  190. * is instantiated outside of the {@code JitsiConference}, so it must be
  191. * stubbed to prevent errors.
  192. *
  193. * @type {Object}
  194. */
  195. const roomStub = {
  196. addPresenceListener: () => { /** no-op */ },
  197. connectionTimes: [],
  198. eventEmitter: { emit: emitter },
  199. getMediaPresenceInfo: () => {
  200. // Errors occur if this function does not return an object
  201. return {};
  202. },
  203. removePresenceListener: () => { /** no-op */ },
  204. supportsRestartByTerminate: () => false
  205. };
  206. /**
  207. * A {@code JitsiConference} stub passed to the {@link RTC} module.
  208. * @type {Object}
  209. */
  210. const conferenceStub = {
  211. myUserId: () => ''
  212. };
  213. /**
  214. * Create an instance of {@code RTC} as it is required for peer
  215. * connection creation by {@code JingleSessionPC}. An existing instance
  216. * of {@code RTC} from elsewhere should not be re-used because it is
  217. * a stateful grouping of utilities.
  218. */
  219. this._rtc = new RTC(conferenceStub, {});
  220. /**
  221. * Add the remote track listener here as {@code JingleSessionPC} has
  222. * {@code TraceablePeerConnection} which uses {@code RTC}'s event
  223. * emitter.
  224. */
  225. this._rtc.addListener(
  226. RTCEvents.REMOTE_TRACK_ADDED,
  227. this._onRemoteStream
  228. );
  229. const peerConnection = new JingleSessionPC(
  230. undefined, // sid
  231. undefined, // localJid
  232. this._options.peerJid, // remoteJid
  233. connectionStub, // connection
  234. {
  235. offerToReceiveAudio: this._options.receiveAudio,
  236. offerToReceiveVideo: this._options.receiveVideo
  237. }, // mediaConstraints
  238. pcConfigStub, // pcConfig
  239. true, // isP2P
  240. this._options.isInitiator // isInitiator
  241. );
  242. const signalingLayer = new SignalingLayerImpl();
  243. signalingLayer.setChatRoom(roomStub);
  244. /**
  245. * An additional initialize call is necessary to properly set instance
  246. * variable for calling.
  247. */
  248. peerConnection.initialize(roomStub, this._rtc, configStub);
  249. return peerConnection;
  250. }
  251. /**
  252. * Invoked when a connection related issue has been encountered.
  253. *
  254. * @param {string} errorType - The constant indicating the type of the error
  255. * that occured.
  256. * @param {string} details - Optional additional data about the error.
  257. * @private
  258. * @returns {void}
  259. */
  260. _onError(errorType, details = '') {
  261. this._options.onError(this._options.peerJid, errorType, details);
  262. }
  263. /**
  264. * Callback invoked when the peer connection has received a remote media
  265. * stream.
  266. *
  267. * @param {JitsiRemoteTrack} jitsiRemoteTrack - The remote media stream
  268. * wrapped in {@code JitsiRemoteTrack}.
  269. * @private
  270. * @returns {void}
  271. */
  272. _onRemoteStream(jitsiRemoteTrack) {
  273. this._tracks.push(jitsiRemoteTrack);
  274. this._options.onRemoteStream(jitsiRemoteTrack);
  275. }
  276. /**
  277. * Callback invoked when {@code JingleSessionPC} needs to signal a message
  278. * out to the remote peer.
  279. *
  280. * @param {XML} iq - The message to signal out.
  281. * @private
  282. * @returns {void}
  283. */
  284. _onSendMessage(iq) {
  285. this._options.onSendMessage(this._options.peerJid, iq);
  286. }
  287. /**
  288. * Callback invoked in response to an agreement to start a proxy connection.
  289. * The passed in jingle element should contain an SDP answer to a previously
  290. * sent SDP offer.
  291. *
  292. * @param {Object} $jingle - The jingle element wrapped in jQuery.
  293. * @private
  294. * @returns {void}
  295. */
  296. _onSessionAccept($jingle) {
  297. if (!this._peerConnection) {
  298. logger.error('Received an answer when no peer connection exists.');
  299. return;
  300. }
  301. this._peerConnection.setAnswer($jingle);
  302. }
  303. /**
  304. * Callback invoked in response to a request to start a proxy connection.
  305. * The passed in jingle element should contain an SDP offer.
  306. *
  307. * @param {Object} $jingle - The jingle element wrapped in jQuery.
  308. * @private
  309. * @returns {void}
  310. */
  311. _onSessionInitiate($jingle) {
  312. if (this._peerConnection) {
  313. logger.error('Received an offer when an offer was already sent.');
  314. return;
  315. }
  316. this._peerConnection = this._createPeerConnection();
  317. this._peerConnection.acceptOffer(
  318. $jingle,
  319. () => { /** no-op */ },
  320. () => this._onError(
  321. this._options.peerJid,
  322. ACTIONS.CONNECTION_ERROR,
  323. 'session initiate error'
  324. )
  325. );
  326. }
  327. /**
  328. * Callback invoked in response to a request to disconnect an active proxy
  329. * connection. Cleans up tracks and the peer connection.
  330. *
  331. * @private
  332. * @returns {void}
  333. */
  334. _onSessionTerminate() {
  335. this._tracks.forEach(track => track.dispose());
  336. this._tracks = [];
  337. if (this._peerConnection) {
  338. this._peerConnection.onTerminated();
  339. }
  340. if (this._rtc) {
  341. this._rtc.removeListener(
  342. RTCEvents.REMOTE_TRACK_ADDED,
  343. this._onRemoteStream
  344. );
  345. this._rtc.destroy();
  346. }
  347. }
  348. /**
  349. * Callback invoked in response to ICE candidates from the remote peer.
  350. * The passed in jingle element should contain an ICE candidate.
  351. *
  352. * @param {Object} $jingle - The jingle element wrapped in jQuery.
  353. * @private
  354. * @returns {void}
  355. */
  356. _onTransportInfo($jingle) {
  357. this._peerConnection.addIceCandidates($jingle);
  358. }
  359. }