UNPKG

rtcmulticonnection

Version:

RTCMultiConnection is a WebRTC JavaScript wrapper library runs top over RTCPeerConnection API to support all possible peer-to-peer features.

1,255 lines (1,063 loc) 232 kB
'use strict'; // Last time updated: 2018-10-01 2:40:55 PM UTC // _________________________ // RTCMultiConnection v3.4.7 // Open-Sourced: https://github.com/muaz-khan/RTCMultiConnection // -------------------------------------------------- // Muaz Khan - www.MuazKhan.com // MIT License - www.WebRTC-Experiment.com/licence // -------------------------------------------------- window.RTCMultiConnection = function(roomid, forceOptions) { function SocketConnection(connection, connectCallback) { var parameters = ''; parameters += '?userid=' + connection.userid; parameters += '&sessionid=' + connection.sessionid; parameters += '&msgEvent=' + connection.socketMessageEvent; parameters += '&socketCustomEvent=' + connection.socketCustomEvent; parameters += '&autoCloseEntireSession=' + !!connection.autoCloseEntireSession; if (connection.session.broadcast === true) { parameters += '&oneToMany=true'; } parameters += '&maxParticipantsAllowed=' + connection.maxParticipantsAllowed; if (connection.enableScalableBroadcast) { parameters += '&enableScalableBroadcast=true'; parameters += '&maxRelayLimitPerUser=' + (connection.maxRelayLimitPerUser || 2); } if (connection.socketCustomParameters) { parameters += connection.socketCustomParameters; } try { io.sockets = {}; } catch (e) {}; if (!connection.socketURL) { connection.socketURL = '/'; } if (connection.socketURL.substr(connection.socketURL.length - 1, 1) != '/') { // connection.socketURL = 'https://domain.com:9001/'; throw '"socketURL" MUST end with a slash.'; } if (connection.enableLogs) { if (connection.socketURL == '/') { console.info('socket.io is connected at: ', location.origin + '/'); } else { console.info('socket.io is connected at: ', connection.socketURL); } } try { connection.socket = io(connection.socketURL + parameters); } catch (e) { connection.socket = io.connect(connection.socketURL + parameters, connection.socketOptions); } // detect signaling medium connection.socket.isIO = true; var mPeer = connection.multiPeersHandler; connection.socket.on('extra-data-updated', function(remoteUserId, extra) { if (!connection.peers[remoteUserId]) return; connection.peers[remoteUserId].extra = extra; connection.onExtraDataUpdated({ userid: remoteUserId, extra: extra }); updateExtraBackup(remoteUserId, extra); }); function updateExtraBackup(remoteUserId, extra) { if (!connection.peersBackup[remoteUserId]) { connection.peersBackup[remoteUserId] = { userid: remoteUserId, extra: {} }; } connection.peersBackup[remoteUserId].extra = extra; } function onMessageEvent(message) { if (message.remoteUserId != connection.userid) return; if (connection.peers[message.sender] && connection.peers[message.sender].extra != message.message.extra) { connection.peers[message.sender].extra = message.extra; connection.onExtraDataUpdated({ userid: message.sender, extra: message.extra }); updateExtraBackup(message.sender, message.extra); } if (message.message.streamSyncNeeded && connection.peers[message.sender]) { var stream = connection.streamEvents[message.message.streamid]; if (!stream || !stream.stream) { return; } var action = message.message.action; if (action === 'ended' || action === 'inactive' || action === 'stream-removed') { if (connection.peersBackup[stream.userid]) { stream.extra = connection.peersBackup[stream.userid].extra; } connection.onstreamended(stream); return; } var type = message.message.type != 'both' ? message.message.type : null; if (typeof stream.stream[action] == 'function') { stream.stream[action](type); } return; } if (message.message === 'dropPeerConnection') { connection.deletePeer(message.sender); return; } if (message.message.allParticipants) { if (message.message.allParticipants.indexOf(message.sender) === -1) { message.message.allParticipants.push(message.sender); } message.message.allParticipants.forEach(function(participant) { mPeer[!connection.peers[participant] ? 'createNewPeer' : 'renegotiatePeer'](participant, { localPeerSdpConstraints: { OfferToReceiveAudio: connection.sdpConstraints.mandatory.OfferToReceiveAudio, OfferToReceiveVideo: connection.sdpConstraints.mandatory.OfferToReceiveVideo }, remotePeerSdpConstraints: { OfferToReceiveAudio: connection.session.oneway ? !!connection.session.audio : connection.sdpConstraints.mandatory.OfferToReceiveAudio, OfferToReceiveVideo: connection.session.oneway ? !!connection.session.video || !!connection.session.screen : connection.sdpConstraints.mandatory.OfferToReceiveVideo }, isOneWay: !!connection.session.oneway || connection.direction === 'one-way', isDataOnly: isData(connection.session) }); }); return; } if (message.message.newParticipant) { if (message.message.newParticipant == connection.userid) return; if (!!connection.peers[message.message.newParticipant]) return; mPeer.createNewPeer(message.message.newParticipant, message.message.userPreferences || { localPeerSdpConstraints: { OfferToReceiveAudio: connection.sdpConstraints.mandatory.OfferToReceiveAudio, OfferToReceiveVideo: connection.sdpConstraints.mandatory.OfferToReceiveVideo }, remotePeerSdpConstraints: { OfferToReceiveAudio: connection.session.oneway ? !!connection.session.audio : connection.sdpConstraints.mandatory.OfferToReceiveAudio, OfferToReceiveVideo: connection.session.oneway ? !!connection.session.video || !!connection.session.screen : connection.sdpConstraints.mandatory.OfferToReceiveVideo }, isOneWay: !!connection.session.oneway || connection.direction === 'one-way', isDataOnly: isData(connection.session) }); return; } if (message.message.readyForOffer) { if (connection.attachStreams.length) { connection.waitingForLocalMedia = false; } if (connection.waitingForLocalMedia) { // if someone is waiting to join you // make sure that we've local media before making a handshake setTimeout(function() { onMessageEvent(message); }, 1); return; } } if (message.message.newParticipationRequest && message.sender !== connection.userid) { if (connection.peers[message.sender]) { connection.deletePeer(message.sender); } var userPreferences = { extra: message.extra || {}, localPeerSdpConstraints: message.message.remotePeerSdpConstraints || { OfferToReceiveAudio: connection.sdpConstraints.mandatory.OfferToReceiveAudio, OfferToReceiveVideo: connection.sdpConstraints.mandatory.OfferToReceiveVideo }, remotePeerSdpConstraints: message.message.localPeerSdpConstraints || { OfferToReceiveAudio: connection.session.oneway ? !!connection.session.audio : connection.sdpConstraints.mandatory.OfferToReceiveAudio, OfferToReceiveVideo: connection.session.oneway ? !!connection.session.video || !!connection.session.screen : connection.sdpConstraints.mandatory.OfferToReceiveVideo }, isOneWay: typeof message.message.isOneWay !== 'undefined' ? message.message.isOneWay : !!connection.session.oneway || connection.direction === 'one-way', isDataOnly: typeof message.message.isDataOnly !== 'undefined' ? message.message.isDataOnly : isData(connection.session), dontGetRemoteStream: typeof message.message.isOneWay !== 'undefined' ? message.message.isOneWay : !!connection.session.oneway || connection.direction === 'one-way', dontAttachLocalStream: !!message.message.dontGetRemoteStream, connectionDescription: message, successCallback: function() {} }; connection.onNewParticipant(message.sender, userPreferences); return; } if (message.message.changedUUID) { if (connection.peers[message.message.oldUUID]) { connection.peers[message.message.newUUID] = connection.peers[message.message.oldUUID]; delete connection.peers[message.message.oldUUID]; } } if (message.message.userLeft) { mPeer.onUserLeft(message.sender); if (!!message.message.autoCloseEntireSession) { connection.leave(); } return; } mPeer.addNegotiatedMessage(message.message, message.sender); } connection.socket.on(connection.socketMessageEvent, onMessageEvent); var alreadyConnected = false; connection.socket.resetProps = function() { alreadyConnected = false; }; connection.socket.on('connect', function() { if (alreadyConnected) { return; } alreadyConnected = true; if (connection.enableLogs) { console.info('socket.io connection is opened.'); } setTimeout(function() { connection.socket.emit('extra-data-updated', connection.extra); if (connectCallback) { connectCallback(connection.socket); } }, 1000); }); connection.socket.on('disconnect', function() { if (connection.enableLogs) { console.warn('socket.io connection is closed'); } }); connection.socket.on('join-with-password', function(remoteUserId) { connection.onJoinWithPassword(remoteUserId); }); connection.socket.on('invalid-password', function(remoteUserId, oldPassword) { connection.onInvalidPassword(remoteUserId, oldPassword); }); connection.socket.on('password-max-tries-over', function(remoteUserId) { connection.onPasswordMaxTriesOver(remoteUserId); }); connection.socket.on('user-disconnected', function(remoteUserId) { if (remoteUserId === connection.userid) { return; } connection.onUserStatusChanged({ userid: remoteUserId, status: 'offline', extra: connection.peers[remoteUserId] ? connection.peers[remoteUserId].extra || {} : {} }); connection.deletePeer(remoteUserId); }); connection.socket.on('user-connected', function(userid) { if (userid === connection.userid) { return; } connection.onUserStatusChanged({ userid: userid, status: 'online', extra: connection.peers[userid] ? connection.peers[userid].extra || {} : {} }); }); connection.socket.on('closed-entire-session', function(sessionid, extra) { connection.leave(); connection.onEntireSessionClosed({ sessionid: sessionid, userid: sessionid, extra: extra }); }); connection.socket.on('userid-already-taken', function(useridAlreadyTaken, yourNewUserId) { connection.isInitiator = false; connection.userid = yourNewUserId; connection.onUserIdAlreadyTaken(useridAlreadyTaken, yourNewUserId); }) connection.socket.on('logs', function(log) { if (!connection.enableLogs) return; console.debug('server-logs', log); }); connection.socket.on('number-of-broadcast-viewers-updated', function(data) { connection.onNumberOfBroadcastViewersUpdated(data); }); connection.socket.on('room-full', function(roomid) { connection.onRoomFull(roomid); }); connection.socket.on('become-next-modrator', function(sessionid) { if (sessionid != connection.sessionid) return; connection.isInitiator = true; }); } function MultiPeers(connection) { var self = this; var skipPeers = ['getAllParticipants', 'getLength', 'selectFirst', 'streams', 'send', 'forEach']; connection.peers = { getLength: function() { var numberOfPeers = 0; for (var peer in this) { if (skipPeers.indexOf(peer) == -1) { numberOfPeers++; } } return numberOfPeers; }, selectFirst: function() { var firstPeer; for (var peer in this) { if (skipPeers.indexOf(peer) == -1) { firstPeer = this[peer]; } } return firstPeer; }, getAllParticipants: function(sender) { var allPeers = []; for (var peer in this) { if (skipPeers.indexOf(peer) == -1 && peer != sender) { allPeers.push(peer); } } return allPeers; }, forEach: function(callbcak) { this.getAllParticipants().forEach(function(participant) { callbcak(connection.peers[participant]); }); }, send: function(data, remoteUserId) { var that = this; if (!isNull(data.size) && !isNull(data.type)) { self.shareFile(data, remoteUserId); return; } if (data.type !== 'text' && !(data instanceof ArrayBuffer) && !(data instanceof DataView)) { TextSender.send({ text: data, channel: this, connection: connection, remoteUserId: remoteUserId }); return; } if (data.type === 'text') { data = JSON.stringify(data); } if (remoteUserId) { var remoteUser = connection.peers[remoteUserId]; if (remoteUser) { if (!remoteUser.channels.length) { connection.peers[remoteUserId].createDataChannel(); connection.renegotiate(remoteUserId); setTimeout(function() { that.send(data, remoteUserId); }, 3000); return; } remoteUser.channels.forEach(function(channel) { channel.send(data); }); return; } } this.getAllParticipants().forEach(function(participant) { if (!that[participant].channels.length) { connection.peers[participant].createDataChannel(); connection.renegotiate(participant); setTimeout(function() { that[participant].channels.forEach(function(channel) { channel.send(data); }); }, 3000); return; } that[participant].channels.forEach(function(channel) { channel.send(data); }); }); } }; this.uuid = connection.userid; this.getLocalConfig = function(remoteSdp, remoteUserId, userPreferences) { if (!userPreferences) { userPreferences = {}; } return { streamsToShare: userPreferences.streamsToShare || {}, rtcMultiConnection: connection, connectionDescription: userPreferences.connectionDescription, userid: remoteUserId, localPeerSdpConstraints: userPreferences.localPeerSdpConstraints, remotePeerSdpConstraints: userPreferences.remotePeerSdpConstraints, dontGetRemoteStream: !!userPreferences.dontGetRemoteStream, dontAttachLocalStream: !!userPreferences.dontAttachLocalStream, renegotiatingPeer: !!userPreferences.renegotiatingPeer, peerRef: userPreferences.peerRef, channels: userPreferences.channels || [], onLocalSdp: function(localSdp) { self.onNegotiationNeeded(localSdp, remoteUserId); }, onLocalCandidate: function(localCandidate) { localCandidate = OnIceCandidateHandler.processCandidates(connection, localCandidate) if (localCandidate) { self.onNegotiationNeeded(localCandidate, remoteUserId); } }, remoteSdp: remoteSdp, onDataChannelMessage: function(message) { if (!connection.fbr && connection.enableFileSharing) initFileBufferReader(); if (typeof message == 'string' || !connection.enableFileSharing) { self.onDataChannelMessage(message, remoteUserId); return; } var that = this; if (message instanceof ArrayBuffer || message instanceof DataView) { connection.fbr.convertToObject(message, function(object) { that.onDataChannelMessage(object); }); return; } if (message.readyForNextChunk) { connection.fbr.getNextChunk(message, function(nextChunk, isLastChunk) { connection.peers[remoteUserId].channels.forEach(function(channel) { channel.send(nextChunk); }); }, remoteUserId); return; } if (message.chunkMissing) { connection.fbr.chunkMissing(message); return; } connection.fbr.addChunk(message, function(promptNextChunk) { connection.peers[remoteUserId].peer.channel.send(promptNextChunk); }); }, onDataChannelError: function(error) { self.onDataChannelError(error, remoteUserId); }, onDataChannelOpened: function(channel) { self.onDataChannelOpened(channel, remoteUserId); }, onDataChannelClosed: function(event) { self.onDataChannelClosed(event, remoteUserId); }, onRemoteStream: function(stream) { if (connection.peers[remoteUserId]) { connection.peers[remoteUserId].streams.push(stream); } self.onGettingRemoteMedia(stream, remoteUserId); }, onRemoteStreamRemoved: function(stream) { self.onRemovingRemoteMedia(stream, remoteUserId); }, onPeerStateChanged: function(states) { self.onPeerStateChanged(states); if (states.iceConnectionState === 'new') { self.onNegotiationStarted(remoteUserId, states); } if (states.iceConnectionState === 'connected') { self.onNegotiationCompleted(remoteUserId, states); } if (states.iceConnectionState.search(/closed|failed/gi) !== -1) { self.onUserLeft(remoteUserId); self.disconnectWith(remoteUserId); } } }; }; this.createNewPeer = function(remoteUserId, userPreferences) { if (connection.maxParticipantsAllowed <= connection.getAllParticipants().length) { return; } userPreferences = userPreferences || {}; if (connection.isInitiator && !!connection.session.audio && connection.session.audio === 'two-way' && !userPreferences.streamsToShare) { userPreferences.isOneWay = false; userPreferences.isDataOnly = false; userPreferences.session = connection.session; } if (!userPreferences.isOneWay && !userPreferences.isDataOnly) { userPreferences.isOneWay = true; this.onNegotiationNeeded({ enableMedia: true, userPreferences: userPreferences }, remoteUserId); return; } userPreferences = connection.setUserPreferences(userPreferences, remoteUserId); var localConfig = this.getLocalConfig(null, remoteUserId, userPreferences); connection.peers[remoteUserId] = new PeerInitiator(localConfig); }; this.createAnsweringPeer = function(remoteSdp, remoteUserId, userPreferences) { userPreferences = connection.setUserPreferences(userPreferences || {}, remoteUserId); var localConfig = this.getLocalConfig(remoteSdp, remoteUserId, userPreferences); connection.peers[remoteUserId] = new PeerInitiator(localConfig); }; this.renegotiatePeer = function(remoteUserId, userPreferences, remoteSdp) { if (!connection.peers[remoteUserId]) { if (connection.enableLogs) { console.error('Peer (' + remoteUserId + ') does not exist. Renegotiation skipped.'); } return; } if (!userPreferences) { userPreferences = {}; } userPreferences.renegotiatingPeer = true; userPreferences.peerRef = connection.peers[remoteUserId].peer; userPreferences.channels = connection.peers[remoteUserId].channels; var localConfig = this.getLocalConfig(remoteSdp, remoteUserId, userPreferences); connection.peers[remoteUserId] = new PeerInitiator(localConfig); }; this.replaceTrack = function(track, remoteUserId, isVideoTrack) { if (!connection.peers[remoteUserId]) { throw 'This peer (' + remoteUserId + ') does not exist.'; } var peer = connection.peers[remoteUserId].peer; if (!!peer.getSenders && typeof peer.getSenders === 'function' && peer.getSenders().length) { peer.getSenders().forEach(function(rtpSender) { if (isVideoTrack && rtpSender.track instanceof VideoStreamTrack) { connection.peers[remoteUserId].peer.lastVideoTrack = rtpSender.track; rtpSender.replaceTrack(track); } if (!isVideoTrack && rtpSender.track instanceof AudioStreamTrack) { connection.peers[remoteUserId].peer.lastAudioTrack = rtpSender.track; rtpSender.replaceTrack(track); } }); return; } console.warn('RTPSender.replaceTrack is NOT supported.'); this.renegotiatePeer(remoteUserId); }; this.onNegotiationNeeded = function(message, remoteUserId) {}; this.addNegotiatedMessage = function(message, remoteUserId) { if (message.type && message.sdp) { if (message.type == 'answer') { if (connection.peers[remoteUserId]) { connection.peers[remoteUserId].addRemoteSdp(message); } } if (message.type == 'offer') { if (message.renegotiatingPeer) { this.renegotiatePeer(remoteUserId, null, message); } else { this.createAnsweringPeer(message, remoteUserId); } } if (connection.enableLogs) { console.log('Remote peer\'s sdp:', message.sdp); } return; } if (message.candidate) { if (connection.peers[remoteUserId]) { connection.peers[remoteUserId].addRemoteCandidate(message); } if (connection.enableLogs) { console.log('Remote peer\'s candidate pairs:', message.candidate); } return; } if (message.enableMedia) { connection.session = message.userPreferences.session || connection.session; if (connection.session.oneway && connection.attachStreams.length) { connection.attachStreams = []; } if (message.userPreferences.isDataOnly && connection.attachStreams.length) { connection.attachStreams.length = []; } var streamsToShare = {}; connection.attachStreams.forEach(function(stream) { streamsToShare[stream.streamid] = { isAudio: !!stream.isAudio, isVideo: !!stream.isVideo, isScreen: !!stream.isScreen }; }); message.userPreferences.streamsToShare = streamsToShare; self.onNegotiationNeeded({ readyForOffer: true, userPreferences: message.userPreferences }, remoteUserId); } if (message.readyForOffer) { connection.onReadyForOffer(remoteUserId, message.userPreferences); } function cb(stream) { gumCallback(stream, message, remoteUserId); } }; function gumCallback(stream, message, remoteUserId) { var streamsToShare = {}; connection.attachStreams.forEach(function(stream) { streamsToShare[stream.streamid] = { isAudio: !!stream.isAudio, isVideo: !!stream.isVideo, isScreen: !!stream.isScreen }; }); message.userPreferences.streamsToShare = streamsToShare; self.onNegotiationNeeded({ readyForOffer: true, userPreferences: message.userPreferences }, remoteUserId); } this.onGettingRemoteMedia = function(stream, remoteUserId) {}; this.onRemovingRemoteMedia = function(stream, remoteUserId) {}; this.onGettingLocalMedia = function(localStream) {}; this.onLocalMediaError = function(error, constraints) { connection.onMediaError(error, constraints); }; function initFileBufferReader() { connection.fbr = new FileBufferReader(); connection.fbr.onProgress = function(chunk) { connection.onFileProgress(chunk); }; connection.fbr.onBegin = function(file) { connection.onFileStart(file); }; connection.fbr.onEnd = function(file) { connection.onFileEnd(file); }; } this.shareFile = function(file, remoteUserId) { if (!connection.enableFileSharing) { throw '"connection.enableFileSharing" is false.'; } initFileBufferReader(); connection.fbr.readAsArrayBuffer(file, function(uuid) { var arrayOfUsers = connection.getAllParticipants(); if (remoteUserId) { arrayOfUsers = [remoteUserId]; } arrayOfUsers.forEach(function(participant) { connection.fbr.getNextChunk(uuid, function(nextChunk) { connection.peers[participant].channels.forEach(function(channel) { channel.send(nextChunk); }); }, participant); }); }, { userid: connection.userid, // extra: connection.extra, chunkSize: DetectRTC.browser.name === 'Firefox' ? 15 * 1000 : connection.chunkSize || 0 }); }; if (typeof 'TextReceiver' !== 'undefined') { var textReceiver = new TextReceiver(connection); } this.onDataChannelMessage = function(message, remoteUserId) { textReceiver.receive(JSON.parse(message), remoteUserId, connection.peers[remoteUserId] ? connection.peers[remoteUserId].extra : {}); }; this.onDataChannelClosed = function(event, remoteUserId) { event.userid = remoteUserId; event.extra = connection.peers[remoteUserId] ? connection.peers[remoteUserId].extra : {}; connection.onclose(event); }; this.onDataChannelError = function(error, remoteUserId) { error.userid = remoteUserId; event.extra = connection.peers[remoteUserId] ? connection.peers[remoteUserId].extra : {}; connection.onerror(error); }; this.onDataChannelOpened = function(channel, remoteUserId) { // keep last channel only; we are not expecting parallel/channels channels if (connection.peers[remoteUserId].channels.length) { connection.peers[remoteUserId].channels = [channel]; return; } connection.peers[remoteUserId].channels.push(channel); connection.onopen({ userid: remoteUserId, extra: connection.peers[remoteUserId] ? connection.peers[remoteUserId].extra : {}, channel: channel }); }; this.onPeerStateChanged = function(state) { connection.onPeerStateChanged(state); }; this.onNegotiationStarted = function(remoteUserId, states) {}; this.onNegotiationCompleted = function(remoteUserId, states) {}; this.getRemoteStreams = function(remoteUserId) { remoteUserId = remoteUserId || connection.peers.getAllParticipants()[0]; return connection.peers[remoteUserId] ? connection.peers[remoteUserId].streams : []; }; } 'use strict'; // Last Updated On: 2018-05-05 12:25:07 PM UTC // ________________ // DetectRTC v1.3.6 // Open-Sourced: https://github.com/muaz-khan/DetectRTC // -------------------------------------------------- // Muaz Khan - www.MuazKhan.com // MIT License - www.WebRTC-Experiment.com/licence // -------------------------------------------------- (function() { var browserFakeUserAgent = 'Fake/5.0 (FakeOS) AppleWebKit/123 (KHTML, like Gecko) Fake/12.3.4567.89 Fake/123.45'; var isNodejs = typeof process === 'object' && typeof process.versions === 'object' && process.versions.node && /*node-process*/ !process.browser; if (isNodejs) { var version = process.versions.node.toString().replace('v', ''); browserFakeUserAgent = 'Nodejs/' + version + ' (NodeOS) AppleWebKit/' + version + ' (KHTML, like Gecko) Nodejs/' + version + ' Nodejs/' + version } (function(that) { if (typeof window !== 'undefined') { return; } if (typeof window === 'undefined' && typeof global !== 'undefined') { global.navigator = { userAgent: browserFakeUserAgent, getUserMedia: function() {} }; /*global window:true */ that.window = global; } else if (typeof window === 'undefined') { // window = this; } if (typeof location === 'undefined') { /*global location:true */ that.location = { protocol: 'file:', href: '', hash: '' }; } if (typeof screen === 'undefined') { /*global screen:true */ that.screen = { width: 0, height: 0 }; } })(typeof global !== 'undefined' ? global : window); /*global navigator:true */ var navigator = window.navigator; if (typeof navigator !== 'undefined') { if (typeof navigator.webkitGetUserMedia !== 'undefined') { navigator.getUserMedia = navigator.webkitGetUserMedia; } if (typeof navigator.mozGetUserMedia !== 'undefined') { navigator.getUserMedia = navigator.mozGetUserMedia; } } else { navigator = { getUserMedia: function() {}, userAgent: browserFakeUserAgent }; } var isMobileDevice = !!(/Android|webOS|iPhone|iPad|iPod|BB10|BlackBerry|IEMobile|Opera Mini|Mobile|mobile/i.test(navigator.userAgent || '')); var isEdge = navigator.userAgent.indexOf('Edge') !== -1 && (!!navigator.msSaveOrOpenBlob || !!navigator.msSaveBlob); var isOpera = !!window.opera || navigator.userAgent.indexOf(' OPR/') >= 0; var isFirefox = typeof window.InstallTrigger !== 'undefined'; var isSafari = /^((?!chrome|android).)*safari/i.test(navigator.userAgent); var isChrome = !!window.chrome && !isOpera; var isIE = typeof document !== 'undefined' && !!document.documentMode && !isEdge; // this one can also be used: // https://www.websocket.org/js/stuff.js (DetectBrowser.js) function getBrowserInfo() { var nVer = navigator.appVersion; var nAgt = navigator.userAgent; var browserName = navigator.appName; var fullVersion = '' + parseFloat(navigator.appVersion); var majorVersion = parseInt(navigator.appVersion, 10); var nameOffset, verOffset, ix; // In Opera, the true version is after 'Opera' or after 'Version' if (isOpera) { browserName = 'Opera'; try { fullVersion = navigator.userAgent.split('OPR/')[1].split(' ')[0]; majorVersion = fullVersion.split('.')[0]; } catch (e) { fullVersion = '0.0.0.0'; majorVersion = 0; } } // In MSIE version <=10, the true version is after 'MSIE' in userAgent // In IE 11, look for the string after 'rv:' else if (isIE) { verOffset = nAgt.indexOf('rv:'); if (verOffset > 0) { //IE 11 fullVersion = nAgt.substring(verOffset + 3); } else { //IE 10 or earlier verOffset = nAgt.indexOf('MSIE'); fullVersion = nAgt.substring(verOffset + 5); } browserName = 'IE'; } // In Chrome, the true version is after 'Chrome' else if (isChrome) { verOffset = nAgt.indexOf('Chrome'); browserName = 'Chrome'; fullVersion = nAgt.substring(verOffset + 7); } // In Safari, the true version is after 'Safari' or after 'Version' else if (isSafari) { verOffset = nAgt.indexOf('Safari'); browserName = 'Safari'; fullVersion = nAgt.substring(verOffset + 7); if ((verOffset = nAgt.indexOf('Version')) !== -1) { fullVersion = nAgt.substring(verOffset + 8); } if (navigator.userAgent.indexOf('Version/') !== -1) { fullVersion = navigator.userAgent.split('Version/')[1].split(' ')[0]; } } // In Firefox, the true version is after 'Firefox' else if (isFirefox) { verOffset = nAgt.indexOf('Firefox'); browserName = 'Firefox'; fullVersion = nAgt.substring(verOffset + 8); } // In most other browsers, 'name/version' is at the end of userAgent else if ((nameOffset = nAgt.lastIndexOf(' ') + 1) < (verOffset = nAgt.lastIndexOf('/'))) { browserName = nAgt.substring(nameOffset, verOffset); fullVersion = nAgt.substring(verOffset + 1); if (browserName.toLowerCase() === browserName.toUpperCase()) { browserName = navigator.appName; } } if (isEdge) { browserName = 'Edge'; fullVersion = navigator.userAgent.split('Edge/')[1]; // fullVersion = parseInt(navigator.userAgent.match(/Edge\/(\d+).(\d+)$/)[2], 10).toString(); } // trim the fullVersion string at semicolon/space/bracket if present if ((ix = fullVersion.search(/[; \)]/)) !== -1) { fullVersion = fullVersion.substring(0, ix); } majorVersion = parseInt('' + fullVersion, 10); if (isNaN(majorVersion)) { fullVersion = '' + parseFloat(navigator.appVersion); majorVersion = parseInt(navigator.appVersion, 10); } return { fullVersion: fullVersion, version: majorVersion, name: browserName, isPrivateBrowsing: false }; } // via: https://gist.github.com/cou929/7973956 function retry(isDone, next) { var currentTrial = 0, maxRetry = 50, interval = 10, isTimeout = false; var id = window.setInterval( function() { if (isDone()) { window.clearInterval(id); next(isTimeout); } if (currentTrial++ > maxRetry) { window.clearInterval(id); isTimeout = true; next(isTimeout); } }, 10 ); } function isIE10OrLater(userAgent) { var ua = userAgent.toLowerCase(); if (ua.indexOf('msie') === 0 && ua.indexOf('trident') === 0) { return false; } var match = /(?:msie|rv:)\s?([\d\.]+)/.exec(ua); if (match && parseInt(match[1], 10) >= 10) { return true; } return false; } function detectPrivateMode(callback) { var isPrivate; try { if (window.webkitRequestFileSystem) { window.webkitRequestFileSystem( window.TEMPORARY, 1, function() { isPrivate = false; }, function(e) { isPrivate = true; } ); } else if (window.indexedDB && /Firefox/.test(window.navigator.userAgent)) { var db; try { db = window.indexedDB.open('test'); db.onerror = function() { return true; }; } catch (e) { isPrivate = true; } if (typeof isPrivate === 'undefined') { retry( function isDone() { return db.readyState === 'done' ? true : false; }, function next(isTimeout) { if (!isTimeout) { isPrivate = db.result ? false : true; } } ); } } else if (isIE10OrLater(window.navigator.userAgent)) { isPrivate = false; try { if (!window.indexedDB) { isPrivate = true; } } catch (e) { isPrivate = true; } } else if (window.localStorage && /Safari/.test(window.navigator.userAgent)) { try { window.localStorage.setItem('test', 1); } catch (e) { isPrivate = true; } if (typeof isPrivate === 'undefined') { isPrivate = false; window.localStorage.removeItem('test'); } } } catch (e) { isPrivate = false; } retry( function isDone() { return typeof isPrivate !== 'undefined' ? true : false; }, function next(isTimeout) { callback(isPrivate); } ); } var isMobile = { Android: function() { return navigator.userAgent.match(/Android/i); }, BlackBerry: function() { return navigator.userAgent.match(/BlackBerry|BB10/i); }, iOS: function() { return navigator.userAgent.match(/iPhone|iPad|iPod/i); }, Opera: function() { return navigator.userAgent.match(/Opera Mini/i); }, Windows: function() { return navigator.userAgent.match(/IEMobile/i); }, any: function() { return (isMobile.Android() || isMobile.BlackBerry() || isMobile.iOS() || isMobile.Opera() || isMobile.Windows()); }, getOsName: function() { var osName = 'Unknown OS'; if (isMobile.Android()) { osName = 'Android'; } if (isMobile.BlackBerry()) { osName = 'BlackBerry'; } if (isMobile.iOS()) { osName = 'iOS'; } if (isMobile.Opera()) { osName = 'Opera Mini'; } if (isMobile.Windows()) { osName = 'Windows'; } return osName; } }; // via: http://jsfiddle.net/ChristianL/AVyND/ function detectDesktopOS() { var unknown = '-'; var nVer = navigator.appVersion; var nAgt = navigator.userAgent; var os = unknown; var clientStrings = [{ s: 'Windows 10', r: /(Windows 10.0|Windows NT 10.0)/ }, { s: 'Windows 8.1', r: /(Windows 8.1|Windows NT 6.3)/ }, { s: 'Windows 8', r: /(Windows 8|Windows NT 6.2)/ }, { s: 'Windows 7', r: /(Windows 7|Windows NT 6.1)/ }, { s: 'Windows Vista', r: /Windows NT 6.0/ }, { s: 'Windows Server 2003', r: /Windows NT 5.2/ }, { s: 'Windows XP', r: /(Windows NT 5.1|Windows XP)/ }, { s: 'Windows 2000', r: /(Windows NT 5.0|Windows 2000)/ }, { s: 'Windows ME', r: /(Win 9x 4.90|Windows ME)/ }, { s: 'Windows 98', r: /(Windows 98|Win98)/ }, { s: 'Windows 95', r: /(Windows 95|Win95|Windows_95)/ }, { s: 'Windows NT 4.0', r: /(Windows NT 4.0|WinNT4.0|WinNT|Windows NT)/ }, { s: 'Windows CE', r: /Windows CE/ }, { s: 'Windows 3.11', r: /Win16/ }, { s: 'Android', r: /Android/ }, { s: 'Open BSD', r: /OpenBSD/ }, { s: 'Sun OS', r: /SunOS/ }, { s: 'Linux', r: /(Linux|X11)/ }, { s: 'iOS', r: /(iPhone|iPad|iPod)/ }, { s: 'Mac OS X', r: /Mac OS X/ }, { s: 'Mac OS', r: /(MacPPC|MacIntel|Mac_PowerPC|Macintosh)/ }, { s: 'QNX', r: /QNX/ }, { s: 'UNIX', r: /UNIX/ }, { s: 'BeOS', r: /BeOS/ }, { s: 'OS/2', r: /OS\/2/ }, { s: 'Search Bot', r: /(nuhk|Googlebot|Yammybot|Openbot|Slurp|MSNBot|Ask Jeeves\/Teoma|ia_archiver)/ }]; for (var i = 0, cs; cs = clientStrings[i]; i++) { if (cs.r.test(nAgt)) { os = cs.s; break; } } var osVersion = unknown; if (/Windows/.test(os)) { if (/Windows (.*)/.test(os)) { osVersion = /Windows (.*)/.exec(os)[1]; } os = 'Windows'; } switch (os) { case 'Mac OS X': if (/Mac OS X (10[\.\_\d]+)/.test(nAgt)) { osVersion = /Mac OS X (10[\.\