diff --git a/ios/app/POSIX.h b/ios/app/POSIX.h new file mode 100644 index 000000000..1f589fb55 --- /dev/null +++ b/ios/app/POSIX.h @@ -0,0 +1,4 @@ +#import "RCTBridgeModule.h" + +@interface POSIX : NSObject +@end diff --git a/ios/app/POSIX.m b/ios/app/POSIX.m new file mode 100644 index 000000000..9e73925c6 --- /dev/null +++ b/ios/app/POSIX.m @@ -0,0 +1,63 @@ +#import "POSIX.h" + +#include +#include +#include +#include + +@implementation POSIX + +RCT_EXPORT_MODULE(); + +RCT_EXPORT_METHOD(getaddrinfo:(NSString *)hostname + resolve:(RCTPromiseResolveBlock)resolve + reject:(RCTPromiseRejectBlock)reject) { + int err; + struct addrinfo *res; + NSString *rejectCode; + + if (0 == (err = getaddrinfo(hostname.UTF8String, NULL, NULL, &res))) { + int af = res->ai_family; + struct sockaddr *sa = res->ai_addr; + void *addr; + + switch (af) { + case AF_INET: + addr = &(((struct sockaddr_in *) sa)->sin_addr); + break; + case AF_INET6: + addr = &(((struct sockaddr_in6 *) sa)->sin6_addr); + break; + default: + addr = NULL; + break; + } + if (addr) { + char v[MAX(INET_ADDRSTRLEN, INET6_ADDRSTRLEN)]; + + if (inet_ntop(af, addr, v, sizeof(v))) { + resolve([NSString stringWithUTF8String:v]); + } else { + err = errno; + rejectCode = @"inet_ntop"; + } + } else { + err = EAFNOSUPPORT; + rejectCode = @"EAFNOSUPPORT"; + } + + freeaddrinfo(res); + } else { + rejectCode = @"getaddrinfo"; + } + if (0 != err) { + NSError *error + = [NSError errorWithDomain:NSPOSIXErrorDomain + code:err + userInfo:nil]; + + reject(rejectCode, error.localizedDescription, error); + } +} + +@end diff --git a/ios/jitsi-meet-react.xcodeproj/project.pbxproj b/ios/jitsi-meet-react.xcodeproj/project.pbxproj index 8ce251a14..4bf84fe60 100644 --- a/ios/jitsi-meet-react.xcodeproj/project.pbxproj +++ b/ios/jitsi-meet-react.xcodeproj/project.pbxproj @@ -26,6 +26,7 @@ 832341BD1AAA6AB300B99B32 /* libRCTText.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 832341B51AAA6A8300B99B32 /* libRCTText.a */; }; B30EF2311DC0ED7C00690F45 /* WebRTC.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = B30EF2301DC0ED7C00690F45 /* WebRTC.framework */; }; B30EF2331DC0EEA500690F45 /* WebRTC.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = B30EF2301DC0ED7C00690F45 /* WebRTC.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; + B3A9D0251E0481E10009343D /* POSIX.m in Sources */ = {isa = PBXBuildFile; fileRef = B3A9D0241E0481E10009343D /* POSIX.m */; }; BF9643821C34FBB300B0BBDF /* AVFoundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = BF9643811C34FBB300B0BBDF /* AVFoundation.framework */; }; BF9643841C34FBBB00B0BBDF /* AudioToolbox.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = BF9643831C34FBBB00B0BBDF /* AudioToolbox.framework */; }; BF9643861C34FBC100B0BBDF /* CoreGraphics.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = BF9643851C34FBC100B0BBDF /* CoreGraphics.framework */; }; @@ -212,6 +213,8 @@ 821D8ABD506944B4BDBB069B /* libRNVectorIcons.a */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = archive.ar; path = libRNVectorIcons.a; sourceTree = ""; }; 832341B01AAA6A8300B99B32 /* RCTText.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = RCTText.xcodeproj; path = "../node_modules/react-native/Libraries/Text/RCTText.xcodeproj"; sourceTree = ""; }; B30EF2301DC0ED7C00690F45 /* WebRTC.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = WebRTC.framework; path = "../node_modules/react-native-webrtc/ios/WebRTC.framework"; sourceTree = ""; }; + B3A9D0231E0481E10009343D /* POSIX.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = POSIX.h; path = app/POSIX.h; sourceTree = ""; }; + B3A9D0241E0481E10009343D /* POSIX.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = POSIX.m; path = app/POSIX.m; sourceTree = ""; }; B3B083EB1D4955FF0069CEE7 /* jitsi-meet-react.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.xml; path = "jitsi-meet-react.entitlements"; sourceTree = ""; }; B96AF9B6FBC0453798399985 /* FontAwesome.ttf */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = unknown; name = FontAwesome.ttf; path = "../node_modules/react-native-vector-icons/Fonts/FontAwesome.ttf"; sourceTree = ""; }; BF9643811C34FBB300B0BBDF /* AVFoundation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = AVFoundation.framework; path = System/Library/Frameworks/AVFoundation.framework; sourceTree = SDKROOT; }; @@ -331,6 +334,8 @@ 13B07FB11A68108700A75B9A /* LaunchScreen.xib */, 008F07F21AC5B25A0029DE68 /* main.jsbundle */, 13B07FB71A68108700A75B9A /* main.m */, + B3A9D0231E0481E10009343D /* POSIX.h */, + B3A9D0241E0481E10009343D /* POSIX.m */, ); name = app; sourceTree = ""; @@ -755,6 +760,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + B3A9D0251E0481E10009343D /* POSIX.m in Sources */, 13B07FBC1A68108700A75B9A /* AppDelegate.m in Sources */, 13B07FC11A68108700A75B9A /* main.m in Sources */, ); diff --git a/react/features/base/lib-jitsi-meet/native/RTCPeerConnection.js b/react/features/base/lib-jitsi-meet/native/RTCPeerConnection.js new file mode 100644 index 000000000..964f39fae --- /dev/null +++ b/react/features/base/lib-jitsi-meet/native/RTCPeerConnection.js @@ -0,0 +1,317 @@ +import { Platform } from 'react-native'; +import { + RTCPeerConnection, + RTCSessionDescription +} from 'react-native-webrtc'; + +import { POSIX } from '../../react-native'; + +// XXX At the time of this writing extending RTCPeerConnection using ES6 'class' +// and 'extends' causes a runtime error related to the attempt to define the +// onaddstream property setter. The error mentions that babelHelpers.set is +// undefined which appears to be a thing inside React Native's packager. As a +// workaround, extend using the pre-ES6 way. + +/** + * The RTCPeerConnection provided by react-native-webrtc fires onaddstream + * before it remembers remotedescription (and thus makes it available to API + * clients). Because that appears to be a problem for lib-jitsi-meet which has + * been successfully running on Chrome, Firefox, Temasys, etc. for a very long + * time, attempt to meets its expectations (by extending RTCPPeerConnection). + * + * @class + */ +export default function _RTCPeerConnection(...args) { + + /* eslint-disable no-invalid-this */ + + RTCPeerConnection.apply(this, args); + + this.onaddstream = (...args) => // eslint-disable-line no-shadow + (this._onaddstreamQueue + ? this._queueOnaddstream + : this._invokeOnaddstream) + .apply(this, args); + + // Shadow RTCPeerConnection's onaddstream but after _RTCPeerConnection has + // assigned to the property in question. Defining the property on + // _RTCPeerConnection's prototype may (or may not, I don't know) work but I + // don't want to try because the following approach appears to work and I + // understand it. + Object.defineProperty(this, 'onaddstream', { + configurable: true, + enumerable: true, + get() { + return this._onaddstream; + }, + set(value) { + this._onaddstream = value; + } + }); + + /* eslint-enable no-invalid-this */ +} + +_RTCPeerConnection.prototype = Object.create(RTCPeerConnection.prototype); +_RTCPeerConnection.prototype.constructor = _RTCPeerConnection; + +_RTCPeerConnection.prototype._invokeOnaddstream = function(...args) { + const onaddstream = this._onaddstream; + + return onaddstream && onaddstream.apply(this, args); +}; + +_RTCPeerConnection.prototype._invokeQueuedOnaddstream = function(q) { + q && q.forEach(args => { + try { + this._invokeOnaddstream(...args); + } catch (e) { + // TODO Determine whether the combination of the standard + // setRemoteDescription and onaddstream results in a similar + // swallowing of errors. + _LOGE(e); + } + }); +}; + +_RTCPeerConnection.prototype._queueOnaddstream = function(...args) { + this._onaddstreamQueue.push(Array.from(args)); +}; + +_RTCPeerConnection.prototype.setRemoteDescription = function( + sessionDescription, + successCallback, + errorCallback) { + // If the deprecated callback-based version is used, translate it to the + // Promise-based version. + if (typeof successCallback !== 'undefined' + || typeof errorCallback !== 'undefined') { + // XXX Returning a Promise is not necessary. But I don't see why it'd + // hurt (much). + return ( + _RTCPeerConnection.prototype.setRemoteDescription.call( + this, + sessionDescription) + .then(successCallback, errorCallback)); + } + + return ( + _synthesizeIPv6Addresses(sessionDescription) + .catch(reason => { + reason && _LOGE(reason); + + return sessionDescription; + }) + .then(value => _setRemoteDescription.bind(this)(value))); + +}; + +/** + * Logs at error level. + * + * @returns {void} + */ +function _LOGE(...args) { + console && console.error && console.error(...args); +} + +/** + * Adapts react-native-webrtc's {@link RTCPeerConnection#setRemoteDescription} + * implementation which uses the deprecated, callback-based version to the + * Promise-based version. + * + * @param {RTCSessionDescription} sessionDescription - The RTCSessionDescription + * which specifies the configuration of the remote end of the connection. + * @returns {Promise} + */ +function _setRemoteDescription(sessionDescription) { + return new Promise((resolve, reject) => { + + /* eslint-disable no-invalid-this */ + + // Ensure I'm not remembering onaddstream invocations from previous + // setRemoteDescription calls. I shouldn't be but... anyway. + this._onaddstreamQueue = []; + + RTCPeerConnection.prototype.setRemoteDescription.call( + this, + sessionDescription, + (...args) => { + let q; + + try { + resolve(...args); + } finally { + q = this._onaddstreamQueue; + this._onaddstreamQueue = undefined; + } + + this._invokeQueuedOnaddstream(q); + }, + (...args) => { + this._onaddstreamQueue = undefined; + + reject(...args); + }); + + /* eslint-enable no-invalid-this */ + }); +} + +/** + * Synthesize IPv6 addresses on iOS in order to support IPv6 NAT64 networks. + * + * @param {RTCSessionDescription} sdp - The RTCSessionDescription which + * specifies the configuration of the remote end of the connection. + * @returns {Promise} + */ +function _synthesizeIPv6Addresses(sdp) { + // The synthesis of IPv6 addresses is implemented on iOS only at the time of + // this writing. + if (Platform.OS !== 'ios') { + return Promise.resolve(sdp); + } + + return ( + new Promise(resolve => resolve(_synthesizeIPv6Addresses0(sdp))) + .then(({ ips, lines }) => + Promise.all(Array.from(ips.values())) + .then(() => _synthesizeIPv6Addresses1(sdp, ips, lines)) + )); +} + +/* eslint-disable max-depth */ + +/** + * Implements the initial phase of the synthesis of IPv6 addresses. + * + * @param {RTCSessionDescription} sessionDescription - The RTCSessionDescription + * for which IPv6 addresses will be synthesized. + * @returns {{ + * ips: Map, + * lines: Array + * }} + */ +function _synthesizeIPv6Addresses0(sessionDescription) { + const sdp = sessionDescription.sdp; + let start = 0; + const lines = []; + const ips = new Map(); + + do { + const end = sdp.indexOf('\r\n', start); + let line; + + if (end === -1) { + line = sdp.substring(start); + + // Break out of the loop at the end of the iteration. + start = undefined; + } else { + line = sdp.substring(start, end); + start = end + 2; + } + + if (line.startsWith('a=candidate:')) { + const candidate = line.split(' '); + + if (candidate.length >= 10 && candidate[6] === 'typ') { + const ip4s = [ candidate[4] ]; + let abort = false; + + for (let i = 8; i < candidate.length; ++i) { + if (candidate[i] === 'raddr') { + ip4s.push(candidate[++i]); + break; + } + } + + for (const ip of ip4s) { + if (ip.indexOf(':') === -1) { + ips.has(ip) + || ips.set(ip, new Promise((resolve, reject) => { + const v = ips.get(ip); + + if (v && typeof v === 'string') { + resolve(v); + } else { + POSIX.getaddrinfo(ip).then( + value => { + if (value.indexOf(':') === -1 + || value === ips.get(ip)) { + ips.delete(ip); + } else { + ips.set(ip, value); + } + resolve(value); + }, + reject); + } + })); + } else { + abort = true; + break; + } + } + if (abort) { + ips.clear(); + break; + } + + line = candidate; + } + } + + lines.push(line); + } while (start); + + return { + ips, + lines + }; +} + +/* eslint-enable max-depth */ + +/** + * Implements the initial phase of the synthesis of IPv6 addresses. + * + * @param {RTCSessionDescription} sessionDescription - The RTCSessionDescription + * for which IPv6 addresses are being synthesized. + * @param {Map} ips - A Map of IPv4 addresses found in the specified + * sessionDescription to synthesized IPv6 addresses. + * @param {Array} lines - The lines of the specified sessionDescription. + * @returns {RTCSessionDescription} A RTCSessionDescription that represents the + * result of the synthesis of IPv6 addresses. + */ +function _synthesizeIPv6Addresses1(sessionDescription, ips, lines) { + if (ips.size === 0) { + return sessionDescription; + } + + for (let l = 0; l < lines.length; ++l) { + const candidate = lines[l]; + + if (typeof candidate !== 'string') { + let ip4 = candidate[4]; + let ip6 = ips.get(ip4); + + ip6 && (candidate[4] = ip6); + + for (let i = 8; i < candidate.length; ++i) { + if (candidate[i] === 'raddr') { + ip4 = candidate[++i]; + (ip6 = ips.get(ip4)) && (candidate[i] = ip6); + break; + } + } + + lines[l] = candidate.join(' '); + } + } + + return new RTCSessionDescription({ + sdp: lines.join('\r\n'), + type: sessionDescription.type + }); +} diff --git a/react/features/base/lib-jitsi-meet/native/polyfills-webrtc.js b/react/features/base/lib-jitsi-meet/native/polyfills-webrtc.js index c6d18279f..4754421dc 100644 --- a/react/features/base/lib-jitsi-meet/native/polyfills-webrtc.js +++ b/react/features/base/lib-jitsi-meet/native/polyfills-webrtc.js @@ -1,144 +1,21 @@ -(global => { - const { - MediaStream, - MediaStreamTrack, - RTCPeerConnection, - RTCSessionDescription, - getUserMedia - } = require('react-native-webrtc'); +import { + MediaStream, + MediaStreamTrack, + RTCSessionDescription, + getUserMedia +} from 'react-native-webrtc'; +import RTCPeerConnection from './RTCPeerConnection'; + +(global => { if (typeof global.webkitMediaStream === 'undefined') { global.webkitMediaStream = MediaStream; } - if (typeof global.MediaStreamTrack === 'undefined') { global.MediaStreamTrack = MediaStreamTrack; } - if (typeof global.webkitRTCPeerConnection === 'undefined') { - // XXX At the time of this writing extending RTCPeerConnection using ES6 - // 'class' and 'extends' causes a runtime error related to the attempt - // to define the onaddstream property setter. The error mentions that - // babelHelpers.set is undefined which appears to be a thing inside - // React Native's packager. As a workaround, extend using the pre-ES6 - // way. - - /* eslint-disable no-inner-declarations */ - - /** - * The RTCPeerConnection provided by react-native-webrtc fires - * onaddstream before it remembers remotedescription (and thus makes it - * available to API clients). Because that appears to be a problem for - * lib-jitsi-meet which has been successfully running - * on Chrome, Firefox, Temasys, etc. for a very long time, attempt to - * meets its expectations (by extending RTCPPeerConnection). - * - * @class - */ - function _RTCPeerConnection(...args) { - - /* eslint-disable no-invalid-this */ - - RTCPeerConnection.apply(this, args); - - this.onaddstream = (...args) => // eslint-disable-line no-shadow - (this._onaddstreamQueue - ? this._queueOnaddstream - : this._invokeOnaddstream) - .apply(this, args); - - // Shadow RTCPeerConnection's onaddstream but after - // _RTCPeerConnection has assigned to the property in question. - // Defining the property on _RTCPeerConnection's prototype may (or - // may not, I don't know) work but I don't want to try because the - // following approach appears to work and I understand it. - Object.defineProperty(this, 'onaddstream', { - configurable: true, - enumerable: true, - get() { - return this._onaddstream; - }, - set(value) { - this._onaddstream = value; - } - }); - - /* eslint-enable no-invalid-this */ - } - - /* eslint-enable no-inner-declarations */ - - _RTCPeerConnection.prototype - = Object.create(RTCPeerConnection.prototype); - _RTCPeerConnection.prototype.constructor = _RTCPeerConnection; - _RTCPeerConnection.prototype._invokeOnaddstream = function(...args) { - const onaddstream = this._onaddstream; - let r; - - if (onaddstream) { - r = onaddstream.apply(this, args); - } - - return r; - }; - _RTCPeerConnection.prototype._invokeQueuedOnaddstream = function(q) { - q && q.every(function(args) { - try { - this._invokeOnaddstream(...args); - } catch (e) { - // TODO Determine whether the combination of the standard - // setRemoteDescription and onaddstream results in a similar - // swallowing of errors. - console && console.error && console.error(e); - } - - return true; - }, this); - }; - _RTCPeerConnection.prototype._queueOnaddstream = function(...args) { - this._onaddstreamQueue.push(Array.from(args)); - }; - _RTCPeerConnection.prototype.setRemoteDescription - = function(sessionDescription, successCallback, errorCallback) { - // Ensure I'm not remembering onaddstream invocations from - // previous setRemoteDescription calls. I shouldn't be but... - // anyway. - this._onaddstreamQueue = []; - - return RTCPeerConnection.prototype.setRemoteDescription.call( - this, - sessionDescription, - (...args) => { - let r; - let q; - - try { - if (successCallback) { - r = successCallback(...args); - } - } finally { - q = this._onaddstreamQueue; - this._onaddstreamQueue = undefined; - } - - this._invokeQueuedOnaddstream(q); - - return r; - }, - (...args) => { - let r; - - this._onaddstreamQueue = undefined; - - if (errorCallback) { - r = errorCallback(...args); - } - - return r; - }); - }; - - global.webkitRTCPeerConnection = _RTCPeerConnection; + global.webkitRTCPeerConnection = RTCPeerConnection; } if (typeof global.RTCSessionDescription === 'undefined') { global.RTCSessionDescription = RTCSessionDescription; diff --git a/react/features/base/react-native/POSIX.js b/react/features/base/react-native/POSIX.js new file mode 100644 index 000000000..49138601d --- /dev/null +++ b/react/features/base/react-native/POSIX.js @@ -0,0 +1,3 @@ +import { NativeModules } from 'react-native'; + +export default NativeModules.POSIX; diff --git a/react/features/base/react-native/index.js b/react/features/base/react-native/index.js new file mode 100644 index 000000000..0690ec17f --- /dev/null +++ b/react/features/base/react-native/index.js @@ -0,0 +1 @@ +export { default as POSIX } from './POSIX';