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

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