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.

RTCPeerConnection.js 14KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436
  1. // @flow
  2. import { NativeModules } from 'react-native';
  3. import { RTCPeerConnection, RTCSessionDescription } from 'react-native-webrtc';
  4. const logger = require('jitsi-meet-logger').getLogger(__filename);
  5. /* eslint-disable no-unused-vars */
  6. // Address families.
  7. const AF_INET6 = 30; /* IPv6 */
  8. // Protocols (RFC 1700)
  9. const IPPROTO_TCP = 6; /* tcp */
  10. const IPPROTO_UDP = 17; /* user datagram protocol */
  11. // Protocol families, same as address families for now.
  12. const PF_INET6 = AF_INET6;
  13. const SOCK_DGRAM = 2; /* datagram socket */
  14. const SOCK_STREAM = 1; /* stream socket */
  15. /* eslint-enable no-unused-vars */
  16. // XXX At the time of this writing extending RTCPeerConnection using ES6 'class'
  17. // and 'extends' causes a runtime error related to the attempt to define the
  18. // onaddstream property setter. The error mentions that babelHelpers.set is
  19. // undefined which appears to be a thing inside React Native's packager. As a
  20. // workaround, extend using the pre-ES6 way.
  21. /**
  22. * The RTCPeerConnection provided by react-native-webrtc fires onaddstream
  23. * before it remembers remotedescription (and thus makes it available to API
  24. * clients). Because that appears to be a problem for lib-jitsi-meet which has
  25. * been successfully running on Chrome, Firefox, etc. for a very long
  26. * time, attempt to meet its expectations (by extending RTCPPeerConnection).
  27. *
  28. * @class
  29. */
  30. export default function _RTCPeerConnection(...args: any[]) {
  31. /* eslint-disable indent, no-invalid-this */
  32. RTCPeerConnection.apply(this, args);
  33. this.onaddstream = (...args) => // eslint-disable-line no-shadow
  34. (this._onaddstreamQueue
  35. ? this._queueOnaddstream
  36. : this._invokeOnaddstream)
  37. .apply(this, args);
  38. // Shadow RTCPeerConnection's onaddstream but after _RTCPeerConnection has
  39. // assigned to the property in question. Defining the property on
  40. // _RTCPeerConnection's prototype may (or may not, I don't know) work but I
  41. // don't want to try because the following approach appears to work and I
  42. // understand it.
  43. // $FlowFixMe
  44. Object.defineProperty(this, 'onaddstream', {
  45. configurable: true,
  46. enumerable: true,
  47. get() {
  48. return this._onaddstream;
  49. },
  50. set(value) {
  51. this._onaddstream = value;
  52. }
  53. });
  54. /* eslint-enable indent, no-invalid-this */
  55. }
  56. _RTCPeerConnection.prototype = Object.create(RTCPeerConnection.prototype);
  57. _RTCPeerConnection.prototype.constructor = _RTCPeerConnection;
  58. _RTCPeerConnection.prototype.addIceCandidate
  59. = _makePromiseAware(RTCPeerConnection.prototype.addIceCandidate, 1, 0);
  60. _RTCPeerConnection.prototype.createAnswer
  61. = _makePromiseAware(RTCPeerConnection.prototype.createAnswer, 0, 1);
  62. _RTCPeerConnection.prototype.createOffer
  63. = _makePromiseAware(RTCPeerConnection.prototype.createOffer, 0, 1);
  64. _RTCPeerConnection.prototype._invokeOnaddstream = function(...args) {
  65. const onaddstream = this._onaddstream;
  66. return onaddstream && onaddstream.apply(this, args);
  67. };
  68. _RTCPeerConnection.prototype._invokeQueuedOnaddstream = function(q) {
  69. q && q.forEach(args => {
  70. try {
  71. this._invokeOnaddstream(...args);
  72. } catch (e) {
  73. // TODO Determine whether the combination of the standard
  74. // setRemoteDescription and onaddstream results in a similar
  75. // swallowing of errors.
  76. _LOGE(e);
  77. }
  78. });
  79. };
  80. _RTCPeerConnection.prototype._queueOnaddstream = function(...args) {
  81. this._onaddstreamQueue.push(Array.from(args));
  82. };
  83. _RTCPeerConnection.prototype.setLocalDescription
  84. = _makePromiseAware(RTCPeerConnection.prototype.setLocalDescription, 1, 0);
  85. _RTCPeerConnection.prototype.setRemoteDescription = function(
  86. sessionDescription,
  87. successCallback,
  88. errorCallback) {
  89. // If the deprecated callback-based version is used, translate it to the
  90. // Promise-based version.
  91. if (typeof successCallback !== 'undefined'
  92. || typeof errorCallback !== 'undefined') {
  93. // XXX Returning a Promise is not necessary. But I don't see why it'd
  94. // hurt (much).
  95. return (
  96. _RTCPeerConnection.prototype.setRemoteDescription.call(
  97. this,
  98. sessionDescription)
  99. .then(successCallback, errorCallback));
  100. }
  101. return (
  102. _synthesizeIPv6Addresses(sessionDescription)
  103. .catch(reason => {
  104. reason && _LOGE(reason);
  105. return sessionDescription;
  106. })
  107. .then(value => _setRemoteDescription.bind(this)(value)));
  108. };
  109. /**
  110. * Logs at error level.
  111. *
  112. * @private
  113. * @returns {void}
  114. */
  115. function _LOGE(...args) {
  116. logger.error(...args);
  117. }
  118. /**
  119. * Makes a {@code Promise}-returning function out of a specific void function
  120. * with {@code successCallback} and {@code failureCallback}.
  121. *
  122. * @param {Function} f - The (void) function with {@code successCallback} and
  123. * {@code failureCallback}.
  124. * @param {number} beforeCallbacks - The number of arguments before
  125. * {@code successCallback} and {@code failureCallback}.
  126. * @param {number} afterCallbacks - The number of arguments after
  127. * {@code successCallback} and {@code failureCallback}.
  128. * @returns {Promise}
  129. */
  130. function _makePromiseAware(
  131. f: Function,
  132. beforeCallbacks: number,
  133. afterCallbacks: number) {
  134. return function(...args) {
  135. return new Promise((resolve, reject) => {
  136. if (args.length <= beforeCallbacks + afterCallbacks) {
  137. args.splice(beforeCallbacks, 0, resolve, reject);
  138. }
  139. let fPromise;
  140. try {
  141. // eslint-disable-next-line no-invalid-this
  142. fPromise = f.apply(this, args);
  143. } catch (e) {
  144. reject(e);
  145. }
  146. // If the super implementation returns a Promise from the deprecated
  147. // invocation by any chance, try to make sense of it.
  148. if (fPromise) {
  149. const { then } = fPromise;
  150. typeof then === 'function'
  151. && then.call(fPromise, resolve, reject);
  152. }
  153. });
  154. };
  155. }
  156. /**
  157. * Adapts react-native-webrtc's {@link RTCPeerConnection#setRemoteDescription}
  158. * implementation which uses the deprecated, callback-based version to the
  159. * {@code Promise}-based version.
  160. *
  161. * @param {RTCSessionDescription} sessionDescription - The RTCSessionDescription
  162. * which specifies the configuration of the remote end of the connection.
  163. * @private
  164. * @private
  165. * @returns {Promise}
  166. */
  167. function _setRemoteDescription(sessionDescription) {
  168. return new Promise((resolve, reject) => {
  169. /* eslint-disable no-invalid-this */
  170. // Ensure I'm not remembering onaddstream invocations from previous
  171. // setRemoteDescription calls. I shouldn't be but... anyway.
  172. this._onaddstreamQueue = [];
  173. RTCPeerConnection.prototype.setRemoteDescription.call(
  174. this,
  175. sessionDescription,
  176. (...args) => {
  177. let q;
  178. try {
  179. resolve(...args);
  180. } finally {
  181. q = this._onaddstreamQueue;
  182. this._onaddstreamQueue = undefined;
  183. }
  184. this._invokeQueuedOnaddstream(q);
  185. },
  186. (...args) => {
  187. this._onaddstreamQueue = undefined;
  188. reject(...args);
  189. });
  190. /* eslint-enable no-invalid-this */
  191. });
  192. }
  193. // XXX The function _synthesizeIPv6FromIPv4Address is not placed relative to the
  194. // other functions in the file according to alphabetical sorting rule of the
  195. // coding style. But eslint wants constants to be defined before they are used.
  196. /**
  197. * Synthesizes an IPv6 address from a specific IPv4 address.
  198. *
  199. * @param {string} ipv4 - The IPv4 address from which an IPv6 address is to be
  200. * synthesized.
  201. * @returns {Promise<?string>} A {@code Promise} which gets resolved with the
  202. * IPv6 address synthesized from the specified {@code ipv4} or a falsy value to
  203. * be treated as inability to synthesize an IPv6 address from the specified
  204. * {@code ipv4}.
  205. */
  206. const _synthesizeIPv6FromIPv4Address: string => Promise<?string> = (function() {
  207. // POSIX.getaddrinfo
  208. const { POSIX } = NativeModules;
  209. if (POSIX) {
  210. const { getaddrinfo } = POSIX;
  211. if (typeof getaddrinfo === 'function') {
  212. return ipv4 =>
  213. getaddrinfo(/* hostname */ ipv4, /* servname */ undefined)
  214. .then(([ { ai_addr: ipv6 } ]) => ipv6);
  215. }
  216. }
  217. // NAT64AddrInfo.getIPv6Address
  218. const { NAT64AddrInfo } = NativeModules;
  219. if (NAT64AddrInfo) {
  220. const { getIPv6Address } = NAT64AddrInfo;
  221. if (typeof getIPv6Address === 'function') {
  222. return getIPv6Address;
  223. }
  224. }
  225. // There's no POSIX.getaddrinfo or NAT64AddrInfo.getIPv6Address.
  226. return () =>
  227. Promise.reject(
  228. 'The impossible just happened! No POSIX.getaddrinfo or'
  229. + ' NAT64AddrInfo.getIPv6Address!');
  230. })();
  231. /**
  232. * Synthesizes IPv6 addresses on iOS in order to support IPv6 NAT64 networks.
  233. *
  234. * @param {RTCSessionDescription} sdp - The RTCSessionDescription which
  235. * specifies the configuration of the remote end of the connection.
  236. * @private
  237. * @returns {Promise}
  238. */
  239. function _synthesizeIPv6Addresses(sdp) {
  240. return (
  241. new Promise(resolve => resolve(_synthesizeIPv6Addresses0(sdp)))
  242. .then(({ ips, lines }) =>
  243. Promise.all(Array.from(ips.values()))
  244. .then(() => _synthesizeIPv6Addresses1(sdp, ips, lines))
  245. ));
  246. }
  247. /* eslint-disable max-depth */
  248. /**
  249. * Begins the asynchronous synthesis of IPv6 addresses.
  250. *
  251. * @param {RTCSessionDescription} sessionDescription - The RTCSessionDescription
  252. * for which IPv6 addresses will be synthesized.
  253. * @private
  254. * @returns {{
  255. * ips: Map,
  256. * lines: Array
  257. * }}
  258. */
  259. function _synthesizeIPv6Addresses0(sessionDescription) {
  260. const sdp = sessionDescription.sdp;
  261. let start = 0;
  262. const lines = [];
  263. const ips = new Map();
  264. do {
  265. const end = sdp.indexOf('\r\n', start);
  266. let line;
  267. if (end === -1) {
  268. line = sdp.substring(start);
  269. // Break out of the loop at the end of the iteration.
  270. start = undefined;
  271. } else {
  272. line = sdp.substring(start, end);
  273. start = end + 2;
  274. }
  275. if (line.startsWith('a=candidate:')) {
  276. const candidate = line.split(' ');
  277. if (candidate.length >= 10 && candidate[6] === 'typ') {
  278. const ip4s = [ candidate[4] ];
  279. let abort = false;
  280. for (let i = 8; i < candidate.length; ++i) {
  281. if (candidate[i] === 'raddr') {
  282. ip4s.push(candidate[++i]);
  283. break;
  284. }
  285. }
  286. for (const ip of ip4s) {
  287. if (ip.indexOf(':') === -1) {
  288. ips.has(ip)
  289. || ips.set(ip, new Promise((resolve, reject) => {
  290. const v = ips.get(ip);
  291. if (v && typeof v === 'string') {
  292. resolve(v);
  293. } else {
  294. _synthesizeIPv6FromIPv4Address(ip).then(
  295. value => {
  296. if (!value
  297. || value.indexOf(':') === -1
  298. || value === ips.get(ip)) {
  299. ips.delete(ip);
  300. } else {
  301. ips.set(ip, value);
  302. }
  303. resolve(value);
  304. },
  305. reject);
  306. }
  307. }));
  308. } else {
  309. abort = true;
  310. break;
  311. }
  312. }
  313. if (abort) {
  314. ips.clear();
  315. break;
  316. }
  317. line = candidate;
  318. }
  319. }
  320. lines.push(line);
  321. } while (start);
  322. return {
  323. ips,
  324. lines
  325. };
  326. }
  327. /* eslint-enable max-depth */
  328. /**
  329. * Completes the asynchronous synthesis of IPv6 addresses.
  330. *
  331. * @param {RTCSessionDescription} sessionDescription - The RTCSessionDescription
  332. * for which IPv6 addresses are being synthesized.
  333. * @param {Map} ips - A Map of IPv4 addresses found in the specified
  334. * sessionDescription to synthesized IPv6 addresses.
  335. * @param {Array} lines - The lines of the specified sessionDescription.
  336. * @private
  337. * @returns {RTCSessionDescription} A RTCSessionDescription that represents the
  338. * result of the synthesis of IPv6 addresses.
  339. */
  340. function _synthesizeIPv6Addresses1(sessionDescription, ips, lines) {
  341. if (ips.size === 0) {
  342. return sessionDescription;
  343. }
  344. for (let l = 0; l < lines.length; ++l) {
  345. const candidate = lines[l];
  346. if (typeof candidate !== 'string') {
  347. let ip4 = candidate[4];
  348. let ip6 = ips.get(ip4);
  349. ip6 && (candidate[4] = ip6);
  350. for (let i = 8; i < candidate.length; ++i) {
  351. if (candidate[i] === 'raddr') {
  352. ip4 = candidate[++i];
  353. (ip6 = ips.get(ip4)) && (candidate[i] = ip6);
  354. break;
  355. }
  356. }
  357. lines[l] = candidate.join(' ');
  358. }
  359. }
  360. return new RTCSessionDescription({
  361. sdp: lines.join('\r\n'),
  362. type: sessionDescription.type
  363. });
  364. }