diff --git a/DetectRTC.js b/DetectRTC.js new file mode 100644 index 0000000..10938db --- /dev/null +++ b/DetectRTC.js @@ -0,0 +1,953 @@ + +(function() { + + 'use strict'; + + 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: 'Fake/5.0 (FakeOS) AppleWebKit/123 (KHTML, like Gecko) Fake/12.3.4567.89 Fake/123.45' + }; + } + + var isMobileDevice = !!(/Android|webOS|iPhone|iPad|iPod|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 = Object.prototype.toString.call(window.HTMLElement).indexOf('Constructor') > 0; + var isChrome = !!window.chrome && !isOpera; + var isIE = !!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, the true version is after 'MSIE' in userAgent + else if (isIE) { + verOffset = nAgt.indexOf('MSIE'); + browserName = 'IE'; + fullVersion = nAgt.substring(verOffset + 5); + } + // 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); + } + } + // 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 if present + if ((ix = fullVersion.indexOf(';')) !== -1) { + fullVersion = fullVersion.substring(0, ix); + } + + if ((ix = fullVersion.indexOf(' ')) !== -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; + + if (window.webkitRequestFileSystem) { + window.webkitRequestFileSystem( + window.TEMPORARY, 1, + function() { + isPrivate = false; + }, + function(e) { + console.log(e); + isPrivate = true; + } + ); + } else if (window.indexedDB && /Firefox/.test(window.navigator.userAgent)) { + var db; + try { + db = window.indexedDB.open('test'); + } 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'); + } + } + + 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/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 id in clientStrings) { + var cs = clientStrings[id]; + 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[\.\_\d]+)/.exec(nAgt)[1]; + } + break; + case 'Android': + if (/Android ([\.\_\d]+)/.test(nAgt)) { + osVersion = /Android ([\.\_\d]+)/.exec(nAgt)[1]; + } + break; + case 'iOS': + if (/OS (\d+)_(\d+)_?(\d+)?/.test(nAgt)) { + osVersion = /OS (\d+)_(\d+)_?(\d+)?/.exec(nVer); + osVersion = osVersion[1] + '.' + osVersion[2] + '.' + (osVersion[3] | 0); + } + break; + } + + return { + osName: os, + osVersion: osVersion + }; + } + + var osName = 'Unknown OS'; + var osVersion = 'Unknown OS Version'; + + if (isMobile.any()) { + osName = isMobile.getOsName(); + } else { + var osInfo = detectDesktopOS(); + osName = osInfo.osName; + osVersion = osInfo.osVersion; + } + + var isCanvasSupportsStreamCapturing = false; + var isVideoSupportsStreamCapturing = false; + ['captureStream', 'mozCaptureStream', 'webkitCaptureStream'].forEach(function(item) { + if (!isCanvasSupportsStreamCapturing && item in document.createElement('canvas')) { + isCanvasSupportsStreamCapturing = true; + } + + if (!isVideoSupportsStreamCapturing && item in document.createElement('video')) { + isVideoSupportsStreamCapturing = true; + } + }); + + // via: https://github.com/diafygi/webrtc-ips + function DetectLocalIPAddress(callback) { + if (!DetectRTC.isWebRTCSupported) { + return; + } + + if (DetectRTC.isORTCSupported) { + return; + } + + getIPs(function(ip) { + //local IPs + if (ip.match(/^(192\.168\.|169\.254\.|10\.|172\.(1[6-9]|2\d|3[01]))/)) { + callback('Local: ' + ip); + } + + //assume the rest are public IPs + else { + callback('Public: ' + ip); + } + }); + } + + //get the IP addresses associated with an account + function getIPs(callback) { + var ipDuplicates = {}; + + //compatibility for firefox and chrome + var RTCPeerConnection = window.RTCPeerConnection || window.mozRTCPeerConnection || window.webkitRTCPeerConnection; + var useWebKit = !!window.webkitRTCPeerConnection; + + // bypass naive webrtc blocking using an iframe + if (!RTCPeerConnection) { + var iframe = document.getElementById('iframe'); + if (!iframe) { + // + throw 'NOTE: you need to have an iframe in the page right above the script tag.'; + } + var win = iframe.contentWindow; + RTCPeerConnection = win.RTCPeerConnection || win.mozRTCPeerConnection || win.webkitRTCPeerConnection; + useWebKit = !!win.webkitRTCPeerConnection; + } + + // if still no RTCPeerConnection then it is not supported by the browser so just return + if (!RTCPeerConnection) { + return; + } + + //minimal requirements for data connection + var mediaConstraints = { + optional: [{ + RtpDataChannels: true + }] + }; + + //firefox already has a default stun server in about:config + // media.peerconnection.default_iceservers = + // [{"url": "stun:stun.services.mozilla.com"}] + var servers; + + //add same stun server for chrome + if (useWebKit) { + servers = { + iceServers: [{ + urls: 'stun:stun.services.mozilla.com' + }] + }; + + if (typeof DetectRTC !== 'undefined' && DetectRTC.browser.isFirefox && DetectRTC.browser.version <= 38) { + servers[0] = { + url: servers[0].urls + }; + } + } + + //construct a new RTCPeerConnection + var pc = new RTCPeerConnection(servers, mediaConstraints); + + function handleCandidate(candidate) { + //match just the IP address + var ipRegex = /([0-9]{1,3}(\.[0-9]{1,3}){3})/; + var match = ipRegex.exec(candidate); + if (!match) { + console.warn('Could not match IP address in', candidate); + return; + } + var ipAddress = match[1]; + + //remove duplicates + if (ipDuplicates[ipAddress] === undefined) { + callback(ipAddress); + } + + ipDuplicates[ipAddress] = true; + } + + //listen for candidate events + pc.onicecandidate = function(ice) { + //skip non-candidate events + if (ice.candidate) { + handleCandidate(ice.candidate.candidate); + } + }; + + //create a bogus data channel + pc.createDataChannel(''); + + //create an offer sdp + pc.createOffer(function(result) { + + //trigger the stun server request + pc.setLocalDescription(result, function() {}, function() {}); + + }, function() {}); + + //wait for a while to let everything done + setTimeout(function() { + //read candidate info from local description + var lines = pc.localDescription.sdp.split('\n'); + + lines.forEach(function(line) { + if (line.indexOf('a=candidate:') === 0) { + handleCandidate(line); + } + }); + }, 1000); + } + + var MediaDevices = []; + + var audioInputDevices = []; + var audioOutputDevices = []; + var videoInputDevices = []; + + if (navigator.mediaDevices && navigator.mediaDevices.enumerateDevices) { + // Firefox 38+ seems having support of enumerateDevices + // Thanks @xdumaine/enumerateDevices + navigator.enumerateDevices = function(callback) { + navigator.mediaDevices.enumerateDevices().then(callback); + }; + } + + // ---------- Media Devices detection + var canEnumerate = false; + + /*global MediaStreamTrack:true */ + if (typeof MediaStreamTrack !== 'undefined' && 'getSources' in MediaStreamTrack) { + canEnumerate = true; + } else if (navigator.mediaDevices && !!navigator.mediaDevices.enumerateDevices) { + canEnumerate = true; + } + + var hasMicrophone = false; + var hasSpeakers = false; + var hasWebcam = false; + + var isWebsiteHasMicrophonePermissions = false; + var isWebsiteHasWebcamPermissions = false; + + // http://dev.w3.org/2011/webrtc/editor/getusermedia.html#mediadevices + // todo: switch to enumerateDevices when landed in canary. + function checkDeviceSupport(callback) { + if (!canEnumerate) { + return; + } + + // This method is useful only for Chrome! + + if (!navigator.enumerateDevices && window.MediaStreamTrack && window.MediaStreamTrack.getSources) { + navigator.enumerateDevices = window.MediaStreamTrack.getSources.bind(window.MediaStreamTrack); + } + + if (!navigator.enumerateDevices && navigator.enumerateDevices) { + navigator.enumerateDevices = navigator.enumerateDevices.bind(navigator); + } + + if (!navigator.enumerateDevices) { + if (callback) { + callback(); + } + return; + } + + MediaDevices = []; + + audioInputDevices = []; + audioOutputDevices = []; + videoInputDevices = []; + + navigator.enumerateDevices(function(devices) { + devices.forEach(function(_device) { + var device = {}; + for (var d in _device) { + device[d] = _device[d]; + } + + // if it is MediaStreamTrack.getSources + if (device.kind === 'audio') { + device.kind = 'audioinput'; + } + + if (device.kind === 'video') { + device.kind = 'videoinput'; + } + + var skip; + MediaDevices.forEach(function(d) { + if (d.id === device.id && d.kind === device.kind) { + skip = true; + } + }); + + if (skip) { + return; + } + + if (!device.deviceId) { + device.deviceId = device.id; + } + + if (!device.id) { + device.id = device.deviceId; + } + + if (!device.label) { + device.label = 'Please invoke getUserMedia once.'; + if (location.protocol !== 'https:') { + if (document.domain.search && document.domain.search(/localhost|127.0./g) === -1) { + device.label = 'HTTPs is required to get label of this ' + device.kind + ' device.'; + } + } + } else { + if (device.kind === 'videoinput' && !isWebsiteHasWebcamPermissions) { + isWebsiteHasWebcamPermissions = true; + } + + if (device.kind === 'audioinput' && !isWebsiteHasMicrophonePermissions) { + isWebsiteHasMicrophonePermissions = true; + } + } + + if (device.kind === 'audioinput') { + hasMicrophone = true; + + if (audioInputDevices.indexOf(device) === -1) { + audioInputDevices.push(device); + } + } + + if (device.kind === 'audiooutput') { + hasSpeakers = true; + + if (audioOutputDevices.indexOf(device) === -1) { + audioOutputDevices.push(device); + } + } + + if (device.kind === 'videoinput') { + hasWebcam = true; + + if (videoInputDevices.indexOf(device) === -1) { + videoInputDevices.push(device); + } + } + + // there is no 'videoouput' in the spec. + + if (MediaDevices.indexOf(device) === -1) { + MediaDevices.push(device); + } + }); + + if (typeof DetectRTC !== 'undefined') { + // to sync latest outputs + DetectRTC.MediaDevices = MediaDevices; + DetectRTC.hasMicrophone = hasMicrophone; + DetectRTC.hasSpeakers = hasSpeakers; + DetectRTC.hasWebcam = hasWebcam; + + DetectRTC.isWebsiteHasWebcamPermissions = isWebsiteHasWebcamPermissions; + DetectRTC.isWebsiteHasMicrophonePermissions = isWebsiteHasMicrophonePermissions; + + DetectRTC.audioInputDevices = audioInputDevices; + DetectRTC.audioOutputDevices = audioOutputDevices; + DetectRTC.videoInputDevices = videoInputDevices; + } + + if (callback) { + callback(); + } + }); + } + + // check for microphone/camera support! + checkDeviceSupport(); + + var DetectRTC = window.DetectRTC || {}; + + // ---------- + // DetectRTC.browser.name || DetectRTC.browser.version || DetectRTC.browser.fullVersion + DetectRTC.browser = getBrowserInfo(); + + detectPrivateMode(function(isPrivateBrowsing) { + DetectRTC.browser.isPrivateBrowsing = !!isPrivateBrowsing; + }); + + // DetectRTC.isChrome || DetectRTC.isFirefox || DetectRTC.isEdge + DetectRTC.browser['is' + DetectRTC.browser.name] = true; + + var isNodeWebkit = !!(window.process && (typeof window.process === 'object') && window.process.versions && window.process.versions['node-webkit']); + + // --------- Detect if system supports WebRTC 1.0 or WebRTC 1.1. + var isWebRTCSupported = false; + ['RTCPeerConnection', 'webkitRTCPeerConnection', 'mozRTCPeerConnection', 'RTCIceGatherer'].forEach(function(item) { + if (isWebRTCSupported) { + return; + } + + if (item in window) { + isWebRTCSupported = true; + } + }); + DetectRTC.isWebRTCSupported = isWebRTCSupported; + + //------- + DetectRTC.isORTCSupported = typeof RTCIceGatherer !== 'undefined'; + + // --------- Detect if system supports screen capturing API + var isScreenCapturingSupported = false; + if (DetectRTC.browser.isChrome && DetectRTC.browser.version >= 35) { + isScreenCapturingSupported = true; + } else if (DetectRTC.browser.isFirefox && DetectRTC.browser.version >= 34) { + isScreenCapturingSupported = true; + } + + if (location.protocol !== 'https:') { + isScreenCapturingSupported = false; + } + DetectRTC.isScreenCapturingSupported = isScreenCapturingSupported; + + // --------- Detect if WebAudio API are supported + var webAudio = { + isSupported: false, + isCreateMediaStreamSourceSupported: false + }; + + ['AudioContext', 'webkitAudioContext', 'mozAudioContext', 'msAudioContext'].forEach(function(item) { + if (webAudio.isSupported) { + return; + } + + if (item in window) { + webAudio.isSupported = true; + + if ('createMediaStreamSource' in window[item].prototype) { + webAudio.isCreateMediaStreamSourceSupported = true; + } + } + }); + DetectRTC.isAudioContextSupported = webAudio.isSupported; + DetectRTC.isCreateMediaStreamSourceSupported = webAudio.isCreateMediaStreamSourceSupported; + + // ---------- Detect if SCTP/RTP channels are supported. + + var isRtpDataChannelsSupported = false; + if (DetectRTC.browser.isChrome && DetectRTC.browser.version > 31) { + isRtpDataChannelsSupported = true; + } + DetectRTC.isRtpDataChannelsSupported = isRtpDataChannelsSupported; + + var isSCTPSupportd = false; + if (DetectRTC.browser.isFirefox && DetectRTC.browser.version > 28) { + isSCTPSupportd = true; + } else if (DetectRTC.browser.isChrome && DetectRTC.browser.version > 25) { + isSCTPSupportd = true; + } else if (DetectRTC.browser.isOpera && DetectRTC.browser.version >= 11) { + isSCTPSupportd = true; + } + DetectRTC.isSctpDataChannelsSupported = isSCTPSupportd; + + // --------- + + DetectRTC.isMobileDevice = isMobileDevice; // "isMobileDevice" boolean is defined in "getBrowserInfo.js" + + // ------ + var isGetUserMediaSupported = false; + if (navigator.getUserMedia) { + isGetUserMediaSupported = true; + } else if (navigator.mediaDevices && navigator.mediaDevices.getUserMedia) { + isGetUserMediaSupported = true; + } + if (DetectRTC.browser.isChrome && DetectRTC.browser.version >= 46 && location.protocol !== 'https:') { + DetectRTC.isGetUserMediaSupported = 'Requires HTTPs'; + } + DetectRTC.isGetUserMediaSupported = isGetUserMediaSupported; + + // ----------- + DetectRTC.osName = osName; + DetectRTC.osVersion = osVersion; + + var displayResolution = ''; + if (screen.width) { + var width = (screen.width) ? screen.width : ''; + var height = (screen.height) ? screen.height : ''; + displayResolution += '' + width + ' x ' + height; + } + DetectRTC.displayResolution = displayResolution; + + // ---------- + DetectRTC.isCanvasSupportsStreamCapturing = isCanvasSupportsStreamCapturing; + DetectRTC.isVideoSupportsStreamCapturing = isVideoSupportsStreamCapturing; + + // ------ + DetectRTC.DetectLocalIPAddress = DetectLocalIPAddress; + + DetectRTC.isWebSocketsSupported = 'WebSocket' in window && 2 === window.WebSocket.CLOSING; + DetectRTC.isWebSocketsBlocked = !DetectRTC.isWebSocketsSupported; + + DetectRTC.checkWebSocketsSupport = function(callback) { + callback = callback || function() {}; + try { + var websocket = new WebSocket('wss://echo.websocket.org:443/'); + websocket.onopen = function() { + DetectRTC.isWebSocketsBlocked = false; + callback(); + websocket.close(); + websocket = null; + }; + websocket.onerror = function() { + DetectRTC.isWebSocketsBlocked = true; + callback(); + }; + } catch (e) { + DetectRTC.isWebSocketsBlocked = true; + callback(); + } + }; + + // ------- + DetectRTC.load = function(callback) { + callback = callback || function() {}; + checkDeviceSupport(callback); + }; + + DetectRTC.MediaDevices = MediaDevices; + DetectRTC.hasMicrophone = hasMicrophone; + DetectRTC.hasSpeakers = hasSpeakers; + DetectRTC.hasWebcam = hasWebcam; + + DetectRTC.isWebsiteHasWebcamPermissions = isWebsiteHasWebcamPermissions; + DetectRTC.isWebsiteHasMicrophonePermissions = isWebsiteHasMicrophonePermissions; + + DetectRTC.audioInputDevices = audioInputDevices; + DetectRTC.audioOutputDevices = audioOutputDevices; + DetectRTC.videoInputDevices = videoInputDevices; + + // ------ + var isSetSinkIdSupported = false; + if ('setSinkId' in document.createElement('video')) { + isSetSinkIdSupported = true; + } + DetectRTC.isSetSinkIdSupported = isSetSinkIdSupported; + + // ----- + var isRTPSenderReplaceTracksSupported = false; + if (DetectRTC.browser.isFirefox /*&& DetectRTC.browser.version > 39*/ ) { + /*global mozRTCPeerConnection:true */ + if ('getSenders' in mozRTCPeerConnection.prototype) { + isRTPSenderReplaceTracksSupported = true; + } + } else if (DetectRTC.browser.isChrome && typeof webkitRTCPeerConnection !== 'undefined') { + /*global webkitRTCPeerConnection:true */ + if ('getSenders' in webkitRTCPeerConnection.prototype) { + isRTPSenderReplaceTracksSupported = true; + } + } + DetectRTC.isRTPSenderReplaceTracksSupported = isRTPSenderReplaceTracksSupported; + + //------ + var isRemoteStreamProcessingSupported = false; + if (DetectRTC.browser.isFirefox && DetectRTC.browser.version > 38) { + isRemoteStreamProcessingSupported = true; + } + DetectRTC.isRemoteStreamProcessingSupported = isRemoteStreamProcessingSupported; + + //------- + var isApplyConstraintsSupported = false; + + /*global MediaStreamTrack:true */ + if (typeof MediaStreamTrack !== 'undefined' && 'applyConstraints' in MediaStreamTrack.prototype) { + isApplyConstraintsSupported = true; + } + DetectRTC.isApplyConstraintsSupported = isApplyConstraintsSupported; + + //------- + var isMultiMonitorScreenCapturingSupported = false; + if (DetectRTC.browser.isFirefox && DetectRTC.browser.version >= 43) { + // version 43 merely supports platforms for multi-monitors + // version 44 will support exact multi-monitor selection i.e. you can select any monitor for screen capturing. + isMultiMonitorScreenCapturingSupported = true; + } + DetectRTC.isMultiMonitorScreenCapturingSupported = isMultiMonitorScreenCapturingSupported; + + DetectRTC.isPromisesSupported = !!('Promise' in window); + + if (typeof DetectRTC === 'undefined') { + window.DetectRTC = {}; + } + + var MediaStream = window.MediaStream; + + if (typeof MediaStream === 'undefined' && typeof webkitMediaStream !== 'undefined') { + MediaStream = webkitMediaStream; + } + + if (typeof MediaStream !== 'undefined') { + DetectRTC.MediaStream = Object.keys(MediaStream.prototype); + } else DetectRTC.MediaStream = false; + + if (typeof MediaStreamTrack !== 'undefined') { + DetectRTC.MediaStreamTrack = Object.keys(MediaStreamTrack.prototype); + } else DetectRTC.MediaStreamTrack = false; + + var RTCPeerConnection = window.RTCPeerConnection || window.mozRTCPeerConnection || window.webkitRTCPeerConnection; + + if (typeof RTCPeerConnection !== 'undefined') { + DetectRTC.RTCPeerConnection = Object.keys(RTCPeerConnection.prototype); + } else DetectRTC.RTCPeerConnection = false; + + window.DetectRTC = DetectRTC; + +})(); diff --git a/RTCPeerConnection.js b/RTCPeerConnection.js new file mode 100644 index 0000000..980f625 --- /dev/null +++ b/RTCPeerConnection.js @@ -0,0 +1,310 @@ + +window.moz = !!navigator.mozGetUserMedia; +var chromeVersion = !!navigator.mozGetUserMedia ? 0 : parseInt(navigator.userAgent.match(/Chrom(e|ium)\/([0-9]+)\./)[2]); + +(function() { + window.RTCPeerConnection = function(options) { + var w = window, + PeerConnection = w.mozRTCPeerConnection || w.webkitRTCPeerConnection, + SessionDescription = w.mozRTCSessionDescription || w.RTCSessionDescription, + IceCandidate = w.mozRTCIceCandidate || w.RTCIceCandidate; + + var iceServers = { + iceServers: RTCPeerConnection.iceServers + }; + + console.debug('ice-servers', JSON.stringify(iceServers.iceServers, null, '\t')); + + var optional = { + optional: [] + }; + + console.debug('optional-arguments', JSON.stringify(optional.optional, null, '\t')); + + var peer = new PeerConnection(iceServers, optional); + + peer.onicecandidate = function(event) { + if (event.candidate) { + options.onICE(event.candidate); + } + }; + + // attachStream = MediaStream; + if (options.attachStream) peer.addStream(options.attachStream); + + // attachStreams[0] = audio-stream; + // attachStreams[1] = video-stream; + // attachStreams[2] = screen-capturing-stream; + if (options.attachStreams && options.attachStream.length) { + var streams = options.attachStreams; + for (var i = 0; i < streams.length; i++) { + peer.addStream(streams[i]); + } + } + + peer.onaddstream = function(event) { + setTimeout(function() { + var remoteMediaStream = event.stream; + + // onRemoteStreamEnded(MediaStream) + remoteMediaStream.onended = function() { + if (options.onRemoteStreamEnded) options.onRemoteStreamEnded(remoteMediaStream); + }; + + // onRemoteStream(MediaStream) + if (options.onRemoteStream) options.onRemoteStream(remoteMediaStream); + + console.debug('on:add:stream', remoteMediaStream); + }, 2000); + }; + + var OfferToReceiveAudio = false; + var OfferToReceiveVideo = false; + if(peer.getLocalStreams()[0] && peer.getLocalStreams()[0].getAudioTracks().length) { + OfferToReceiveAudio = true; + } + + if(peer.getLocalStreams()[0] && peer.getLocalStreams()[0].getVideoTracks().length) { + OfferToReceiveVideo = true; + } + + var firefoxVersion = 50; + matchArray = navigator.userAgent.match(/Firefox\/(.*)/); + if (moz && matchArray && matchArray[1]) { + firefoxVersion = parseInt(matchArray[1], 10); + } + + var sdpConstraints = options.constraints || { + optional: [], + mandatory: { + OfferToReceiveAudio: OfferToReceiveAudio, + OfferToReceiveVideo: OfferToReceiveVideo + } + }; + + if(moz && firefoxVersion > 34) { + sdpConstraints = { + OfferToReceiveAudio: OfferToReceiveAudio, + OfferToReceiveVideo: OfferToReceiveVideo + }; + } + + console.debug('sdp-constraints', JSON.stringify(sdpConstraints, null, '\t')); + + // onOfferSDP(RTCSessionDescription) + + function createOffer() { + if (!options.onOfferSDP) return; + + peer.createOffer(function(sessionDescription) { + sessionDescription.sdp = setBandwidth(sessionDescription.sdp); + peer.setLocalDescription(sessionDescription); + options.onOfferSDP(sessionDescription); + + console.debug('offer-sdp', sessionDescription.sdp); + }, onSdpError, sdpConstraints); + } + + // onAnswerSDP(RTCSessionDescription) + + function createAnswer() { + if (!options.onAnswerSDP) return; + + //options.offerSDP.sdp = addStereo(options.offerSDP.sdp); + console.debug('offer-sdp', options.offerSDP.sdp); + peer.setRemoteDescription(new SessionDescription(options.offerSDP), onSdpSuccess, onSdpError); + peer.createAnswer(function(sessionDescription) { + sessionDescription.sdp = setBandwidth(sessionDescription.sdp); + peer.setLocalDescription(sessionDescription); + options.onAnswerSDP(sessionDescription); + console.debug('answer-sdp', sessionDescription.sdp); + }, onSdpError, sdpConstraints); + } + + // options.bandwidth = { audio: 50, video: 256, data: 30 * 1000 * 1000 } + var bandwidth = options.bandwidth; + + function setBandwidth(sdp) { + if (moz || !bandwidth /* || navigator.userAgent.match( /Android|iPhone|iPad|iPod|BlackBerry|IEMobile/i ) */ ) return sdp; + + // remove existing bandwidth lines + sdp = sdp.replace(/b=AS([^\r\n]+\r\n)/g, ''); + + if (bandwidth.audio) { + sdp = sdp.replace(/a=mid:audio\r\n/g, 'a=mid:audio\r\nb=AS:' + bandwidth.audio + '\r\n'); + } + + if (bandwidth.video) { + sdp = sdp.replace(/a=mid:video\r\n/g, 'a=mid:video\r\nb=AS:' + bandwidth.video + '\r\n'); + } + + if (bandwidth.data) { + sdp = sdp.replace(/a=mid:data\r\n/g, 'a=mid:data\r\nb=AS:' + bandwidth.data + '\r\n'); + } + + return sdp; + } + + // DataChannel management + var channel; + + if (!!options.onChannelMessage) { + peer.ondatachannel = function(event) { + channel = event.channel; + setChannelEvents(); + }; + openOffererChannel(); + } + + createOffer(); + createAnswer(); + + function openOffererChannel() { + channel = peer.createDataChannel(options.channel || 'RTCDataChannel', {}); + setChannelEvents(); + } + + function setChannelEvents() { + channel.onmessage = function(event) { + if (options.onChannelMessage) options.onChannelMessage(event); + }; + + channel.onopen = function() { + if (options.onChannelOpened && !options.onChannelOpenInvoked) { + options.onChannelOpenInvoked = true; + options.onChannelOpened(channel); + } + }; + channel.onclose = function(event) { + if (options.onChannelClosed) options.onChannelClosed(event); + + console.warn('WebRTC DataChannel closed', event); + }; + channel.onerror = function(event) { + if (options.onChannelError) options.onChannelError(event); + + console.error('WebRTC DataChannel error', event); + }; + } + + function onSdpSuccess() {} + + function onSdpError(e) { + console.error('onSdpError:', JSON.stringify(e, null, '\t')); + } + + return { + addAnswerSDP: function(sdp) { + console.debug('adding answer-sdp', sdp.sdp); + peer.setRemoteDescription(new SessionDescription(sdp), onSdpSuccess, onSdpError); + }, + addICE: function(candidate) { + peer.addIceCandidate(new IceCandidate({ + sdpMLineIndex: candidate.sdpMLineIndex, + candidate: candidate.candidate + })); + + console.debug('adding-ice', candidate.candidate); + }, + + peer: peer, + channel: channel, + sendData: function(message) { + channel && channel.send(message); + } + }; + } + + // getUserMedia + var video_constraints = { + mandatory: {}, + optional: [] + }; + + window.getUserMedia = function(options) { + var n = navigator, + media; + n.getMedia = n.webkitGetUserMedia || n.mozGetUserMedia; + n.getMedia(options.constraints || { + audio: true, + video: video_constraints + }, streaming, options.onerror || function(e) { + console.error(e); + }); + + function streaming(stream) { + var video = options.video; + if (video) { + video[moz ? 'mozSrcObject' : 'src'] = moz ? stream : window.webkitURL.createObjectURL(stream); + video.play(); + } + options.onsuccess && options.onsuccess(stream); + media = stream; + } + + return media; + } +})(); + +function listenEventHandler(eventName, eventHandler) { + window.removeEventListener(eventName, eventHandler); + window.addEventListener(eventName, eventHandler, false); +} + +var loadedIceFrame; + +function loadIceFrame(callback) { + if (loadedIceFrame) { + return; + } + + loadedIceFrame = true; + + var iframe = document.createElement('iframe'); + iframe.onload = function() { + iframe.isLoaded = true; + + listenEventHandler('message', iFrameLoaderCallback); + + function iFrameLoaderCallback(event) { + if (!event.data || !event.data.iceServers) { + return; + } + callback(event.data.iceServers); + + // this event listener is no more needed + window.removeEventListener('message', iFrameLoaderCallback); + } + + iframe.contentWindow.postMessage('get-ice-servers', '*'); + }; + iframe.src = 'https://cdn.webrtc-experiment.com/getIceServers/'; + iframe.style.display = 'none'; + (document.body || document.documentElement).appendChild(iframe); +} + +RTCPeerConnection.iceServers = []; + +RTCPeerConnection.iceServers.push({ + url: 'stun:stun.l.google.com:19302' +}); + +RTCPeerConnection.iceServers.push({ + url: 'stun:stun.anyfirewall.com:3478' +}); + +RTCPeerConnection.iceServers.push({ + url: 'turn:turn.bistri.com:80', + credential: 'homeo', + username: 'homeo' +}); + +RTCPeerConnection.iceServers.push({ + url: 'turn:turn.anyfirewall.com:443?transport=tcp', + credential: 'webrtc', + username: 'webrtc' +}); + +loadIceFrame(function(servers) { + RTCPeerConnection.iceServers = RTCPeerConnection.iceServers.concat(servers); +}); diff --git a/broadcast.js b/broadcast.js new file mode 100644 index 0000000..7406bb8 --- /dev/null +++ b/broadcast.js @@ -0,0 +1,238 @@ + + +var broadcast = function(config) { + var self = { + userToken: uniqueToken() + }, + channels = '--', + isbroadcaster, + isGetNewRoom = true, + participants = 1, + defaultSocket = { }; + + function openDefaultSocket() { + defaultSocket = config.openSocket({ + onmessage: onDefaultSocketResponse, + callback: function(socket) { + defaultSocket = socket; + } + }); + } + + function onDefaultSocketResponse(response) { + if (response.userToken == self.userToken) return; + + if (isGetNewRoom && response.roomToken && response.broadcaster) config.onRoomFound(response); + + if (response.userToken && response.joinUser == self.userToken && response.participant && channels.indexOf(response.userToken) == -1) { + channels += response.userToken + '--'; + openSubSocket({ + isofferer: true, + channel: response.channel || response.userToken, + closeSocket: true + }); + } + } + + function openSubSocket(_config) { + if (!_config.channel) return; + var socketConfig = { + channel: _config.channel, + onmessage: socketResponse, + onopen: function() { + if (isofferer && !peer) initPeer(); + } + }; + + socketConfig.callback = function(_socket) { + socket = _socket; + this.onopen(); + }; + + var socket = config.openSocket(socketConfig), + isofferer = _config.isofferer, + gotstream, + htmlElement = document.createElement(self.isAudio ? 'audio' : 'video'), + inner = { }, + peer; + + var peerConfig = { + constraints: { + mandatory: { + OfferToReceiveAudio: true, + OfferToReceiveVideo: true + }, + optional: [] + }, + attachStream: config.attachStream, + onICE: function(candidate) { + socket.send({ + userToken: self.userToken, + candidate: { + sdpMLineIndex: candidate.sdpMLineIndex, + candidate: JSON.stringify(candidate.candidate) + } + }); + }, + onRemoteStream: function(stream) { + if (!stream) return; + + htmlElement[moz ? 'mozSrcObject' : 'src'] = moz ? stream : webkitURL.createObjectURL(stream); + htmlElement.play(); + + _config.stream = stream; + if (self.isAudio) { + htmlElement.addEventListener('play', function() { + this.muted = false; + this.volume = 1; + afterRemoteStreamStartedFlowing(); + }, false); + } else onRemoteStreamStartsFlowing(); + } + }; + + function initPeer(offerSDP) { + if (!offerSDP) { + peerConfig.onOfferSDP = sendsdp; + } else { + peerConfig.offerSDP = offerSDP; + peerConfig.onAnswerSDP = sendsdp; + } + + peer = RTCPeerConnection(peerConfig); + } + + function onRemoteStreamStartsFlowing() { + if(navigator.userAgent.match(/Android|iPhone|iPad|iPod|BlackBerry|IEMobile/i)) { + // if mobile device + return afterRemoteStreamStartedFlowing(); + } + + if (!(htmlElement.readyState <= HTMLMediaElement.HAVE_CURRENT_DATA || htmlElement.paused || htmlElement.currentTime <= 0)) { + afterRemoteStreamStartedFlowing(); + } else setTimeout(onRemoteStreamStartsFlowing, 50); + } + + function afterRemoteStreamStartedFlowing() { + gotstream = true; + config.onRemoteStream(htmlElement); + + /* closing subsocket here on the offerer side */ + if (_config.closeSocket) socket = null; + } + + function sendsdp(sdp) { + sdp = JSON.stringify(sdp); + var part = parseInt(sdp.length / 3); + + var firstPart = sdp.slice(0, part), + secondPart = sdp.slice(part, sdp.length - 1), + thirdPart = ''; + + if (sdp.length > part + part) { + secondPart = sdp.slice(part, part + part); + thirdPart = sdp.slice(part + part, sdp.length); + } + + socket.send({ + userToken: self.userToken, + firstPart: firstPart + }); + + socket.send({ + userToken: self.userToken, + secondPart: secondPart + }); + + socket.send({ + userToken: self.userToken, + thirdPart: thirdPart + }); + } + + function socketResponse(response) { + if (response.userToken == self.userToken) return; + if (response.firstPart || response.secondPart || response.thirdPart) { + if (response.firstPart) { + inner.firstPart = response.firstPart; + if (inner.secondPart && inner.thirdPart) selfInvoker(); + } + if (response.secondPart) { + inner.secondPart = response.secondPart; + if (inner.firstPart && inner.thirdPart) selfInvoker(); + } + + if (response.thirdPart) { + inner.thirdPart = response.thirdPart; + if (inner.firstPart && inner.secondPart) selfInvoker(); + } + } + + if (response.candidate && !gotstream) { + peer && peer.addICE({ + sdpMLineIndex: response.candidate.sdpMLineIndex, + candidate: JSON.parse(response.candidate.candidate) + }); + } + } + + var invokedOnce = false; + + function selfInvoker() { + if (invokedOnce) return; + + invokedOnce = true; + + inner.sdp = JSON.parse(inner.firstPart + inner.secondPart + inner.thirdPart); + if (isofferer) { + peer.addAnswerSDP(inner.sdp); + if (config.onNewParticipant) config.onNewParticipant(participants++); + } else initPeer(inner.sdp); + } + } + + function startBroadcasting() { + defaultSocket && defaultSocket.send({ + roomToken: self.roomToken, + roomName: self.roomName, + broadcaster: self.userToken, + isAudio: self.isAudio + }); + setTimeout(startBroadcasting, 3000); + } + + function uniqueToken() { + var s4 = function() { + return Math.floor(Math.random() * 0x10000).toString(16); + }; + return s4() + s4() + "-" + s4() + "-" + s4() + "-" + s4() + "-" + s4() + s4() + s4(); + } + + openDefaultSocket(); + return { + createRoom: function(_config) { + self.roomName = _config.roomName || 'Anonymous'; + self.isAudio = _config.isAudio; + self.roomToken = uniqueToken(); + + isbroadcaster = true; + isGetNewRoom = false; + startBroadcasting(); + }, + joinRoom: function(_config) { + self.roomToken = _config.roomToken; + self.isAudio = _config.isAudio; + isGetNewRoom = false; + + openSubSocket({ + channel: self.userToken + }); + + defaultSocket.send({ + participant: true, + userToken: self.userToken, + joinUser: _config.joinUser + }); + } + }; +}; diff --git a/firebase.js b/firebase.js new file mode 100644 index 0000000..107a15b --- /dev/null +++ b/firebase.js @@ -0,0 +1,128 @@ +(function() {function g(a){throw a;}var j=void 0,k=!0,l=null,o=!1;function aa(a){return function(){return this[a]}}function p(a){return function(){return a}}var r,ba=this;function ca(a,b){var c=a.split("."),d=ba;!(c[0]in d)&&d.execScript&&d.execScript("var "+c[0]);for(var e;c.length&&(e=c.shift());)!c.length&&s(b)?d[e]=b:d=d[e]?d[e]:d[e]={}}function da(){} +function ea(a){var b=typeof a;if("object"==b)if(a){if(a instanceof Array)return"array";if(a instanceof Object)return b;var c=Object.prototype.toString.call(a);if("[object Window]"==c)return"object";if("[object Array]"==c||"number"==typeof a.length&&"undefined"!=typeof a.splice&&"undefined"!=typeof a.propertyIsEnumerable&&!a.propertyIsEnumerable("splice"))return"array";if("[object Function]"==c||"undefined"!=typeof a.call&&"undefined"!=typeof a.propertyIsEnumerable&&!a.propertyIsEnumerable("call"))return"function"}else return"null"; +else if("function"==b&&"undefined"==typeof a.call)return"object";return b}function s(a){return a!==j}function fa(a){var b=ea(a);return"array"==b||"object"==b&&"number"==typeof a.length}function t(a){return"string"==typeof a}function ga(a){return"number"==typeof a}function ha(a){var b=typeof a;return"object"==b&&a!=l||"function"==b}Math.floor(2147483648*Math.random()).toString(36);function ia(a,b,c){return a.call.apply(a.bind,arguments)} +function ja(a,b,c){a||g(Error());if(2b?e+="000":256>b?e+="00":4096>b&&(e+="0");return pa[a]=e+b.toString(16)}),'"')};function y(a){if("undefined"!==typeof JSON&&s(JSON.stringify))a=JSON.stringify(a);else{var b=[];na(new ma,a,b);a=b.join("")}return a};function ra(a){for(var b=[],c=0,d=0;d=e&&(e-=55296,d++,z(de?b[c++]=e:(2048>e?b[c++]=e>>6|192:(65536>e?b[c++]=e>>12|224:(b[c++]=e>>18|240,b[c++]=e>>12&63|128),b[c++]=e>>6&63|128),b[c++]=e&63|128)}return b};function A(a,b,c,d){var e;dc&&(e=0===c?"none":"no more than "+c);e&&g(Error(a+" failed: Was called with "+d+(1===d?" argument.":" arguments.")+" Expects "+e+"."))}function B(a,b,c){var d="";switch(b){case 1:d=c?"first":"First";break;case 2:d=c?"second":"Second";break;case 3:d=c?"third":"Third";break;case 4:d=c?"fourth":"Fourth";break;default:sa.assert(o,"errorPrefix_ called with argumentNumber > 4. Need to update it?")}return a+" failed: "+(d+" argument ")} +function C(a,b,c,d){(!d||s(c))&&"function"!=ea(c)&&g(Error(B(a,b,d)+"must be a valid function."))}function ta(a,b,c){s(c)&&(!ha(c)||c===l)&&g(Error(B(a,b,k)+"must be a valid context object."))};function D(a,b){return Object.prototype.hasOwnProperty.call(a,b)}function ua(a,b){if(Object.prototype.hasOwnProperty.call(a,b))return a[b]};var va={},sa={},wa=/[\[\].#$\/]/,xa=/[\[\].#$]/;function ya(a){return t(a)&&0!==a.length&&!wa.test(a)}function za(a,b,c){(!c||s(b))&&Aa(B(a,1,c),b)} +function Aa(a,b,c,d){c||(c=0);d||(d=[]);s(b)||g(Error(a+"contains undefined"+Ba(d)));"function"==ea(b)&&g(Error(a+"contains a function"+Ba(d)));Ca(b)&&g(Error(a+"contains "+b.toString()+Ba(d)));1E310485760/3&&10485760=a)&&g("Query.limit: First argument must be a positive integer.");return new G(this.o,this.path,a,this.Z,this.la,this.ra,this.Pa)};G.prototype.limit=G.prototype.rd;G.prototype.Ed=function(a,b){A("Query.startAt",0,2,arguments.length);Ea("Query.startAt",1,a,k);Ga("Query.startAt",b);s(a)||(b=a=l);return new G(this.o,this.path,this.ta,a,b,this.ra,this.Pa)};G.prototype.startAt=G.prototype.Ed; +G.prototype.ld=function(a,b){A("Query.endAt",0,2,arguments.length);Ea("Query.endAt",1,a,k);Ga("Query.endAt",b);return new G(this.o,this.path,this.ta,this.Z,this.la,a,b)};G.prototype.endAt=G.prototype.ld;function Ja(a){var b={};s(a.Z)&&(b.sp=a.Z);s(a.la)&&(b.sn=a.la);s(a.ra)&&(b.ep=a.ra);s(a.Pa)&&(b.en=a.Pa);s(a.ta)&&(b.l=a.ta);s(a.Z)&&(s(a.la)&&a.Z===l&&a.la===l)&&(b.vf="l");return b}G.prototype.Ia=function(){var a=Ka(Ja(this));return"{}"===a?"default":a}; +function Ia(a,b,c){var d={};b&&c?(d.cancel=b,C(a,3,d.cancel,k),d.W=c,ta(a,4,d.W)):b&&("object"===typeof b&&b!==l?d.W=b:"function"===typeof b?d.cancel=b:g(Error(B(a,3,k)+"must either be a cancel callback or a context object.")));return d};function I(a){if(a instanceof I)return a;if(1==arguments.length){this.m=a.split("/");for(var b=0,c=0;c=a.m.length?l:a.m[a.X]}function La(a){var b=a.X;b=this.m.length)return l;for(var a=[],b=this.X;b=this.m.length};function Na(a,b){var c=F(a);if(c===l)return b;if(c===F(b))return Na(La(a),La(b));g("INTERNAL ERROR: innerPath ("+b+") is not within outerPath ("+a+")")}r.contains=function(a){var b=0;if(this.m.length>a.m.length)return o;for(;bb?1:0}r=Va.prototype;r.ia=function(a,b){return new Va(this.Ma,this.Y.ia(a,b,this.Ma).copy(l,l,o,l,l))};r.remove=function(a){return new Va(this.Ma,this.Y.remove(a,this.Ma).copy(l,l,o,l,l))};r.get=function(a){for(var b,c=this.Y;!c.f();){b=this.Ma(a,c.key);if(0===b)return c.value;0>b?c=c.left:0c?d=d.left:0d?e.copy(l,l,l,e.left.ia(a,b,c),l):0===d?e.copy(l,b,l,l,l):e.copy(l,l,l,l,e.right.ia(a,b,c));return cb(e)};function db(a){if(a.left.f())return Xa;!a.left.H()&&!a.left.left.H()&&(a=eb(a));a=a.copy(l,l,l,db(a.left),l);return cb(a)} +r.remove=function(a,b){var c,d;c=this;if(0>b(a,c.key))!c.left.f()&&(!c.left.H()&&!c.left.left.H())&&(c=eb(c)),c=c.copy(l,l,l,c.left.remove(a,b),l);else{c.left.H()&&(c=gb(c));!c.right.f()&&(!c.right.H()&&!c.right.left.H())&&(c=hb(c),c.left.left.H()&&(c=gb(c),c=hb(c)));if(0===b(a,c.key)){if(c.right.f())return Xa;d=bb(c.right);c=c.copy(d.key,d.value,l,l,db(c.right))}c=c.copy(l,l,l,l,c.right.remove(a,b))}return cb(c)};r.H=aa("color"); +function cb(a){a.right.H()&&!a.left.H()&&(a=ib(a));a.left.H()&&a.left.left.H()&&(a=gb(a));a.left.H()&&a.right.H()&&(a=hb(a));return a}function eb(a){a=hb(a);a.right.left.H()&&(a=a.copy(l,l,l,l,gb(a.right)),a=ib(a),a=hb(a));return a}function ib(a){var b;b=a.copy(l,l,k,l,a.right.left);return a.right.copy(l,l,a.color,b,l)}function gb(a){var b;b=a.copy(l,l,k,a.left.right,l);return a.left.copy(l,l,a.color,l,b)} +function hb(a){var b,c;b=a.left.copy(l,l,!a.left.color,l,l);c=a.right.copy(l,l,!a.right.color,l,l);return a.copy(l,l,!a.color,b,c)}function jb(){}r=jb.prototype;r.copy=function(){return this};r.ia=function(a,b){return new ab(a,b,j,j,j)};r.remove=function(){return this};r.get=p(l);r.count=p(0);r.f=p(k);r.sa=p(o);r.Ja=p(o);r.ib=p(l);r.Sa=p(l);r.H=p(o);var Xa=new jb;var kb=Array.prototype,lb=kb.forEach?function(a,b,c){kb.forEach.call(a,b,c)}:function(a,b,c){for(var d=a.length,e=t(a)?a.split(""):a,f=0;fa;++a)this.Ob[a]=0;this.reset()}ka(ob,nb);ob.prototype.reset=function(){this.z[0]=1732584193;this.z[1]=4023233417;this.z[2]=2562383102;this.z[3]=271733878;this.z[4]=3285377520;this.Ac=this.eb=0}; +function pb(a,b){var c;c||(c=0);for(var d=a.hd,e=c;ee;e++){var f=d[e-3]^d[e-8]^d[e-14]^d[e-16];d[e]=(f<<1|f>>>31)&4294967295}c=a.z[0];for(var h=a.z[1],i=a.z[2],m=a.z[3],n=a.z[4],q,e=0;80>e;e++)40>e?20>e?(f=m^h&(i^m),q=1518500249):(f=h^i^m,q=1859775393):60>e?(f=h&i|m&(h|i),q=2400959708):(f=h^i^m,q=3395469782),f=(c<<5|c>>>27)+f+n+q+d[e]&4294967295,n=m,m=i,i=(h<<30|h>>>2)&4294967295,h=c,c=f;a.z[0]=a.z[0]+c&4294967295;a.z[1]=a.z[1]+h& +4294967295;a.z[2]=a.z[2]+i&4294967295;a.z[3]=a.z[3]+m&4294967295;a.z[4]=a.z[4]+n&4294967295}ob.prototype.update=function(a,b){s(b)||(b=a.length);var c=this.gc,d=this.eb,e=0;if(t(a))for(;ec;c++)Gb[c]="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=".charAt(c),Hb[c]="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_.".charAt(c)}for(var c=b?Hb:Gb,d=[],e=0;e>2,f=(f&3)<<4|i>>4,i=(i&15)<<2|n>>6,n=n&63;m||(n=64,h||(i=64));d.push(c[q],c[f],c[i],c[n])}return d.join("")} +;var Jb,Kb=1;Jb=function(){return Kb++};function z(a,b){a||g(Error("Firebase INTERNAL ASSERT FAILED:"+b))}function Lb(a){var b=ra(a),a=new ob;a.update(b);var b=[],c=8*a.Ac;56>a.eb?a.update(a.Ob,56-a.eb):a.update(a.Ob,64-(a.eb-56));for(var d=63;56<=d;d--)a.gc[d]=c&255,c/=256;pb(a,a.gc);for(d=c=0;5>d;d++)for(var e=24;0<=e;e-=8)b[c++]=a.z[d]>>e&255;return Ib(b)} +function Mb(){for(var a="",b=0;bb?1:-1:0}function Xb(a,b){if(b&&a in b)return b[a];g(Error("Missing required key ("+a+") in object: "+y(b)))}var Yb=0;function Ka(a){if("object"!==typeof a||a===l)return y(a);var b=[],c;for(c in a)b.push(c);b.sort();c="{";for(var d=0;da?c.push(a.substring(d,a.length)):c.push(a.substring(d,d+b));return c} +function $b(a){z(!Ca(a));var b,c,d,e;0===a?(d=c=0,b=-Infinity===1/a?1:0):(b=0>a,a=Math.abs(a),a>=Math.pow(2,-1022)?(d=Math.min(Math.floor(Math.log(a)/Math.LN2),1023),c=d+1023,d=Math.round(a*Math.pow(2,52-d)-Math.pow(2,52))):(c=0,d=Math.round(a/Math.pow(2,-1074))));e=[];for(a=52;a;a-=1)e.push(d%2?1:0),d=Math.floor(d/2);for(a=11;a;a-=1)e.push(c%2?1:0),c=Math.floor(c/2);e.push(b?1:0);e.reverse();b=e.join("");c="";for(a=0;64>a;a+=8)d=parseInt(b.substr(a,8),2).toString(16),1===d.length&&(d="0"+d),c+=d; +return c.toLowerCase()};function ac(a,b){this.oa=a;z(this.oa!==l,"LeafNode shouldn't be created with null value.");this.Ua="undefined"!==typeof b?b:l}r=ac.prototype;r.J=p(k);r.k=aa("Ua");r.ec=function(a){return new ac(this.oa,a)};r.N=function(){return O};r.F=function(a){return F(a)===l?this:O};r.T=p(l);r.D=function(a,b){return(new P(new Va,this.Ua)).D(a,b)};r.Ya=function(a,b){var c=F(a);return c===l?b:this.D(c,O.Ya(La(a),b))};r.f=p(o);r.Hb=p(0); +r.P=function(a){return a&&this.k()!==l?{".value":this.j(),".priority":this.k()}:this.j()};r.hash=function(){var a="";this.k()!==l&&(a+="priority:"+bc(this.k())+":");var b=typeof this.oa,a=a+(b+":"),a="number"===b?a+$b(this.oa):a+this.oa;return Lb(a)};r.j=aa("oa");r.toString=function(){return"string"===typeof this.oa?'"'+this.oa+'"':this.oa};function P(a,b){this.R=a||new Va;this.Ua="undefined"!==typeof b?b:l}r=P.prototype;r.J=p(o);r.k=aa("Ua");r.ec=function(a){return new P(this.R,a)};r.D=function(a,b){var c=this.R.remove(a);b&&b.f()&&(b=l);b!==l&&(c=c.ia(a,b));return b&&b.k()!==l?new cc(c,l,this.Ua):new P(c,this.Ua)};r.Ya=function(a,b){var c=F(a);if(c===l)return b;var d=this.N(c).Ya(La(a),b);return this.D(c,d)};r.f=function(){return this.R.f()};r.Hb=function(){return this.R.count()};var dc=/^\d+$/;r=P.prototype; +r.P=function(a){if(this.f())return l;var b={},c=0,d=0,e=k;this.B(function(f,h){b[f]=h.P(a);c++;e&&dc.test(f)?d=Math.max(d,Number(f)):e=o});if(!a&&e&&d<2*c){var f=[],h;for(h in b)f[h]=b[h];return f}a&&this.k()!==l&&(b[".priority"]=this.k());return b};r.hash=function(){var a="";this.k()!==l&&(a+="priority:"+bc(this.k())+":");this.B(function(b,c){var d=c.hash();""!==d&&(a+=":"+b+":"+d)});return""===a?"":Lb(a)};r.N=function(a){a=this.R.get(a);return a===l?O:a}; +r.F=function(a){var b=F(a);return b===l?this:this.N(b).F(La(a))};r.T=function(a){return Ya(this.R,a)};r.Kc=function(){return this.R.ib()};r.Lc=function(){return this.R.Sa()};r.B=function(a){return this.R.sa(a)};r.lc=function(a){return this.R.Ja(a)};r.Qa=function(){return this.R.Qa()};r.toString=function(){var a="{",b=k;this.B(function(c,d){b?b=o:a+=", ";a+='"'+c+'" : '+d.toString()});return a+="}"};var O=new P(new Va);function cc(a,b,c){P.call(this,a,c);b===l&&(b=new Va(ec),a.sa(function(a,c){b=b.ia({name:a,wa:c.k()},c)}));this.ka=b}ka(cc,P);r=cc.prototype;r.D=function(a,b){var c=this.N(a),d=this.R,e=this.ka;c!==l&&(d=d.remove(a),e=e.remove({name:a,wa:c.k()}));b&&b.f()&&(b=l);b!==l&&(d=d.ia(a,b),e=e.ia({name:a,wa:b.k()},b));return new cc(d,e,this.k())};r.T=function(a,b){var c=Ya(this.ka,{name:a,wa:b.k()});return c?c.name:l};r.B=function(a){return this.ka.sa(function(b,c){return a(b.name,c)})}; +r.lc=function(a){return this.ka.Ja(function(b,c){return a(b.name,c)})};r.Qa=function(){return this.ka.Qa(function(a,b){return{key:a.name,value:b}})};r.Kc=function(){return this.ka.f()?l:this.ka.ib().name};r.Lc=function(){return this.ka.f()?l:this.ka.Sa().name};function Q(a,b){if("object"!==typeof a)return new ac(a,b);if(a===l)return O;var c=l;".priority"in a?c=a[".priority"]:"undefined"!==typeof b&&(c=b);z(c===l||"string"===typeof c||"number"===typeof c);if(".value"in a&&a[".value"]!==l)return new ac(a[".value"],c);var c=new P(new Va,c),d;for(d in a)if(D(a,d)&&"."!==d.substring(0,1)){var e=Q(a[d]);if(e.J()||!e.f())c=c.D(d,e)}return c}function ec(a,b){return Wb(a.wa,b.wa)||(a.name!==b.name?a.name=a.length){var b=Number(a);if(!isNaN(b)){c.zc= +b;c.frames=[];a=l;break a}}c.zc=1;c.frames=[]}a!==l&&sc(c,a)}};this.U.onerror=function(){c.e("WebSocket error. Closing connection.");c.Ha()}};pc.prototype.start=function(){};pc.isAvailable=function(){return!("undefined"!==typeof navigator&&"Opera"===navigator.appName)&&oc!==l&&!qc};function sc(a,b){a.frames.push(b);if(a.frames.length==a.zc){var c=a.frames.join("");a.frames=l;c="undefined"!==typeof JSON&&s(JSON.parse)?JSON.parse(c):la(c);a.Mb(c)}} +pc.prototype.send=function(a){rc(this);a=y(a);ic(this.$,"bytes_sent",a.length);a=Zb(a,16384);1"),this.ca.qa.close()}catch(e){Pb("frame writing exception"),e.stack&&Pb(e.stack),Pb(e)}}zc.prototype.close=function(){this.fc=o;if(this.ca){this.ca.qa.body.innerHTML="";var a=this;setTimeout(function(){a.ca!==l&&(document.body.removeChild(a.ca),a.ca=l)},0)}var b=this.da;b&&(this.da=l,b())}; +function Bc(a){if(a.fc&&a.Zc&&a.tc.count()<(0=a.qb[0].Fc.length+30+c.length){var e=a.qb.shift(),c=c+"&seg"+d+"="+e.Ad+"&ts"+d+"="+e.Gd+"&d"+d+"="+e.Fc;d++}else break;var b=b+c,f=a.kc;a.tc.add(f);var h=function(){a.tc.remove(f);Bc(a)},i=setTimeout(h,25E3);Ac(a,b,function(){clearTimeout(i);h()});return k}return o} +function Ac(a,b,c){setTimeout(function(){try{var d=a.ca.qa.createElement("script");d.type="text/javascript";d.async=k;d.src=b;d.onload=d.onreadystatechange=function(){var a=d.readyState;if(!a||"loaded"===a||"complete"===a)d.onload=d.onreadystatechange=l,d.parentNode&&d.parentNode.removeChild(d),c()};d.onerror=function(){Pb("Long-poll script failed to load.");a.close()};a.ca.qa.body.appendChild(d)}catch(e){}},1)};function Cc(){function a(a,c){c&&c.isAvailable()&&b.push(c)}var b=[],c=Dc;if("array"==ea(c))for(var d=0;dc)f=ua(x,K.key),s(f)?(n.push({Jc:K,cd:i[f]}),i[f]=l):(v[K.key]=m.length,m.push(K)),f=k,K=$a(w);else{if(0=d.la}):c.push(function(a,b){return 0<=Wb(b,d.Z)}));s(d.ra)&&(s(d.Pa)?c.push(function(a,b){var c=Wb(b,d.ra);return 0>c||0===c&&a<=d.Pa}):c.push(function(a,b){return 0>=Wb(b,d.ra)}));var e=l,f=l;if(s(this.I.ta))if(s(this.I.Z)){if(e=qd(a,c,this.I.ta,o)){var h=a.N(e).k();c.push(function(a,b){var c=Wb(b,h);return 0>c||0===c&&a<=e})}}else if(f=qd(a,c,this.I.ta, +k)){var i=a.N(f).k();c.push(function(a,b){var c=Wb(b,i);return 0=f})}for(var m=[],n=[],q=[],x=[],v=0;vd;d++)Rd[d]=Math.floor(64*Math.random());for(d=0;12>d;d++)a+="-0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ_abcdefghijklmnopqrstuvwxyz".charAt(Rd[d]);z(20===a.length,"NextPushId: Length should be 20.");return a};function W(){var a,b,c;if(arguments[0]instanceof Cd)c=arguments[0],a=arguments[1];else{A("new Firebase",1,2,arguments.length);var d=arguments[0];b=a="";var e=k,f="";if(t(d)){var h=d.indexOf("//");if(0<=h)var i=d.substring(0,h-1),d=d.substring(h+2);h=d.indexOf("/");-1===h&&(h=d.length);a=d.substring(0,h);var d=d.substring(h+1),m=a.split(".");if(3==m.length){h=m[2].indexOf(":");e=0<=h?"https"===i:k;if("firebase"===m[1])Ub(a+" is no longer supported. Please use .firebaseio.com instead"); +else{b=m[0];f="";d=("/"+d).split("/");for(i=0;i + + + Broadcasting + + + + + + + + + + + + + + + + + + + + +
+
+

Broadcasting

+
+ +
+
+ Media:    New broadcast name: +     + +
+ + +
+ + +
+
+ + + + +
+ + diff --git a/style.css b/style.css new file mode 100644 index 0000000..d46aebc --- /dev/null +++ b/style.css @@ -0,0 +1,565 @@ +@font-face { + font-family: 'Myriad'; + src: url('https://www.webrtc-experiment.com/fonts/MyriadPro-Light.otf') format("opentype"); + font-weight: 400; +} + +html { + background: #eee; +} + +body { + font-family: Myriad, Arial, Verdana; + font-size: 1.2em; + line-height: 1.5em; + margin: 0; +} + +article, footer { + display: block; + max-width: 900px; + min-width: 360px; + width: 80%; +} + +article { + background: #fff; + border: 1px solid; + border-color: #ddd #aaa #aaa #ddd; + margin: 2.5em auto 0 auto; + padding: 2em; +} + +h1 { + margin-top: 0; +} + +article p:first-of-type { + margin-top: 1.6em; +} + +article p:last-child { + margin-bottom: 0; +} + +footer { + margin: 0 auto 2em auto; + text-align: center; +} + +footer a { + color: #666; + font-size: inherit; + padding: 1em; + text-decoration: none; + text-shadow: 0 1px 1px #fff; +} + +footer a:hover, footer a:focus { + color: #111; +} + +h1, h2 { + border-bottom: 1px solid rgb(189, 189, 189); + display: inline; + font-weight: normal; + line-height: 36px; + padding: 0 0 3px 0; +} + +a { + color: #2844FA; + text-decoration: none; +} + +a:hover, a:focus { + color: #1B29A4; +} + +a:active { + color: #000; +} + +:-moz-any-link:focus { + border: 0; + color: #000; +} + +::selection { + background: #ccc; +} + +::-moz-selection { + background: #ccc; +} + +button, input[type=button] { + -moz-border-radius: 3px; + -moz-transition: none; + -webkit-transition: none; + background: #0370ea; + background: -moz-linear-gradient(top, #008dfd 0, #0370ea 100%); + background: -webkit-linear-gradient(top, #008dfd 0, #0370ea 100%); + border: 1px solid #076bd2; + border-radius: 3px; + color: #fff; + display: inline-block; + font-family: inherit; + font-size: .8em; + line-height: 1.3; + padding: 5px 12px; + text-align: center; + text-shadow: 1px 1px 1px #076bd2; + font-size: 1.5em; +} + +button:hover, input[type=button]:hover { + background: rgb(9, 147, 240); +} + +button:active, input[type=button]:active { + background: rgb(10, 118, 190); +} + +button[disabled], input[type=button][disabled] { + background: none; + border: 1px solid rgb(187, 181, 181); + color: gray; + text-shadow: none; +} + +strong { + color: rgb(204, 14, 14); + font-family: inherit; + font-weight: normal; +} + +tr, td, th { + vertical-align: top; + padding: .7em 1.4em; + border-top: 1px dotted #BBA9A9; + border-right: 1px dotted #BBA9A9; +} + +table { + width: 100%; +} + +.logo img { + border-radius: 50%; + box-shadow: 0 0 5px black, 0 0 5px black, 0 0 5px black, 0 0 5px black, 0 0 5px black; +} + +.experiment { + border: 1px solid rgb(189, 189, 189); + margin: 1em 3em; + border-radius: .2em; + text-align: left; +} + +.experiment .header { + padding: .2em .4em; +} + +.experiment .description { + padding: .8em 1.4em; +} + +ol { + margin-left: 1em; +} + +pre { + border-left: 2px solid red; + margin-left: 2em; + padding-left: 1em; + overflow: auto; +} + +.commit { + font-size: .8em; + margin: 1em .6em; + padding: 8px 8px 0; + background: #e6f1f6; + border: 1px solid #c5d5dd; + border-radius: 4px; +} + +.commit-desc { + display: block; + margin: -5px 0 10px 0; +} + +.commit-desc img { + max-width: 100%; +} + +.commit-meta { + margin-left: -8px; + width: 100%; + padding: 8px; + background: #fff; + border-top: 1px solid #d8e6ec; + border-bottom-right-radius: 4px; + border-bottom-left-radius: 4px; +} + +.authorship { + margin-top: -2px; + margin-left: -4px; + margin-bottom: -4px; + font-size: 14px; + color: #999; +} + +.gravatar { + margin-top: -2px; + margin-right: 3px; + vertical-align: middle; + border-radius: 3px; +} + +.author-name { + color: #444; +} + +.commit-url { + float: right; + margin-left: 15px; + color: #888; + font-size: 12px; +} + +.dim { + color: rgb(223, 223, 223); +} + +.roshan { + color: red; +} + +.github-stargazers { + position: absolute; + right: 14%; + top: 8%; + font: bold 11px/14px "Helvetica Neue", Helvetica, Arial, sans-serif; + text-rendering: optimizeLegibility; + overflow: hidden; +} + +.github-btn { + height: 20px; + overflow: hidden; +} + +.gh-btn, +.gh-count, +.gh-ico { + float: left; + margin-left: 5px; +} + +.gh-btn, +.gh-count { + padding: 2px 5px 2px 4px; + color: #555; + text-decoration: none; + text-shadow: 0 1px 0 #fff; + white-space: nowrap; + cursor: pointer; + border-radius: 3px; +} + +.gh-btn { + background-color: #e6e6e6; + background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#fafafa), to(#eaeaea)); + background-image: -webkit-linear-gradient(#fafafa, #eaeaea); + background-image: -moz-linear-gradient(top, #fafafa, #eaeaea); + background-image: -ms-linear-gradient(#fafafa, #eaeaea); + background-image: -o-linear-gradient(#fafafa, #eaeaea); + background-image: linear-gradient(#fafafa, #eaeaea); + background-repeat: no-repeat; + border: 1px solid #d4d4d4; + border-bottom-color: #bcbcbc; +} + +.gh-btn:hover, +.gh-btn:focus, +.gh-btn:active { + color: #fff; + text-decoration: none; + text-shadow: 0 -1px 0 rgba(0,0,0,.25); + border-color: #518cc6 #518cc6 #2a65a0; + background-color: #3072b3; +} + +.gh-btn:hover, +.gh-btn:focus { + background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#599bdc), to(#3072b3)); + background-image: -webkit-linear-gradient(#599bdc, #3072b3); + background-image: -moz-linear-gradient(top, #599bdc, #3072b3); + background-image: -ms-linear-gradient(#599bdc, #3072b3); + background-image: -o-linear-gradient(#599bdc, #3072b3); + background-image: linear-gradient(#599bdc, #3072b3); +} + +.gh-btn:active { + background-image: none; + -webkit-box-shadow: inset 0 2px 5px rgba(0,0,0,.10); + -moz-box-shadow: inset 0 2px 5px rgba(0,0,0,.10); + box-shadow: inset 0 2px 5px rgba(0,0,0,.10); +} + +.gh-ico { + width: 14px; + height: 15px; + margin-top: -1px; + margin-right: 4px; + vertical-align: middle; + background-image: url(data:image/png;base64,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); + background-repeat: no-repeat; + background-position: 0 0; +} + +.gh-btn:hover .gh-ico, +.gh-btn:focus .gh-ico, +.gh-btn:active .gh-ico { + background-position: -25px 0; +} + +.gh-count { + position: relative; + margin-left: 0px; + background-color: #fafafa; + border: 1px solid #d4d4d4; +} + +.gh-count:hover, +.gh-count:focus { + color: #4183C4; +} + +.gh-count:before, +.gh-count:after { + content: ' '; + position: absolute; + display: inline-block; + width: 0; + height: 0; + border-color: transparent; + border-style: solid; +} + +.gh-count:before { + top: 50%; + left: -3px; + margin-top: -4px; + border-width: 4px 4px 4px 0; + border-right-color: #fafafa; +} + +.gh-count:after { + top: 50%; + left: -4px; + z-index: -1; + margin-top: -5px; + border-width: 5px 5px 5px 0; + border-right-color: #d4d4d4; +} + +.github-btn-large { + height: 30px; +} + +.github-btn-large .gh-btn, +.github-btn-large .gh-count { + padding: 3px 10px 3px 8px; + font-size: 16px; + line-height: 22px; + border-radius: 4px; +} + +.github-btn-large .gh-ico { + width: 22px; + height: 23px; + background-position: 0 -20px; +} + +.github-btn-large .gh-btn:hover .gh-ico, +.github-btn-large .gh-btn:focus .gh-ico, +.github-btn-large .gh-btn:active .gh-ico { + background-position: -25px -20px; +} + +.github-btn-large .gh-count { + margin-left: 6px; +} + +.github-btn-large .gh-count:before { + left: -5px; + margin-top: -6px; + border-width: 6px 6px 6px 0; +} + +.github-btn-large .gh-count:after { + left: -6px; + margin-top: -7px; + border-width: 7px 7px 7px 0; +} + +@media (-moz-min-device-pixel-ratio: 2), (-o-min-device-pixel-ratio: 2/1), (-webkit-min-device-pixel-ratio: 2), (min-device-pixel-ratio: 2) { + .gh-ico { + background-image: url(data:image/png;base64,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); + background-size: 50px 45px; + }; +} + +.plusone-gplus { + position: absolute; + top: 8%; +} + +@media all and (max-width: 800px) { + body { + font-size: 1.1em; + } + + article { + margin: 1.5em auto 0 auto; + padding: 1.5em; + } + + .experiment { + margin: 1em .2em; + } +} + +@media all and (max-width: 500px) { + body { + font-size: .9em; + } + + article { + margin: .5em auto 0 auto; + padding: .5em; + } + + .experiment { + margin: 1em .1em; + } +} + +@media all and (max-width: 300px) { + body { + font-size: .8em; + } + + article { + margin: 0 auto 0 auto; + padding: 0; + } + + .experiment { + margin: 1em 0; + } +} + +@media all and (min-width: 1300px) { + .latest-commits { + position: fixed; + left: -3em; + bottom: -1em; + height: 50%; + overflow: auto; + width: 20%; + font-size: 1em; + } +} + +li pre { + margin: 0; +} + +li h2 { + color: red; +} + +li li h2 { + font-size: 1em; + color: rgb(6, 101, 243); +} + +.fork-left, .fork-right { + background-repeat: no-repeat; + background-position: center center; + width: 140px; + height: 140px; +} + +.fork-left { + position: absolute; + top: 0; + left: 0; + background: url('https://cdn.webrtc-experiment.com/images/fork-left.png'); +} + +.fork-right { + position: absolute; + top: 0; + right: 0; + background: url('https://cdn.webrtc-experiment.com/images/fork-right.png'); +} + +select { + border: 1px solid #d9d9d9; + border-radius: 1px; + height: 50px; + margin-left: 1em; + margin-right: -5px; + padding: 1.1em; + vertical-align: 6px; +} + +p { + padding: 0 .8em; + padding-bottom: .4em; +} + +li { + border-bottom: 1px solid rgb(189, 189, 189); + border-left: 1px solid rgb(189, 189, 189); + padding: .5em; +} + +code { + font-size: 1.2em; +} + +.commit pre { + border: 1px dotted black; + margin: 1em; + font-size: 1.2em; +} + +blockquote { + background: rgb(241, 241, 241); + padding: 1em; + border: 1px dotted gray; + margin: 0 1em; +} + +.answer { + border-left: 1px dotted gray; + margin-left: 5em; + padding: 0 1em; +} + +pre a { + text-decoration: underline; +} + +blockquote.inline { + margin: 1em; + border-radius: 3px; + box-shadow: 2px 2px rgb(182, 170, 170); +} \ No newline at end of file