adapterjs
Version:
Creating a common API for WebRTC in the browser
565 lines (508 loc) • 26.1 kB
JavaScript
// Define extension popup bar text
AdapterJS.TEXT.EXTENSION = {
REQUIRE_INSTALLATION_FF: 'To enable screensharing you need to install the Skylink WebRTC tools Firefox Add-on.',
REQUIRE_INSTALLATION_CHROME: 'To enable screensharing you need to install the Skylink WebRTC tools Chrome Extension.',
REQUIRE_REFRESH: 'Please refresh this page after the Skylink WebRTC tools extension has been installed.',
BUTTON_FF: 'Install Now',
BUTTON_CHROME: 'Go to Chrome Web Store'
};
// Define extension settings
AdapterJS.extensionInfo = AdapterJS.extensionInfo || {
chrome: {
extensionId: 'ljckddiekopnnjoeaiofddfhgnbdoafc',
extensionLink: 'https://chrome.google.com/webstore/detail/skylink-webrtc-tools/ljckddiekopnnjoeaiofddfhgnbdoafc',
// Deprecated! Define this to use iframe method that works with previous extension codebase that does not honor "mediaSource" flag
iframeLink: 'https://cdn.temasys.com.sg/skylink/extensions/detectRTC.html'
},
// Required only for Firefox 51 and below
firefox: {
extensionLink: 'https://addons.mozilla.org/en-US/firefox/addon/skylink-webrtc-tools/'
},
opera: {
// Define the extensionId and extensionLink to integrate the Opera screensharing extension
extensionId: null,
extensionLink: null
}
};
AdapterJS._mediaSourcePolyfillIsDefined = false;
AdapterJS._defineMediaSourcePolyfill = function () {
// Sanity checks to prevent re-defining the polyfills again in any case.
if (AdapterJS._mediaSourcePolyfillIsDefined) {
return;
}
AdapterJS._mediaSourcePolyfillIsDefined = true;
var baseGetUserMedia = null;
var clone = function(obj) {
if (null === obj || 'object' !== typeof obj) {
return obj;
}
var copy = obj.constructor();
for (var attr in obj) {
if (obj.hasOwnProperty(attr)) {
copy[attr] = obj[attr];
}
}
return copy;
};
var checkIfConstraintsIsValid = function (constraints, successCb, failureCb) {
// Append checks for overrides as these are mandatory
// Browsers (not Firefox since they went Promise based) does these checks and they can be quite useful
if (!(constraints && typeof constraints === 'object')) {
throw new Error('GetUserMedia: (constraints, .., ..) argument required');
} else if (typeof successCb !== 'function') {
throw new Error('GetUserMedia: (.., successCb, ..) argument required');
} else if (typeof failureCb !== 'function') {
throw new Error('GetUserMedia: (.., .., failureCb) argument required');
}
};
if (AdapterJS.webrtcDetectedType === 'moz') {
baseGetUserMedia = window.navigator.getUserMedia;
navigator.getUserMedia = function (constraints, successCb, failureCb) {
checkIfConstraintsIsValid(constraints, successCb, failureCb);
// Prevent accessing property from Boolean errors
if (constraints.video && typeof constraints.video === 'object' &&
constraints.video.hasOwnProperty('mediaSource')) {
var updatedConstraints = clone(constraints);
// See: http://fluffy.github.io/w3c-screen-share/#screen-based-video-constraints
// See also: https://bugzilla.mozilla.org/show_bug.cgi?id=1037405
var mediaSourcesList = ['screen', 'window', 'application', 'browser', 'camera'];
var useExtensionErrors = ['NotAllowedError', 'PermissionDeniedError', 'SecurityError'];
// Obtain first item in array if array is provided
if (Array.isArray(updatedConstraints.video.mediaSource)) {
var i = 0;
while (i < updatedConstraints.video.mediaSource.length) {
if (mediaSourcesList.indexOf(updatedConstraints.video.mediaSource[i]) > -1) {
updatedConstraints.video.mediaSource = updatedConstraints.video.mediaSource[i];
break;
}
i++;
}
updatedConstraints.video.mediaSource = typeof updatedConstraints.video.mediaSource === 'string' ?
updatedConstraints.video.mediaSource : null;
}
// Invalid mediaSource for firefox, only specified sources are supported
if (mediaSourcesList.indexOf(updatedConstraints.video.mediaSource) === -1) {
failureCb(new Error('GetUserMedia: Only "screen" and "window" are supported as mediaSource constraints'));
return;
}
// Apparently requires document.readyState to be completed before the getUserMedia() could be invoked
// NOTE: Doesn't make sense but let's keep it that way for now
var checkIfReady = setInterval(function () {
if (document.readyState !== 'complete') {
return;
}
clearInterval(checkIfReady);
updatedConstraints.video.mozMediaSource = updatedConstraints.video.mediaSource;
baseGetUserMedia(updatedConstraints, successCb, function (error) {
// Reference: https://developer.mozilla.org/en-US/docs/Web/API/MediaDevices/getUserMedia
// Firefox 51 and below throws the following errors when screensharing is disabled, in which we can
// trigger installation for legacy extension (which no longer can be used) to enable screensharing
if (useExtensionErrors.indexOf(error.name) > -1 &&
// Note that "https:" should be required for screensharing
AdapterJS.webrtcDetectedVersion < 52 && window.parent.location.protocol === 'https:') {
// Render the notification bar to install legacy Firefox (for 51 and below) extension
AdapterJS.renderNotificationBar(AdapterJS.TEXT.EXTENSION.REQUIRE_INSTALLATION_FF,
AdapterJS.TEXT.EXTENSION.BUTTON_FF, function (e) {
// Render the refresh bar once the user clicks to install extension from addons store
window.open(AdapterJS.extensionInfo.firefox.extensionLink, '_blank');
if (e.target && e.target.parentElement && e.target.nextElementSibling &&
e.target.nextElementSibling.click) {
e.target.nextElementSibling.click();
}
AdapterJS.renderNotificationBar(AdapterJS.TEXT.EXTENSION ?
AdapterJS.TEXT.EXTENSION.REQUIRE_REFRESH : AdapterJS.TEXT.REFRESH.REQUIRE_REFRESH,
AdapterJS.TEXT.REFRESH.BUTTON, function () {
window.open('javascript:location.reload()', '_top');
});
});
} else {
failureCb(error);
}
});
}, 1);
// Regular getUserMedia() call
} else {
baseGetUserMedia(constraints, successCb, failureCb);
}
};
AdapterJS.getUserMedia = window.getUserMedia = navigator.getUserMedia;
// Comment out to prevent recursive errors as webrtc/adapter polyfills navigator.getUserMedia and calls
// navigator.mediaDevices.getUserMedia internally
/*navigator.mediaDevices.getUserMedia = function(constraints) {
return new Promise(function(resolve, reject) {
window.getUserMedia(constraints, resolve, reject);
});
};*/
} else if (AdapterJS.webrtcDetectedType === 'webkit') {
baseGetUserMedia = window.navigator.getUserMedia;
var iframe = document.createElement('iframe');
navigator.getUserMedia = function (constraints, successCb, failureCb) {
checkIfConstraintsIsValid(constraints, successCb, failureCb);
// Prevent accessing property from Boolean errors
if (constraints.video && typeof constraints.video === 'object' && constraints.video.hasOwnProperty('mediaSource')) {
var updatedConstraints = clone(constraints);
// See: https://developer.chrome.com/extensions/desktopCapture#type-DesktopCaptureSourceType
var mediaSourcesList = ['window', 'screen', 'tab', 'audio'];
// Check if it is Android phone for experimental 59 screensharing
// See: https://bugs.chromium.org/p/chromium/issues/detail?id=487935
if (navigator.userAgent.toLowerCase().indexOf('android') > -1) {
if (Array.isArray(updatedConstraints.video.mediaSource) ?
updatedConstraints.video.mediaSource.indexOf('screen') > -1 :
updatedConstraints.video.mediaSource === 'screen') {
updatedConstraints.video.mandatory = updatedConstraints.video.mandatory || {};
updatedConstraints.video.mandatory.chromeMediaSource = 'screen';
updatedConstraints.video.mandatory.maxHeight = window.screen.height;
updatedConstraints.video.mandatory.maxWidth = window.screen.width;
delete updatedConstraints.video.mediaSource;
baseGetUserMedia(updatedConstraints, successCb, failureCb);
} else {
failureCb(new Error('GetUserMedia: Only "screen" are supported as mediaSource constraints for Android'));
}
return;
}
// Backwards compability for Opera browsers not working when not configured
if (!(AdapterJS.webrtcDetectedBrowser === 'opera' ? !!AdapterJS.extensionInfo.opera.extensionId :
AdapterJS.webrtcDetectedBrowser === 'chrome')) {
failureCb(new Error('Current browser does not support screensharing'));
return;
}
// Check against non valid sources
if (typeof updatedConstraints.video.mediaSource === 'string' &&
mediaSourcesList.indexOf(updatedConstraints.video.mediaSource) > -1 &&
updatedConstraints.video.mediaSource !== 'audio') {
updatedConstraints.video.mediaSource = [updatedConstraints.video.mediaSource];
// Loop array and remove invalid sources
} else if (Array.isArray(updatedConstraints.video.mediaSource)) {
var i = 0;
var outputMediaSource = [];
while (i < mediaSourcesList.length) {
var j = 0;
while (j < updatedConstraints.video.mediaSource.length) {
if (mediaSourcesList[i] === updatedConstraints.video.mediaSource[j]) {
outputMediaSource.push(updatedConstraints.video.mediaSource[j]);
}
j++;
}
i++;
}
updatedConstraints.video.mediaSource = outputMediaSource;
} else {
updatedConstraints.video.mediaSource = [];
}
// Check against returning "audio" or ["audio"] without "tab"
if (updatedConstraints.video.mediaSource.indexOf('audio') > -1 &&
updatedConstraints.video.mediaSource.indexOf('tab') === -1) {
failureCb(new Error('GetUserMedia: "audio" mediaSource must be provided with ["audio", "tab"]'));
return;
// No valid sources specified
} else if (updatedConstraints.video.mediaSource.length === 0) {
failureCb(new Error('GetUserMedia: Only "screen", "window", "tab" are supported as mediaSource constraints'));
return;
// Warn users that no tab audio will be used because constraints.audio must be enabled
} else if (updatedConstraints.video.mediaSource.indexOf('tab') > -1 &&
updatedConstraints.video.mediaSource.indexOf('audio') > -1 && !updatedConstraints.audio) {
console.warn('Audio must be requested if "tab" and "audio" mediaSource constraints is requested');
}
var fetchStream = function (response) {
if (response.success) {
updatedConstraints.video.mandatory = updatedConstraints.video.mandatory || {};
updatedConstraints.video.mandatory.chromeMediaSource = 'desktop';
updatedConstraints.video.mandatory.maxWidth = window.screen.width > 1920 ? window.screen.width : 1920;
updatedConstraints.video.mandatory.maxHeight = window.screen.height > 1080 ? window.screen.height : 1080;
updatedConstraints.video.mandatory.chromeMediaSourceId = response.sourceId;
if (Array.isArray(updatedConstraints.video.mediaSource) &&
updatedConstraints.video.mediaSource.indexOf('tab') > -1 &&
updatedConstraints.video.mediaSource.indexOf('audio') > -1 && updatedConstraints.audio) {
updatedConstraints.audio = typeof updatedConstraints.audio === 'object' ? updatedConstraints.audio : {};
updatedConstraints.audio.mandatory = updatedConstraints.audio.mandatory || {};
updatedConstraints.audio.mandatory.chromeMediaSource = 'desktop';
updatedConstraints.audio.mandatory.chromeMediaSourceId = response.sourceId;
}
delete updatedConstraints.video.mediaSource;
baseGetUserMedia(updatedConstraints, successCb, failureCb);
} else {
// Extension not installed, trigger to install
if (response.extensionLink) {
// Render the notification bar to install extension
AdapterJS.renderNotificationBar(AdapterJS.TEXT.EXTENSION.REQUIRE_INSTALLATION_CHROME,
AdapterJS.TEXT.EXTENSION.BUTTON_CHROME, function (e) {
// Render the refresh bar once the user clicks to install extension from addons store
window.open(response.extensionLink, '_blank');
if (e.target && e.target.parentElement && e.target.nextElementSibling &&
e.target.nextElementSibling.click) {
e.target.nextElementSibling.click();
}
AdapterJS.renderNotificationBar(AdapterJS.TEXT.EXTENSION ?
AdapterJS.TEXT.EXTENSION.REQUIRE_REFRESH : AdapterJS.TEXT.REFRESH.REQUIRE_REFRESH,
AdapterJS.TEXT.REFRESH.BUTTON, function () {
window.open('javascript:location.reload()', '_top');
});
});
}
failureCb(response.error);
}
};
// Communicate with detectRTC (iframe) method to retrieve source ID
// Opera browser should not use iframe method
if (AdapterJS.extensionInfo.chrome.iframeLink && AdapterJS.webrtcDetectedBrowser !== 'opera') {
iframe.getSourceId(updatedConstraints.video.mediaSource, fetchStream);
// Communicate with extension directly (needs updated extension code)
} else {
var extensionId = AdapterJS.extensionInfo[AdapterJS.webrtcDetectedBrowser === 'opera' ? 'opera' : 'chrome'].extensionId;
var extensionLink = AdapterJS.extensionInfo[AdapterJS.webrtcDetectedBrowser === 'opera' ? 'opera' : 'chrome'].extensionLink;
var icon = document.createElement('img');
icon.src = 'chrome-extension://' + extensionId + '/icon.png';
icon.onload = function() {
// Check if extension is enabled, it should return data
chrome.runtime.sendMessage(extensionId, {
type: 'get-version'
}, function (versionRes) {
// Extension not enabled
if (!(versionRes && typeof versionRes === 'object' && versionRes.type === 'send-version')) {
fetchStream({
success: false,
error: new Error('Extension is disabled')
});
return;
}
// Retrieve source ID
chrome.runtime.sendMessage(extensionId, {
type: 'get-source',
sources: updatedConstraints.video.mediaSource
}, function (sourceRes) {
// Permission denied
if (!(sourceRes && typeof sourceRes === 'object')) {
fetchStream({
success: false,
error: new Error('Retrieval failed')
});
// Could be cancelled
} else if (sourceRes.type === 'send-source-error') {
fetchStream({
success: false,
error: new Error('Permission denied for screen retrieval')
});
} else {
fetchStream({
success: true,
sourceId: sourceRes.sourceId
});
}
});
});
};
// Extension icon didn't load so extension was not installed
icon.onerror = function () {
fetchStream({
success: false,
error: new Error('Extension not installed'),
extensionLink: extensionLink
});
};
}
} else {
baseGetUserMedia(constraints, successCb, failureCb);
}
};
AdapterJS.getUserMedia = window.getUserMedia = navigator.getUserMedia;
navigator.mediaDevices.getUserMedia = function(constraints) {
return new Promise(function(resolve, reject) {
try {
window.getUserMedia(constraints, resolve, reject);
} catch (error) {
reject(error);
}
});
};
// Start loading the iframe
if (AdapterJS.webrtcDetectedBrowser === 'chrome') {
var states = {
loaded: false,
error: false
};
// Remove previous iframe if it exists
if (iframe) {
// Prevent errors thrown when iframe does not exists yet
try {
(document.body || document.documentElement).removeChild(iframe);
} catch (e) {}
}
// Do not need to load iframe as it is not requested
if (!AdapterJS.extensionInfo.chrome.iframeLink) {
return;
}
iframe.onload = function() {
states.loaded = true;
};
iframe.onerror = function () {
states.error = true;
};
iframe.src = AdapterJS.extensionInfo.chrome.iframeLink;
iframe.style.display = 'none';
// Listen to iframe messages
var getSourceIdFromIFrame = function (sources, cb) {
window.addEventListener('message', function iframeListener (evt) {
if (!(evt.data && typeof evt.data === 'object')) {
return;
// Extension not installed
} else if (evt.data.chromeExtensionStatus === 'not-installed') {
window.removeEventListener('message', iframeListener);
cb({
success: false,
error: new Error('Extension is not installed'),
// Should return the above configured chrome.extensionLink but fallback for users using custom detectRTC.html
extensionLink: evt.data.data || AdapterJS.extensionInfo.chrome.extensionLink
});
// Extension not enabled
} else if (evt.data.chromeExtensionStatus === 'installed-disabled') {
window.removeEventListener('message', iframeListener);
cb({
success: false,
error: new Error('Extension is disabled')
});
// Permission denied for retrieval
} else if (evt.data.chromeMediaSourceId === 'PermissionDeniedError') {
window.removeEventListener('message', iframeListener);
cb({
success: false,
error: new Error('Permission denied for screen retrieval')
});
// Source ID retrieved
} else if (evt.data.chromeMediaSourceId && typeof evt.data.chromeMediaSourceId === 'string') {
window.removeEventListener('message', iframeListener);
cb({
success: true,
sourceId: evt.data.chromeMediaSourceId
});
}
});
// Check if extension has loaded, and then fetch for the sourceId
iframe.contentWindow.postMessage({
captureSourceId: true,
sources: sources,
legacy: true,
extensionId: AdapterJS.extensionInfo.chrome.extensionId,
extensionLink: AdapterJS.extensionInfo.chrome.extensionLink
}, '*');
};
// The function to communicate with iframe
iframe.getSourceId = function (sources, cb) {
// If iframe failed to load, ignore
if (states.error) {
cb({
success: false,
error: new Error('iframe is not loaded')
});
return;
}
// Set interval to wait for iframe to load till 5 seconds before counting as dead
if (!states.loaded) {
var endBlocks = 0;
var intervalChecker = setInterval(function () {
if (!states.loaded) {
// Loading of iframe has been dead.
if (endBlocks === 50) {
clearInterval(intervalChecker);
cb({
success: false,
error: new Error('iframe failed to load')
});
} else {
endBlocks++;
}
} else {
clearInterval(intervalChecker);
getSourceIdFromIFrame(sources, cb);
}
}, 100);
} else {
getSourceIdFromIFrame(sources, cb);
}
};
// Re-append to reload
(document.body || document.documentElement).appendChild(iframe);
}
} else if (AdapterJS.webrtcDetectedBrowser === 'edge') {
// Note: Not overriding getUserMedia() to reject "mediaSource" as to prevent "Invalid calling object" errors.
// Nothing here because edge does not support screensharing
console.warn('Edge does not support screensharing feature in getUserMedia');
} else if (AdapterJS.webrtcDetectedType === 'AppleWebKit') {
// don't do anything. Screensharing is not supported
console.warn('Safari does not support screensharing feature in getUserMedia');
} else if (AdapterJS.webrtcDetectedType === 'plugin') {
AdapterJS.WebRTCPlugin.parseVersion = function(version) {
var components = version.split(".");
var parsed = {
major: parseInt(components[0]),
minor: parseInt(components[1]),
revision: parseInt(components[2]),
}
return parsed;
};
AdapterJS.WebRTCPlugin.isVersionGreater = function(v1, v2) {
// Parse v1 and v2
var parsedV1 = AdapterJS.WebRTCPlugin.parseVersion(v1);
var parsedV2 = AdapterJS.WebRTCPlugin.parseVersion(v2);
// Compare major, minor, revision
return (parsedV1.major > parsedV2.major)
|| (parsedV1.major == parsedV2.major && parsedV1.minor > parsedV2.minor)
|| (parsedV1.major == parsedV2.major && parsedV1.minor == parsedV2.minor && parsedV1.revision > parsedV2.revision);
}
baseGetUserMedia = window.navigator.getUserMedia;
navigator.getUserMedia = function (constraints, successCb, failureCb) {
checkIfConstraintsIsValid(constraints, successCb, failureCb);
if (constraints.video && typeof constraints.video === 'object' && constraints.video.hasOwnProperty('mediaSource')) {
var updatedConstraints = clone(constraints);
// Wait for plugin to be ready
AdapterJS.WebRTCPlugin.callWhenPluginReady(function() {
// Check if screensharing feature is available
if (!!AdapterJS.WebRTCPlugin.plugin.HasScreensharingFeature && !!AdapterJS.WebRTCPlugin.plugin.isScreensharingAvailable) {
// Do strict checks for the source ID - "screen", "window" or ["screen", "window"]
// Note that the screen/window can be JS selected using constraints.video.optional[n].screenId
if (AdapterJS.WebRTCPlugin.plugin.screensharingKeys) {
// Param: ["screen", "window"]
// Legacy: Also s upport for "Screensharing" and "screensharing"
if ((Array.isArray(updatedConstraints.video.mediaSource) &&
updatedConstraints.video.mediaSource.indexOf('screen') > -1 &&
updatedConstraints.video.mediaSource.indexOf('window') > -1)
|| updatedConstraints.video.mediaSource === AdapterJS.WebRTCPlugin.plugin.screensharingKey
|| updatedConstraints.video.mediaSource === AdapterJS.WebRTCPlugin.plugin.screensharingKeys.screenOrWindow
) {
updatedConstraints.video.mediaSource = AdapterJS.WebRTCPlugin.plugin.screensharingKeys.screenOrWindow;
// Param: ["screen"] or "screen"
} else if ((Array.isArray(updatedConstraints.video.mediaSource) &&
updatedConstraints.video.mediaSource.indexOf('screen') > -1) || updatedConstraints.video.mediaSource === 'screen') {
updatedConstraints.video.mediaSource = AdapterJS.WebRTCPlugin.plugin.screensharingKeys.screen;
// Param: ["window"] or "window"
} else if ((Array.isArray(updatedConstraints.video.mediaSource) &&
updatedConstraints.video.mediaSource.indexOf('window') > -1) || updatedConstraints.video.mediaSource === 'window') {
updatedConstraints.video.mediaSource = AdapterJS.WebRTCPlugin.plugin.screensharingKeys.window;
} else {
failureCb(new Error('GetUserMedia: Only "screen", "window", ["screen", "window"] are supported as mediaSource constraints'));
return;
}
}
// Support for legacy plugins : set the sourceId to the mediaSource value
if (!AdapterJS.WebRTCPlugin.isVersionGreater(AdapterJS.WebRTCPlugin.plugin.VERSION, '0.8.874')) {
updatedConstraints.video.optional = updatedConstraints.video.optional || [];
updatedConstraints.video.optional.push({ sourceId: updatedConstraints.video.mediaSource });
}
baseGetUserMedia(updatedConstraints, successCb, failureCb);
} else {
failureCb(new Error('Your version of the WebRTC plugin does not support screensharing'));
return;
}
});
} else {
baseGetUserMedia(constraints, successCb, failureCb);
}
};
AdapterJS.getUserMedia = getUserMedia = window.getUserMedia = navigator.getUserMedia;
if (navigator.mediaDevices && typeof Promise !== 'undefined') {
navigator.mediaDevices.getUserMedia = requestUserMedia;
}
}
};
if (typeof window.require !== 'function') {
AdapterJS._defineMediaSourcePolyfill();
}