commit 077a7d127ed28d4dac6aec0227e8a32ccc98ccfd Author: Artyom Abubakirov Date: Mon May 28 23:27:12 2018 +0500 init project diff --git a/CodecsHandler.js b/CodecsHandler.js new file mode 100755 index 0000000..37bd9b1 --- /dev/null +++ b/CodecsHandler.js @@ -0,0 +1,361 @@ +// CodecsHandler.js + +var CodecsHandler = (function() { + function preferCodec(sdp, codecName) { + var info = splitLines(sdp); + + if (!info.videoCodecNumbers) { + return sdp; + } + + if (codecName === 'vp8' && info.vp8LineNumber === info.videoCodecNumbers[0]) { + return sdp; + } + + if (codecName === 'vp9' && info.vp9LineNumber === info.videoCodecNumbers[0]) { + return sdp; + } + + if (codecName === 'h264' && info.h264LineNumber === info.videoCodecNumbers[0]) { + return sdp; + } + + sdp = preferCodecHelper(sdp, codecName, info); + + return sdp; + } + + function preferCodecHelper(sdp, codec, info, ignore) { + var preferCodecNumber = ''; + + if (codec === 'vp8') { + if (!info.vp8LineNumber) { + return sdp; + } + preferCodecNumber = info.vp8LineNumber; + } + + if (codec === 'vp9') { + if (!info.vp9LineNumber) { + return sdp; + } + preferCodecNumber = info.vp9LineNumber; + } + + if (codec === 'h264') { + if (!info.h264LineNumber) { + return sdp; + } + + preferCodecNumber = info.h264LineNumber; + } + + var newLine = info.videoCodecNumbersOriginal.split('SAVPF')[0] + 'SAVPF '; + + var newOrder = [preferCodecNumber]; + + if (ignore) { + newOrder = []; + } + + info.videoCodecNumbers.forEach(function(codecNumber) { + if (codecNumber === preferCodecNumber) return; + newOrder.push(codecNumber); + }); + + newLine += newOrder.join(' '); + + sdp = sdp.replace(info.videoCodecNumbersOriginal, newLine); + return sdp; + } + + function splitLines(sdp) { + var info = {}; + sdp.split('\n').forEach(function(line) { + if (line.indexOf('m=video') === 0) { + info.videoCodecNumbers = []; + line.split('SAVPF')[1].split(' ').forEach(function(codecNumber) { + codecNumber = codecNumber.trim(); + if (!codecNumber || !codecNumber.length) return; + info.videoCodecNumbers.push(codecNumber); + info.videoCodecNumbersOriginal = line; + }); + } + + if (line.indexOf('VP8/90000') !== -1 && !info.vp8LineNumber) { + info.vp8LineNumber = line.replace('a=rtpmap:', '').split(' ')[0]; + } + + if (line.indexOf('VP9/90000') !== -1 && !info.vp9LineNumber) { + info.vp9LineNumber = line.replace('a=rtpmap:', '').split(' ')[0]; + } + + if (line.indexOf('H264/90000') !== -1 && !info.h264LineNumber) { + info.h264LineNumber = line.replace('a=rtpmap:', '').split(' ')[0]; + } + }); + + return info; + } + + function removeVPX(sdp) { + var info = splitLines(sdp); + + // last parameter below means: ignore these codecs + sdp = preferCodecHelper(sdp, 'vp9', info, true); + sdp = preferCodecHelper(sdp, 'vp8', info, true); + + return sdp; + } + + function disableNACK(sdp) { + if (!sdp || typeof sdp !== 'string') { + throw 'Invalid arguments.'; + } + + sdp = sdp.replace('a=rtcp-fb:126 nack\r\n', ''); + sdp = sdp.replace('a=rtcp-fb:126 nack pli\r\n', 'a=rtcp-fb:126 pli\r\n'); + sdp = sdp.replace('a=rtcp-fb:97 nack\r\n', ''); + sdp = sdp.replace('a=rtcp-fb:97 nack pli\r\n', 'a=rtcp-fb:97 pli\r\n'); + + return sdp; + } + + function prioritize(codecMimeType, peer) { + if (!peer || !peer.getSenders || !peer.getSenders().length) { + return; + } + + if (!codecMimeType || typeof codecMimeType !== 'string') { + throw 'Invalid arguments.'; + } + + peer.getSenders().forEach(function(sender) { + var params = sender.getParameters(); + for (var i = 0; i < params.codecs.length; i++) { + if (params.codecs[i].mimeType == codecMimeType) { + params.codecs.unshift(params.codecs.splice(i, 1)); + break; + } + } + sender.setParameters(params); + }); + } + + function removeNonG722(sdp) { + return sdp.replace(/m=audio ([0-9]+) RTP\/SAVPF ([0-9 ]*)/g, 'm=audio $1 RTP\/SAVPF 9'); + } + + function setBAS(sdp, bandwidth, isScreen) { + if (!bandwidth) { + return sdp; + } + + if (typeof isFirefox !== 'undefined' && isFirefox) { + return sdp; + } + + if (isScreen) { + if (!bandwidth.screen) { + console.warn('It seems that you are not using bandwidth for screen. Screen sharing is expected to fail.'); + } else if (bandwidth.screen < 300) { + console.warn('It seems that you are using wrong bandwidth value for screen. Screen sharing is expected to fail.'); + } + } + + // if screen; must use at least 300kbs + if (bandwidth.screen && isScreen) { + sdp = sdp.replace(/b=AS([^\r\n]+\r\n)/g, ''); + sdp = sdp.replace(/a=mid:video\r\n/g, 'a=mid:video\r\nb=AS:' + bandwidth.screen + '\r\n'); + } + + // remove existing bandwidth lines + if (bandwidth.audio || bandwidth.video) { + 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.screen) { + sdp = sdp.replace(/a=mid:video\r\n/g, 'a=mid:video\r\nb=AS:' + bandwidth.screen + '\r\n'); + } else if (bandwidth.video) { + sdp = sdp.replace(/a=mid:video\r\n/g, 'a=mid:video\r\nb=AS:' + bandwidth.video + '\r\n'); + } + + return sdp; + } + + // Find the line in sdpLines that starts with |prefix|, and, if specified, + // contains |substr| (case-insensitive search). + function findLine(sdpLines, prefix, substr) { + return findLineInRange(sdpLines, 0, -1, prefix, substr); + } + + // Find the line in sdpLines[startLine...endLine - 1] that starts with |prefix| + // and, if specified, contains |substr| (case-insensitive search). + function findLineInRange(sdpLines, startLine, endLine, prefix, substr) { + var realEndLine = endLine !== -1 ? endLine : sdpLines.length; + for (var i = startLine; i < realEndLine; ++i) { + if (sdpLines[i].indexOf(prefix) === 0) { + if (!substr || + sdpLines[i].toLowerCase().indexOf(substr.toLowerCase()) !== -1) { + return i; + } + } + } + return null; + } + + // Gets the codec payload type from an a=rtpmap:X line. + function getCodecPayloadType(sdpLine) { + var pattern = new RegExp('a=rtpmap:(\\d+) \\w+\\/\\d+'); + var result = sdpLine.match(pattern); + return (result && result.length === 2) ? result[1] : null; + } + + function setVideoBitrates(sdp, params) { + params = params || {}; + var xgoogle_min_bitrate = params.min; + var xgoogle_max_bitrate = params.max; + + var sdpLines = sdp.split('\r\n'); + + // VP8 + var vp8Index = findLine(sdpLines, 'a=rtpmap', 'VP8/90000'); + var vp8Payload; + if (vp8Index) { + vp8Payload = getCodecPayloadType(sdpLines[vp8Index]); + } + + if (!vp8Payload) { + return sdp; + } + + var rtxIndex = findLine(sdpLines, 'a=rtpmap', 'rtx/90000'); + var rtxPayload; + if (rtxIndex) { + rtxPayload = getCodecPayloadType(sdpLines[rtxIndex]); + } + + if (!rtxIndex) { + return sdp; + } + + var rtxFmtpLineIndex = findLine(sdpLines, 'a=fmtp:' + rtxPayload.toString()); + if (rtxFmtpLineIndex !== null) { + var appendrtxNext = '\r\n'; + appendrtxNext += 'a=fmtp:' + vp8Payload + ' x-google-min-bitrate=' + (xgoogle_min_bitrate || '228') + '; x-google-max-bitrate=' + (xgoogle_max_bitrate || '228'); + sdpLines[rtxFmtpLineIndex] = sdpLines[rtxFmtpLineIndex].concat(appendrtxNext); + sdp = sdpLines.join('\r\n'); + } + + return sdp; + } + + function setOpusAttributes(sdp, params) { + params = params || {}; + + var sdpLines = sdp.split('\r\n'); + + // Opus + var opusIndex = findLine(sdpLines, 'a=rtpmap', 'opus/48000'); + var opusPayload; + if (opusIndex) { + opusPayload = getCodecPayloadType(sdpLines[opusIndex]); + } + + if (!opusPayload) { + return sdp; + } + + var opusFmtpLineIndex = findLine(sdpLines, 'a=fmtp:' + opusPayload.toString()); + if (opusFmtpLineIndex === null) { + return sdp; + } + + var appendOpusNext = ''; + appendOpusNext += '; stereo=' + (typeof params.stereo != 'undefined' ? params.stereo : '1'); + appendOpusNext += '; sprop-stereo=' + (typeof params['sprop-stereo'] != 'undefined' ? params['sprop-stereo'] : '1'); + + if (typeof params.maxaveragebitrate != 'undefined') { + appendOpusNext += '; maxaveragebitrate=' + (params.maxaveragebitrate || 128 * 1024 * 8); + } + + if (typeof params.maxplaybackrate != 'undefined') { + appendOpusNext += '; maxplaybackrate=' + (params.maxplaybackrate || 128 * 1024 * 8); + } + + if (typeof params.cbr != 'undefined') { + appendOpusNext += '; cbr=' + (typeof params.cbr != 'undefined' ? params.cbr : '1'); + } + + if (typeof params.useinbandfec != 'undefined') { + appendOpusNext += '; useinbandfec=' + params.useinbandfec; + } + + if (typeof params.usedtx != 'undefined') { + appendOpusNext += '; usedtx=' + params.usedtx; + } + + if (typeof params.maxptime != 'undefined') { + appendOpusNext += '\r\na=maxptime:' + params.maxptime; + } + + sdpLines[opusFmtpLineIndex] = sdpLines[opusFmtpLineIndex].concat(appendOpusNext); + + sdp = sdpLines.join('\r\n'); + return sdp; + } + + // forceStereoAudio => via webrtcexample.com + // requires getUserMedia => echoCancellation:false + function forceStereoAudio(sdp) { + var sdpLines = sdp.split('\r\n'); + var fmtpLineIndex = null; + for (var i = 0; i < sdpLines.length; i++) { + if (sdpLines[i].search('opus/48000') !== -1) { + var opusPayload = extractSdp(sdpLines[i], /:(\d+) opus\/48000/i); + break; + } + } + for (var i = 0; i < sdpLines.length; i++) { + if (sdpLines[i].search('a=fmtp') !== -1) { + var payload = extractSdp(sdpLines[i], /a=fmtp:(\d+)/); + if (payload === opusPayload) { + fmtpLineIndex = i; + break; + } + } + } + if (fmtpLineIndex === null) return sdp; + sdpLines[fmtpLineIndex] = sdpLines[fmtpLineIndex].concat('; stereo=1; sprop-stereo=1'); + sdp = sdpLines.join('\r\n'); + return sdp; + } + + return { + removeVPX: removeVPX, + disableNACK: disableNACK, + prioritize: prioritize, + removeNonG722: removeNonG722, + setApplicationSpecificBandwidth: function(sdp, bandwidth, isScreen) { + return setBAS(sdp, bandwidth, isScreen); + }, + setVideoBitrates: function(sdp, params) { + return setVideoBitrates(sdp, params); + }, + setOpusAttributes: function(sdp, params) { + return setOpusAttributes(sdp, params); + }, + preferVP9: function(sdp) { + return preferCodec(sdp, 'vp9'); + }, + preferCodec: preferCodec, + forceStereoAudio: forceStereoAudio + }; +})(); + +// backward compatibility +window.BandwidthHandler = CodecsHandler; diff --git a/IceServersHandler.js b/IceServersHandler.js new file mode 100755 index 0000000..2900b33 --- /dev/null +++ b/IceServersHandler.js @@ -0,0 +1,41 @@ +// IceServersHandler.js + +var IceServersHandler = (function() { + function getIceServers(connection) { + // resiprocate: 3344+4433 + var iceServers = [{ + 'urls': [ + 'turn:webrtcweb.com:7788', // coTURN 7788+8877 + 'turn:webrtcweb.com:4455', // restund udp + + 'turn:webrtcweb.com:7788?transport=udp', // coTURN udp + 'turn:webrtcweb.com:7788?transport=tcp', // coTURN tcp + + 'turn:webrtcweb.com:4455?transport=udp', // restund udp + 'turn:webrtcweb.com:5544?transport=tcp', // restund tcp + + 'turn:webrtcweb.com:7575?transport=udp', // pions/turn + ], + 'username': 'muazkh', + 'credential': 'muazkh' + }, + { + 'urls': [ + 'stun:stun.l.google.com:19302', + 'stun:stun.l.google.com:19302?transport=udp' + ] + } + ]; + + if (typeof window.InstallTrigger !== 'undefined') { + iceServers[0].urls = iceServers[0].urls.pop(); + iceServers[1].urls = iceServers[1].urls.pop(); + } + + return iceServers; + } + + return { + getIceServers: getIceServers + }; +})(); diff --git a/MultiStreamsMixer.js b/MultiStreamsMixer.js new file mode 100755 index 0000000..ecb4a5a --- /dev/null +++ b/MultiStreamsMixer.js @@ -0,0 +1,468 @@ +// Last time updated: 2018-03-02 2:56:28 AM UTC + +// ________________________ +// MultiStreamsMixer v1.0.5 + +// Open-Sourced: https://github.com/muaz-khan/MultiStreamsMixer + +// -------------------------------------------------- +// Muaz Khan - www.MuazKhan.com +// MIT License - www.WebRTC-Experiment.com/licence +// -------------------------------------------------- + +function MultiStreamsMixer(arrayOfMediaStreams) { + + // requires: chrome://flags/#enable-experimental-web-platform-features + + var videos = []; + var isStopDrawingFrames = false; + + var canvas = document.createElement('canvas'); + var context = canvas.getContext('2d'); + canvas.style = 'opacity:0;position:absolute;z-index:-1;top: -100000000;left:-1000000000; margin-top:-1000000000;margin-left:-1000000000;'; + (document.body || document.documentElement).appendChild(canvas); + + this.disableLogs = false; + this.frameInterval = 10; + + this.width = 360; + this.height = 240; + + // use gain node to prevent echo + this.useGainNode = true; + + var self = this; + + // _____________________________ + // Cross-Browser-Declarations.js + + // WebAudio API representer + var AudioContext = window.AudioContext; + + if (typeof AudioContext === 'undefined') { + if (typeof webkitAudioContext !== 'undefined') { + /*global AudioContext:true */ + AudioContext = webkitAudioContext; + } + + if (typeof mozAudioContext !== 'undefined') { + /*global AudioContext:true */ + AudioContext = mozAudioContext; + } + } + + /*jshint -W079 */ + var URL = window.URL; + + if (typeof URL === 'undefined' && typeof webkitURL !== 'undefined') { + /*global URL:true */ + URL = webkitURL; + } + + if (typeof navigator !== 'undefined' && typeof navigator.getUserMedia === 'undefined') { // maybe window.navigator? + if (typeof navigator.webkitGetUserMedia !== 'undefined') { + navigator.getUserMedia = navigator.webkitGetUserMedia; + } + + if (typeof navigator.mozGetUserMedia !== 'undefined') { + navigator.getUserMedia = navigator.mozGetUserMedia; + } + } + + var MediaStream = window.MediaStream; + + if (typeof MediaStream === 'undefined' && typeof webkitMediaStream !== 'undefined') { + MediaStream = webkitMediaStream; + } + + /*global MediaStream:true */ + if (typeof MediaStream !== 'undefined') { + if (!('getVideoTracks' in MediaStream.prototype)) { + MediaStream.prototype.getVideoTracks = function() { + if (!this.getTracks) { + return []; + } + + var tracks = []; + this.getTracks.forEach(function(track) { + if (track.kind.toString().indexOf('video') !== -1) { + tracks.push(track); + } + }); + return tracks; + }; + + MediaStream.prototype.getAudioTracks = function() { + if (!this.getTracks) { + return []; + } + + var tracks = []; + this.getTracks.forEach(function(track) { + if (track.kind.toString().indexOf('audio') !== -1) { + tracks.push(track); + } + }); + return tracks; + }; + } + + // override "stop" method for all browsers + if (typeof MediaStream.prototype.stop === 'undefined') { + MediaStream.prototype.stop = function() { + this.getTracks().forEach(function(track) { + track.stop(); + }); + }; + } + } + + var Storage = {}; + + if (typeof AudioContext !== 'undefined') { + Storage.AudioContext = AudioContext; + } else if (typeof webkitAudioContext !== 'undefined') { + Storage.AudioContext = webkitAudioContext; + } + + function setSrcObject(stream, element, ignoreCreateObjectURL) { + if ('createObjectURL' in URL && !ignoreCreateObjectURL) { + try { + element.src = URL.createObjectURL(stream); + } catch (e) { + setSrcObject(stream, element, true); + return; + } + } else if ('srcObject' in element) { + element.srcObject = stream; + } else if ('mozSrcObject' in element) { + element.mozSrcObject = stream; + } else { + alert('createObjectURL/srcObject both are not supported.'); + } + } + + this.startDrawingFrames = function() { + drawVideosToCanvas(); + }; + + function drawVideosToCanvas() { + if (isStopDrawingFrames) { + return; + } + + var videosLength = videos.length; + + var fullcanvas = false; + var remaining = []; + videos.forEach(function(video) { + if (!video.stream) { + video.stream = {}; + } + + if (video.stream.fullcanvas) { + fullcanvas = video; + } else { + remaining.push(video); + } + }); + + if (fullcanvas) { + canvas.width = fullcanvas.stream.width; + canvas.height = fullcanvas.stream.height; + } else if (remaining.length) { + canvas.width = videosLength > 1 ? remaining[0].width * 2 : remaining[0].width; + + var height = 1; + if (videosLength === 3 || videosLength === 4) { + height = 2; + } + if (videosLength === 5 || videosLength === 6) { + height = 3; + } + if (videosLength === 7 || videosLength === 8) { + height = 4; + } + if (videosLength === 9 || videosLength === 10) { + height = 5; + } + canvas.height = remaining[0].height * height; + } else { + canvas.width = self.width || 360; + canvas.height = self.height || 240; + } + + if (fullcanvas && fullcanvas instanceof HTMLVideoElement) { + drawImage(fullcanvas); + } + + remaining.forEach(function(video, idx) { + drawImage(video, idx); + }); + + setTimeout(drawVideosToCanvas, self.frameInterval); + } + + function drawImage(video, idx) { + if (isStopDrawingFrames) { + return; + } + + var x = 0; + var y = 0; + var width = video.width; + var height = video.height; + + if (idx === 1) { + x = video.width; + } + + if (idx === 2) { + y = video.height; + } + + if (idx === 3) { + x = video.width; + y = video.height; + } + + if (idx === 4) { + y = video.height * 2; + } + + if (idx === 5) { + x = video.width; + y = video.height * 2; + } + + if (idx === 6) { + y = video.height * 3; + } + + if (idx === 7) { + x = video.width; + y = video.height * 3; + } + + if (typeof video.stream.left !== 'undefined') { + x = video.stream.left; + } + + if (typeof video.stream.top !== 'undefined') { + y = video.stream.top; + } + + if (typeof video.stream.width !== 'undefined') { + width = video.stream.width; + } + + if (typeof video.stream.height !== 'undefined') { + height = video.stream.height; + } + + context.drawImage(video, x, y, width, height); + + if (typeof video.stream.onRender === 'function') { + video.stream.onRender(context, x, y, width, height, idx); + } + } + + function getMixedStream() { + isStopDrawingFrames = false; + var mixedVideoStream = getMixedVideoStream(); + + var mixedAudioStream = getMixedAudioStream(); + if (mixedAudioStream) { + mixedAudioStream.getAudioTracks().forEach(function(track) { + mixedVideoStream.addTrack(track); + }); + } + + var fullcanvas; + arrayOfMediaStreams.forEach(function(stream) { + if (stream.fullcanvas) { + fullcanvas = true; + } + }); + + return mixedVideoStream; + } + + function getMixedVideoStream() { + resetVideoStreams(); + + var capturedStream; + + if ('captureStream' in canvas) { + capturedStream = canvas.captureStream(); + } else if ('mozCaptureStream' in canvas) { + capturedStream = canvas.mozCaptureStream(); + } else if (!self.disableLogs) { + console.error('Upgrade to latest Chrome or otherwise enable this flag: chrome://flags/#enable-experimental-web-platform-features'); + } + + var videoStream = new MediaStream(); + + capturedStream.getVideoTracks().forEach(function(track) { + videoStream.addTrack(track); + }); + + canvas.stream = videoStream; + + return videoStream; + } + + function getMixedAudioStream() { + // via: @pehrsons + if (!Storage.AudioContextConstructor) { + Storage.AudioContextConstructor = new Storage.AudioContext(); + } + + self.audioContext = Storage.AudioContextConstructor; + + self.audioSources = []; + + if (self.useGainNode === true) { + self.gainNode = self.audioContext.createGain(); + self.gainNode.connect(self.audioContext.destination); + self.gainNode.gain.value = 0; // don't hear self + } + + var audioTracksLength = 0; + arrayOfMediaStreams.forEach(function(stream) { + if (!stream.getAudioTracks().length) { + return; + } + + audioTracksLength++; + + var audioSource = self.audioContext.createMediaStreamSource(stream); + + if (self.useGainNode === true) { + audioSource.connect(self.gainNode); + } + + self.audioSources.push(audioSource); + }); + + if (!audioTracksLength) { + return; + } + + self.audioDestination = self.audioContext.createMediaStreamDestination(); + self.audioSources.forEach(function(audioSource) { + audioSource.connect(self.audioDestination); + }); + return self.audioDestination.stream; + } + + function getVideo(stream) { + var video = document.createElement('video'); + + setSrcObject(stream, video); + + video.muted = true; + video.volume = 0; + + video.width = stream.width || self.width || 360; + video.height = stream.height || self.height || 240; + + video.play(); + + return video; + } + + this.appendStreams = function(streams) { + if (!streams) { + throw 'First parameter is required.'; + } + + if (!(streams instanceof Array)) { + streams = [streams]; + } + + arrayOfMediaStreams.concat(streams); + + streams.forEach(function(stream) { + if (stream.getVideoTracks().length) { + var video = getVideo(stream); + video.stream = stream; + videos.push(video); + } + + if (stream.getAudioTracks().length && self.audioContext) { + var audioSource = self.audioContext.createMediaStreamSource(stream); + audioSource.connect(self.audioDestination); + self.audioSources.push(audioSource); + } + }); + }; + + this.releaseStreams = function() { + videos = []; + isStopDrawingFrames = true; + + if (self.gainNode) { + self.gainNode.disconnect(); + self.gainNode = null; + } + + if (self.audioSources.length) { + self.audioSources.forEach(function(source) { + source.disconnect(); + }); + self.audioSources = []; + } + + if (self.audioDestination) { + self.audioDestination.disconnect(); + self.audioDestination = null; + } + + if (self.audioContext) { + self.audioContext.close(); + } + + self.audioContext = null; + + context.clearRect(0, 0, canvas.width, canvas.height); + + if (canvas.stream) { + canvas.stream.stop(); + canvas.stream = null; + } + }; + + this.resetVideoStreams = function(streams) { + if (streams && !(streams instanceof Array)) { + streams = [streams]; + } + + resetVideoStreams(streams); + }; + + function resetVideoStreams(streams) { + videos = []; + streams = streams || arrayOfMediaStreams; + + // via: @adrian-ber + streams.forEach(function(stream) { + if (!stream.getVideoTracks().length) { + return; + } + + var video = getVideo(stream); + video.stream = stream; + videos.push(video); + }); + } + + // for debugging + this.name = 'MultiStreamsMixer'; + this.toString = function() { + return this.name; + }; + + this.getMixedStream = getMixedStream; + +} diff --git a/README.md b/README.md new file mode 100755 index 0000000..061f57a --- /dev/null +++ b/README.md @@ -0,0 +1,68 @@ +# Chrome extension for WebRTC Screen Sharing + +WebRTC Screen Sharing + +## How to install? + +Install Dessktop Sharing Extension + +* https://chrome.google.com/webstore/detail/webrtc-desktop-sharing/nkemblooioekjnpfekmjhpgkackcajhg + +## How to view screen? + +Try any of the below URL. Replace `your_room_id` with real room-id: + +``` +https://webrtcweb.com/screen?s=your_room_id +https://cdn.rawgit.com/muaz-khan/Chrome-Extensions/master/desktopCapture-p2p/index.html +``` + +## Developer Notes + +1. Chrome extension can share your screen, tab, any application's window, camera, microphone and speakers. +2. Clicking extension icon will generate a unique random room URL. You can share that URL with multiple users and all of them can view your screen. +3. [RTCMultiConnection](https://github.com/muaz-khan/RTCMultiConnection) is a WebRTC library that is used for peer-to-peer WebRTC streaming. +4. PubNub is used as a signaling method for handshake. However you can use [any WebRTC signaing option](https://github.com/muaz-khan/WebRTC-Experiment/blob/master/Signaling.md). +5. You can replace or include your own STUN+TURN servers in the [IceServersHandler.js](https://github.com/muaz-khan/Chrome-Extensions/blob/master/desktopCapture-p2p/IceServersHandler.js) file. +6. VP8 is currently default video codecs. However VP9 is recommended. You can always change codecs using options page. +7. [getStats](https://github.com/muaz-khan/getStats) is a WebRTC library that is used for bandwidth & codecs detection. This library is optional. You can always remove it. + +## Before publishing it for your own business + +> This step is optional. You can keep using `webrtcweb.com` URL as a screen viewer. + +Open [desktop-capturing.js](https://github.com/muaz-khan/Chrome-Extensions/blob/master/desktopCapture-p2p/desktop-capturing.js) and find following line: + +```javascript +var resultingURL = 'https://webrtcweb.com/screen?s=' + connection.sessionid; +``` + +Replace above line with your own server/website: + +```javascript +var resultingURL = 'https://yourWebSite.com/index.html?s=' + connection.sessionid; +``` + +You can find `index.html` here: + +* [desktopCapture-p2p/index.html](https://github.com/muaz-khan/Chrome-Extensions/blob/master/desktopCapture-p2p/index.html) + +## How to publish it for your own business? + +Make ZIP of the directory. Then navigate to [Chrome WebStore Developer Dashboard](https://chrome.google.com/webstore/developer/dashboard) and click **Add New Item** blue button. + +To learn more about how to publish a chrome extension in Google App Store: + +* https://developer.chrome.com/webstore/publish + +## For more information + +For additional information, click [this link](https://github.com/muaz-khan/WebRTC-Experiment/blob/7cd04a81b30cdca2db159eb746e2714307640767/Chrome-Extensions/desktopCapture/README.md). + +## It is Open-Sourced! + +* https://github.com/muaz-khan/Chrome-Extensions/tree/master/desktopCapture-p2p + +## License + +[Chrome-Extensions](https://github.com/muaz-khan/Chrome-Extensions) are released under [MIT licence](https://www.webrtc-experiment.com/licence/) . Copyright (c) [Muaz Khan](https://plus.google.com/+MuazKhan). diff --git a/RTCMultiConnection.js b/RTCMultiConnection.js new file mode 100755 index 0000000..8b4bea0 --- /dev/null +++ b/RTCMultiConnection.js @@ -0,0 +1,6557 @@ +// Last time updated at Sunday, July 30th, 2017, 9:37:50 AM + +// Quick-Demo for newbies: http://jsfiddle.net/c46de0L8/ +// Another simple demo: http://jsfiddle.net/zar6fg60/ + +// Latest file can be found here: https://cdn.webrtc-experiment.com/RTCMultiConnection.js + +// Muaz Khan - www.MuazKhan.com +// MIT License - www.WebRTC-Experiment.com/licence +// Documentation - www.RTCMultiConnection.org/docs +// FAQ - www.RTCMultiConnection.org/FAQ +// Changes log - www.RTCMultiConnection.org/changes-log/ +// Demos - www.WebRTC-Experiment.com/RTCMultiConnection + +// _________________________ +// RTCMultiConnection-v2.2.2 + +(function() { + + // RMC == RTCMultiConnection + // usually page-URL is used as channel-id + // you can always override it! + // www.RTCMultiConnection.org/docs/channel-id/ + window.RMCDefaultChannel = location.href.replace(/\/|:|#|\?|\$|\^|%|\.|`|~|!|\+|@|\[|\||]|\|*. /g, '').split('\n').join('').split('\r').join(''); + + // www.RTCMultiConnection.org/docs/constructor/ + window.RTCMultiConnection = function(channel) { + // an instance of constructor + var connection = this; + + // a reference to RTCMultiSession + var rtcMultiSession; + + // setting default channel or channel passed through constructor + connection.channel = channel || RMCDefaultChannel; + + // to allow single user to join multiple rooms; + // you can change this property at runtime! + connection.isAcceptNewSession = true; + + // www.RTCMultiConnection.org/docs/open/ + connection.open = function(args) { + connection.isAcceptNewSession = false; + + // www.RTCMultiConnection.org/docs/session-initiator/ + // you can always use this property to determine room owner! + connection.isInitiator = true; + + var dontTransmit = false; + + // a channel can contain multiple rooms i.e. sessions + if (args) { + if (isString(args)) { + connection.sessionid = args; + } else { + if (!isNull(args.transmitRoomOnce)) { + connection.transmitRoomOnce = args.transmitRoomOnce; + } + + if (!isNull(args.dontTransmit)) { + dontTransmit = args.dontTransmit; + } + + if (!isNull(args.sessionid)) { + connection.sessionid = args.sessionid; + } + } + } + + // if firebase && if session initiator + if (connection.socket && connection.socket.remove) { + connection.socket.remove(); + } + + if (!connection.sessionid) connection.sessionid = connection.channel; + connection.sessionDescription = { + sessionid: connection.sessionid, + userid: connection.userid, + session: connection.session, + extra: connection.extra + }; + + if (!connection.sessionDescriptions[connection.sessionDescription.sessionid]) { + connection.numberOfSessions++; + connection.sessionDescriptions[connection.sessionDescription.sessionid] = connection.sessionDescription; + } + + // connect with signaling channel + initRTCMultiSession(function() { + // "captureUserMediaOnDemand" is disabled by default. + // invoke "getUserMedia" only when first participant found. + rtcMultiSession.captureUserMediaOnDemand = args ? !!args.captureUserMediaOnDemand : false; + + if (args && args.onMediaCaptured) { + connection.onMediaCaptured = args.onMediaCaptured; + } + + // for session-initiator, user-media is captured as soon as "open" is invoked. + if (!rtcMultiSession.captureUserMediaOnDemand) captureUserMedia(function() { + rtcMultiSession.initSession({ + sessionDescription: connection.sessionDescription, + dontTransmit: dontTransmit + }); + + invokeMediaCaptured(connection); + }); + + if (rtcMultiSession.captureUserMediaOnDemand) { + rtcMultiSession.initSession({ + sessionDescription: connection.sessionDescription, + dontTransmit: dontTransmit + }); + } + }); + return connection.sessionDescription; + }; + + // www.RTCMultiConnection.org/docs/connect/ + connection.connect = function(sessionid) { + // a channel can contain multiple rooms i.e. sessions + if (sessionid) { + connection.sessionid = sessionid; + } + + // connect with signaling channel + initRTCMultiSession(function() { + log('Signaling channel is ready.'); + }); + + return this; + }; + + // www.RTCMultiConnection.org/docs/join/ + connection.join = joinSession; + + // www.RTCMultiConnection.org/docs/send/ + connection.send = function(data, _channel) { + if (connection.numberOfConnectedUsers <= 0) { + // no connections + setTimeout(function() { + // try again + connection.send(data, _channel); + }, 1000); + return; + } + + // send file/data or /text + if (!data) + throw 'No file, data or text message to share.'; + + // connection.send([file1, file2, file3]) + // you can share multiple files, strings or data objects using "send" method! + if (data instanceof Array && !isNull(data[0].size) && !isNull(data[0].type)) { + // this mechanism can cause failure for subsequent packets/data + // on Firefox especially; and on chrome as well! + // todo: need to use setTimeout instead. + for (var i = 0; i < data.length; i++) { + data[i].size && data[i].type && connection.send(data[i], _channel); + } + return; + } + + // File or Blob object MUST have "type" and "size" properties + if (!isNull(data.size) && !isNull(data.type)) { + if (!connection.enableFileSharing) { + throw '"enableFileSharing" boolean MUST be "true" to support file sharing.'; + } + + if (!rtcMultiSession.fileBufferReader) { + initFileBufferReader(connection, function(fbr) { + rtcMultiSession.fileBufferReader = fbr; + connection.send(data, _channel); + }); + return; + } + + var extra = merge({ + userid: connection.userid + }, data.extra || connection.extra); + + rtcMultiSession.fileBufferReader.readAsArrayBuffer(data, function(uuid) { + rtcMultiSession.fileBufferReader.getNextChunk(uuid, function(nextChunk, isLastChunk, extra) { + if (_channel) _channel.send(nextChunk); + else rtcMultiSession.send(nextChunk); + }); + }, extra); + } else { + // to allow longest string messages + // and largest data objects + // or anything of any size! + // to send multiple data objects concurrently! + + TextSender.send({ + text: data, + channel: rtcMultiSession, + _channel: _channel, + connection: connection + }); + } + }; + + function initRTCMultiSession(onSignalingReady) { + if (screenFrame) { + loadScreenFrame(); + } + + // RTCMultiSession is the backbone object; + // this object MUST be initialized once! + if (rtcMultiSession) return onSignalingReady(); + + // your everything is passed over RTCMultiSession constructor! + rtcMultiSession = new RTCMultiSession(connection, onSignalingReady); + } + + connection.disconnect = function() { + if (rtcMultiSession) rtcMultiSession.disconnect(); + rtcMultiSession = null; + }; + + function joinSession(session, joinAs) { + if (isString(session)) { + connection.skipOnNewSession = true; + } + + if (!rtcMultiSession) { + log('Signaling channel is not ready. Connecting...'); + // connect with signaling channel + initRTCMultiSession(function() { + log('Signaling channel is connected. Joining the session again...'); + setTimeout(function() { + joinSession(session, joinAs); + }, 1000); + }); + return; + } + + // connection.join('sessionid'); + if (isString(session)) { + if (connection.sessionDescriptions[session]) { + session = connection.sessionDescriptions[session]; + } else + return setTimeout(function() { + log('Session-Descriptions not found. Rechecking..'); + joinSession(session, joinAs); + }, 1000); + } + + // connection.join('sessionid', { audio: true }); + if (joinAs) { + return captureUserMedia(function() { + session.oneway = true; + joinSession(session); + }, joinAs); + } + + if (!session || !session.userid || !session.sessionid) { + error('missing arguments', arguments); + + var error = 'Invalid data passed over "connection.join" method.'; + connection.onstatechange({ + userid: 'browser', + extra: {}, + name: 'Unexpected data detected.', + reason: error + }); + + throw error; + } + + if (!connection.dontOverrideSession) { + connection.session = session.session; + } + + var extra = connection.extra || session.extra || {}; + + // todo: need to verify that if-block statement works as expected. + // expectations: if it is oneway streaming; or if it is data-only connection + // then, it shouldn't capture user-media on participant's side. + if (session.oneway || isData(session)) { + rtcMultiSession.joinSession(session, extra); + } else { + captureUserMedia(function() { + rtcMultiSession.joinSession(session, extra); + }); + } + } + + var isFirstSession = true; + + // www.RTCMultiConnection.org/docs/captureUserMedia/ + + function captureUserMedia(callback, _session, dontCheckChromExtension) { + // capture user's media resources + var session = _session || connection.session; + + if (isEmpty(session)) { + if (callback) callback(); + return; + } + + // you can force to skip media capturing! + if (connection.dontCaptureUserMedia) { + return callback(); + } + + // if it is data-only connection + // if it is one-way connection and current user is participant + if (isData(session) || (!connection.isInitiator && session.oneway)) { + // www.RTCMultiConnection.org/docs/attachStreams/ + connection.attachStreams = []; + return callback(); + } + + var constraints = { + audio: !!session.audio ? { + mandatory: {}, + optional: [{ + chromeRenderToAssociatedSink: true + }] + } : false, + video: !!session.video + }; + + // if custom audio device is selected + if (connection._mediaSources.audio) { + constraints.audio.optional.push({ + sourceId: connection._mediaSources.audio + }); + } + if (connection._mediaSources.audiooutput) { + constraints.audio.optional.push({ + sourceId: connection._mediaSources.audiooutput + }); + } + if (connection._mediaSources.audioinput) { + constraints.audio.optional.push({ + sourceId: connection._mediaSources.audioinput + }); + } + + // if custom video device is selected + if (connection._mediaSources.video) { + constraints.video = { + optional: [{ + sourceId: connection._mediaSources.video + }] + }; + } + if (connection._mediaSources.videoinput) { + constraints.video = { + optional: [{ + sourceId: connection._mediaSources.videoinput + }] + }; + } + + // for connection.session = {}; + if (!session.screen && !constraints.audio && !constraints.video) { + return callback(); + } + + var screen_constraints = { + audio: false, + video: { + mandatory: { + chromeMediaSource: DetectRTC.screen.chromeMediaSource, + maxWidth: screen.width > 1920 ? screen.width : 1920, + maxHeight: screen.height > 1080 ? screen.height : 1080 + }, + optional: [] + } + }; + + if (isFirefox && session.screen) { + if (location.protocol !== 'https:') { + return error(SCREEN_COMMON_FAILURE); + } + warn(Firefox_Screen_Capturing_Warning); + + screen_constraints.video = { + mozMediaSource: 'window', // mozMediaSource is redundant here + mediaSource: 'window' // 'screen' || 'window' + }; + + // Firefox is supporting audio+screen from single getUserMedia request + // audio+video+screen will become audio+screen for Firefox + // because Firefox isn't supporting multi-streams feature version < 38 + // version >38 supports multi-stream sharing. + // we can use: firefoxVersion < 38 + // however capturing audio and screen using single getUserMedia is a better option + if (constraints.audio /* && !session.video */ ) { + screen_constraints.audio = true; + constraints = {}; + } + + delete screen_constraints.video.chromeMediaSource; + } + + // if screen is prompted + if (session.screen) { + if (isChrome && DetectRTC.screen.extensionid != ReservedExtensionID) { + useCustomChromeExtensionForScreenCapturing = true; + } + + if (isChrome && !useCustomChromeExtensionForScreenCapturing && !dontCheckChromExtension && !DetectRTC.screen.sourceId) { + listenEventHandler('message', onIFrameCallback); + + function onIFrameCallback(event) { + if (event.data && event.data.chromeMediaSourceId) { + // this event listener is no more needed + window.removeEventListener('message', onIFrameCallback); + + var sourceId = event.data.chromeMediaSourceId; + + DetectRTC.screen.sourceId = sourceId; + DetectRTC.screen.chromeMediaSource = 'desktop'; + + if (sourceId == 'PermissionDeniedError') { + var mediaStreamError = { + message: location.protocol == 'https:' ? 'User denied to share content of his screen.' : SCREEN_COMMON_FAILURE, + name: 'PermissionDeniedError', + constraintName: screen_constraints, + session: session + }; + currentUserMediaRequest.mutex = false; + DetectRTC.screen.sourceId = null; + return connection.onMediaError(mediaStreamError); + } + + captureUserMedia(callback, _session); + } + + if (event.data && event.data.chromeExtensionStatus) { + warn('Screen capturing extension status is:', event.data.chromeExtensionStatus); + DetectRTC.screen.chromeMediaSource = 'screen'; + captureUserMedia(callback, _session, true); + } + } + + if (!screenFrame) { + loadScreenFrame(); + } + + screenFrame.postMessage(); + return; + } + + // check if screen capturing extension is installed. + if (isChrome && useCustomChromeExtensionForScreenCapturing && !dontCheckChromExtension && DetectRTC.screen.chromeMediaSource == 'screen' && DetectRTC.screen.extensionid) { + if (DetectRTC.screen.extensionid == ReservedExtensionID && document.domain.indexOf('webrtc-experiment.com') == -1) { + return captureUserMedia(callback, _session, true); + } + + log('checking if chrome extension is installed.'); + DetectRTC.screen.getChromeExtensionStatus(function(status) { + if (status == 'installed-enabled') { + DetectRTC.screen.chromeMediaSource = 'desktop'; + } + + captureUserMedia(callback, _session, true); + log('chrome extension is installed?', DetectRTC.screen.chromeMediaSource == 'desktop'); + }); + return; + } + + if (isChrome && useCustomChromeExtensionForScreenCapturing && DetectRTC.screen.chromeMediaSource == 'desktop' && !DetectRTC.screen.sourceId) { + DetectRTC.screen.getSourceId(function(sourceId) { + if (sourceId == 'PermissionDeniedError') { + var mediaStreamError = { + message: 'User denied to share content of his screen.', + name: 'PermissionDeniedError', + constraintName: screen_constraints, + session: session + }; + currentUserMediaRequest.mutex = false; + DetectRTC.screen.chromeMediaSource = 'desktop'; + return connection.onMediaError(mediaStreamError); + } + + if (sourceId == 'No-Response') { + error('Chrome extension seems not available. Make sure that manifest.json#L16 has valid content-script matches pointing to your URL.'); + DetectRTC.screen.chromeMediaSource = 'screen'; + return captureUserMedia(callback, _session, true); + } + + captureUserMedia(callback, _session, true); + }); + return; + } + + if (isChrome && DetectRTC.screen.chromeMediaSource == 'desktop') { + screen_constraints.video.mandatory.chromeMediaSourceId = DetectRTC.screen.sourceId; + } + + var _isFirstSession = isFirstSession; + + _captureUserMedia(screen_constraints, constraints.audio || constraints.video ? function() { + + if (_isFirstSession) isFirstSession = true; + + _captureUserMedia(constraints, callback); + } : callback); + } else _captureUserMedia(constraints, callback, session.audio && !session.video); + + function _captureUserMedia(forcedConstraints, forcedCallback, isRemoveVideoTracks, dontPreventSSLAutoAllowed) { + connection.onstatechange({ + userid: 'browser', + extra: {}, + name: 'fetching-usermedia', + reason: 'About to capture user-media with constraints: ' + toStr(forcedConstraints) + }); + + + if (connection.preventSSLAutoAllowed && !dontPreventSSLAutoAllowed && isChrome) { + // if navigator.customGetUserMediaBar.js is missing + if (!navigator.customGetUserMediaBar) { + loadScript(connection.resources.customGetUserMediaBar, function() { + _captureUserMedia(forcedConstraints, forcedCallback, isRemoveVideoTracks, dontPreventSSLAutoAllowed); + }); + return; + } + + navigator.customGetUserMediaBar(forcedConstraints, function() { + _captureUserMedia(forcedConstraints, forcedCallback, isRemoveVideoTracks, true); + }, function() { + connection.onMediaError({ + name: 'PermissionDeniedError', + message: 'User denied permission.', + constraintName: forcedConstraints, + session: session + }); + }); + return; + } + + var mediaConfig = { + onsuccess: function(stream, returnBack, idInstance, streamid) { + onStreamSuccessCallback(stream, returnBack, idInstance, streamid, forcedConstraints, forcedCallback, isRemoveVideoTracks, screen_constraints, constraints, session); + }, + onerror: function(e, constraintUsed) { + // http://goo.gl/hrwF1a + if (isFirefox) { + if (e == 'PERMISSION_DENIED') { + e = { + message: '', + name: 'PermissionDeniedError', + constraintName: constraintUsed, + session: session + }; + } + } + + if (isFirefox && constraintUsed.video && constraintUsed.video.mozMediaSource) { + mediaStreamError = { + message: Firefox_Screen_Capturing_Warning, + name: e.name || 'PermissionDeniedError', + constraintName: constraintUsed, + session: session + }; + + connection.onMediaError(mediaStreamError); + return; + } + + if (isString(e)) { + return connection.onMediaError({ + message: 'Unknown Error', + name: e, + constraintName: constraintUsed, + session: session + }); + } + + // it seems that chrome 35+ throws "DevicesNotFoundError" exception + // when any of the requested media is either denied or absent + if (e.name && (e.name == 'PermissionDeniedError' || e.name == 'DevicesNotFoundError')) { + var mediaStreamError = 'Either: '; + mediaStreamError += '\n Media resolutions are not permitted.'; + mediaStreamError += '\n Another application is using same media device.'; + mediaStreamError += '\n Media device is not attached or drivers not installed.'; + mediaStreamError += '\n You denied access once and it is still denied.'; + + if (e.message && e.message.length) { + mediaStreamError += '\n ' + e.message; + } + + mediaStreamError = { + message: mediaStreamError, + name: e.name, + constraintName: constraintUsed, + session: session + }; + + connection.onMediaError(mediaStreamError); + + if (isChrome && (session.audio || session.video)) { + // todo: this snippet fails if user has two or more + // microphone/webcam attached. + DetectRTC.load(function() { + // it is possible to check presence of the microphone before using it! + if (session.audio && !DetectRTC.hasMicrophone) { + warn('It seems that you have no microphone attached to your device/system.'); + session.audio = session.audio = false; + + if (!session.video) { + alert('It seems that you are capturing microphone and there is no device available or access is denied. Reloading...'); + location.reload(); + } + } + + // it is possible to check presence of the webcam before using it! + if (session.video && !DetectRTC.hasWebcam) { + warn('It seems that you have no webcam attached to your device/system.'); + session.video = session.video = false; + + if (!session.audio) { + alert('It seems that you are capturing webcam and there is no device available or access is denied. Reloading...'); + location.reload(); + } + } + + if (!DetectRTC.hasMicrophone && !DetectRTC.hasWebcam) { + alert('It seems that either both microphone/webcam are not available or access is denied. Reloading...'); + location.reload(); + } else if (!connection.getUserMediaPromptedOnce) { + // make maximum two tries! + connection.getUserMediaPromptedOnce = true; + captureUserMedia(callback, session); + } + }); + } + } + + if (e.name && e.name == 'ConstraintNotSatisfiedError') { + var mediaStreamError = 'Either: '; + mediaStreamError += '\n You are prompting unknown media resolutions.'; + mediaStreamError += '\n You are using invalid media constraints.'; + + if (e.message && e.message.length) { + mediaStreamError += '\n ' + e.message; + } + + mediaStreamError = { + message: mediaStreamError, + name: e.name, + constraintName: constraintUsed, + session: session + }; + + connection.onMediaError(mediaStreamError); + } + + if (session.screen) { + if (isFirefox) { + error(Firefox_Screen_Capturing_Warning); + } else if (location.protocol !== 'https:') { + if (!isNodeWebkit && (location.protocol == 'file:' || location.protocol == 'http:')) { + error('You cannot use HTTP or file protocol for screen capturing. You must either use HTTPs or chrome extension page or Node-Webkit page.'); + } + } else { + error('Unable to detect actual issue. Maybe "deprecated" screen capturing flag was not set using command line or maybe you clicked "No" button or maybe chrome extension returned invalid "sourceId". Please install chrome-extension: http://bit.ly/webrtc-screen-extension'); + } + } + + currentUserMediaRequest.mutex = false; + + // to make sure same stream can be captured again! + var idInstance = JSON.stringify(constraintUsed); + if (currentUserMediaRequest.streams[idInstance]) { + delete currentUserMediaRequest.streams[idInstance]; + } + }, + mediaConstraints: connection.mediaConstraints || {} + }; + + mediaConfig.constraints = forcedConstraints || constraints; + mediaConfig.connection = connection; + getUserMedia(mediaConfig); + } + } + + function onStreamSuccessCallback(stream, returnBack, idInstance, streamid, forcedConstraints, forcedCallback, isRemoveVideoTracks, screen_constraints, constraints, session) { + if (!streamid) streamid = getRandomString(); + + connection.onstatechange({ + userid: 'browser', + extra: {}, + name: 'usermedia-fetched', + reason: 'Captured user media using constraints: ' + toStr(forcedConstraints) + }); + + if (isRemoveVideoTracks) { + stream = convertToAudioStream(stream); + } + + connection.localStreamids.push(streamid); + stream.onended = function() { + if (streamedObject.mediaElement && !streamedObject.mediaElement.parentNode && document.getElementById(stream.streamid)) { + streamedObject.mediaElement = document.getElementById(stream.streamid); + } + + // when a stream is stopped; it must be removed from "attachStreams" array + connection.attachStreams.forEach(function(_stream, index) { + if (_stream == stream) { + delete connection.attachStreams[index]; + connection.attachStreams = swap(connection.attachStreams); + } + }); + + onStreamEndedHandler(streamedObject, connection); + + if (connection.streams[streamid]) { + connection.removeStream(streamid); + } + + // if user clicks "stop" button to close screen sharing + var _stream = connection.streams[streamid]; + if (_stream && _stream.sockets.length) { + _stream.sockets.forEach(function(socket) { + socket.send({ + streamid: _stream.streamid, + stopped: true + }); + }); + } + + currentUserMediaRequest.mutex = false; + // to make sure same stream can be captured again! + if (currentUserMediaRequest.streams[idInstance]) { + delete currentUserMediaRequest.streams[idInstance]; + } + + // to allow re-capturing of the screen + DetectRTC.screen.sourceId = null; + }; + + if (!isIE) { + stream.streamid = streamid; + stream.isScreen = forcedConstraints == screen_constraints; + stream.isVideo = forcedConstraints == constraints && !!constraints.video; + stream.isAudio = forcedConstraints == constraints && !!constraints.audio && !constraints.video; + + // if muted stream is negotiated + stream.preMuted = { + audio: stream.getAudioTracks().length && !stream.getAudioTracks()[0].enabled, + video: stream.getVideoTracks().length && !stream.getVideoTracks()[0].enabled + }; + } + + var mediaElement = createMediaElement(stream, session); + mediaElement.muted = true; + + var streamedObject = { + stream: stream, + streamid: streamid, + mediaElement: mediaElement, + blobURL: mediaElement.mozSrcObject ? URL.createObjectURL(stream) : mediaElement.src, + type: 'local', + userid: connection.userid, + extra: connection.extra, + session: session, + isVideo: !!stream.isVideo, + isAudio: !!stream.isAudio, + isScreen: !!stream.isScreen, + isInitiator: !!connection.isInitiator, + rtcMultiConnection: connection + }; + + if (isFirstSession) { + connection.attachStreams.push(stream); + } + isFirstSession = false; + + connection.streams[streamid] = connection._getStream(streamedObject); + + if (!returnBack) { + connection.onstream(streamedObject); + } + + if (connection.setDefaultEventsForMediaElement) { + connection.setDefaultEventsForMediaElement(mediaElement, streamid); + } + + if (forcedCallback) forcedCallback(stream, streamedObject); + + if (connection.onspeaking) { + initHark({ + stream: stream, + streamedObject: streamedObject, + connection: connection + }); + } + } + + // www.RTCMultiConnection.org/docs/captureUserMedia/ + connection.captureUserMedia = captureUserMedia; + + // www.RTCMultiConnection.org/docs/leave/ + connection.leave = function(userid) { + if (!rtcMultiSession) return; + + isFirstSession = true; + + if (userid) { + connection.eject(userid); + return; + } + + rtcMultiSession.leave(); + }; + + // www.RTCMultiConnection.org/docs/eject/ + connection.eject = function(userid) { + if (!connection.isInitiator) throw 'Only session-initiator can eject a user.'; + if (!connection.peers[userid]) throw 'You ejected invalid user.'; + connection.peers[userid].sendCustomMessage({ + ejected: true + }); + }; + + // www.RTCMultiConnection.org/docs/close/ + connection.close = function() { + // close entire session + connection.autoCloseEntireSession = true; + connection.leave(); + }; + + // www.RTCMultiConnection.org/docs/renegotiate/ + connection.renegotiate = function(stream, session) { + if (connection.numberOfConnectedUsers <= 0) { + // no connections + setTimeout(function() { + // try again + connection.renegotiate(stream, session); + }, 1000); + return; + } + + rtcMultiSession.addStream({ + renegotiate: session || merge({ + oneway: true + }, connection.session), + stream: stream + }); + }; + + connection.attachExternalStream = function(stream, isScreen) { + var constraints = {}; + if (stream.getAudioTracks && stream.getAudioTracks().length) { + constraints.audio = true; + } + if (stream.getVideoTracks && stream.getVideoTracks().length) { + constraints.video = true; + } + + var screen_constraints = { + video: { + chromeMediaSource: 'fake' + } + }; + var forcedConstraints = isScreen ? screen_constraints : constraints; + onStreamSuccessCallback(stream, false, '', null, forcedConstraints, false, false, screen_constraints, constraints, constraints); + }; + + // www.RTCMultiConnection.org/docs/addStream/ + connection.addStream = function(session, socket) { + // www.RTCMultiConnection.org/docs/renegotiation/ + + if (connection.numberOfConnectedUsers <= 0) { + // no connections + setTimeout(function() { + // try again + connection.addStream(session, socket); + }, 1000); + return; + } + + // renegotiate new media stream + if (session) { + var isOneWayStreamFromParticipant; + if (!connection.isInitiator && session.oneway) { + session.oneway = false; + isOneWayStreamFromParticipant = true; + } + + captureUserMedia(function(stream) { + if (isOneWayStreamFromParticipant) { + session.oneway = true; + } + addStream(stream); + }, session); + } else addStream(); + + function addStream(stream) { + rtcMultiSession.addStream({ + stream: stream, + renegotiate: session || connection.session, + socket: socket + }); + } + }; + + // www.RTCMultiConnection.org/docs/removeStream/ + connection.removeStream = function(streamid, dontRenegotiate) { + if (connection.numberOfConnectedUsers <= 0) { + // no connections + setTimeout(function() { + // try again + connection.removeStream(streamid, dontRenegotiate); + }, 1000); + return; + } + + if (!streamid) streamid = 'all'; + if (!isString(streamid) || streamid.search(/all|audio|video|screen/gi) != -1) { + function _detachStream(_stream, config) { + if (config.local && _stream.type != 'local') return; + if (config.remote && _stream.type != 'remote') return; + + // connection.removeStream({screen:true}); + if (config.screen && !!_stream.isScreen) { + connection.detachStreams.push(_stream.streamid); + } + + // connection.removeStream({audio:true}); + if (config.audio && !!_stream.isAudio) { + connection.detachStreams.push(_stream.streamid); + } + + // connection.removeStream({video:true}); + if (config.video && !!_stream.isVideo) { + connection.detachStreams.push(_stream.streamid); + } + + // connection.removeStream({}); + if (!config.audio && !config.video && !config.screen) { + connection.detachStreams.push(_stream.streamid); + } + + if (connection.detachStreams.indexOf(_stream.streamid) != -1) { + log('removing stream', _stream.streamid); + onStreamEndedHandler(_stream, connection); + + if (config.stop) { + connection.stopMediaStream(_stream.stream); + } + } + } + + for (var stream in connection.streams) { + if (connection._skip.indexOf(stream) == -1) { + _stream = connection.streams[stream]; + + if (streamid == 'all') _detachStream(_stream, { + audio: true, + video: true, + screen: true + }); + + else if (isString(streamid)) { + // connection.removeStream('screen'); + var config = {}; + config[streamid] = true; + _detachStream(_stream, config); + } else _detachStream(_stream, streamid); + } + } + + if (!dontRenegotiate && connection.detachStreams.length) { + connection.renegotiate(); + } + + return; + } + + var stream = connection.streams[streamid]; + + // detach pre-attached streams + if (!stream) return warn('No such stream exists. Stream-id:', streamid); + + // www.RTCMultiConnection.org/docs/detachStreams/ + connection.detachStreams.push(stream.streamid); + + log('removing stream', stream.streamid); + onStreamEndedHandler(stream, connection); + + // todo: how to allow "stop" function? + // connection.stopMediaStream(stream.stream) + + if (!dontRenegotiate) { + connection.renegotiate(); + } + }; + + connection.switchStream = function(session) { + if (connection.numberOfConnectedUsers <= 0) { + // no connections + setTimeout(function() { + // try again + connection.switchStream(session); + }, 1000); + return; + } + + connection.removeStream('all', true); + connection.addStream(session); + }; + + // www.RTCMultiConnection.org/docs/sendCustomMessage/ + connection.sendCustomMessage = function(message) { + if (!rtcMultiSession || !rtcMultiSession.defaultSocket) { + return setTimeout(function() { + connection.sendCustomMessage(message); + }, 1000); + } + + rtcMultiSession.defaultSocket.send({ + customMessage: true, + message: message + }); + }; + + // set RTCMultiConnection defaults on constructor invocation + setDefaults(connection); + }; + + function RTCMultiSession(connection, callbackForSignalingReady) { + var socketObjects = {}; + var sockets = []; + var rtcMultiSession = this; + var participants = {}; + + if (!rtcMultiSession.fileBufferReader && connection.session.data && connection.enableFileSharing) { + initFileBufferReader(connection, function(fbr) { + rtcMultiSession.fileBufferReader = fbr; + }); + } + + var textReceiver = new TextReceiver(connection); + + function onDataChannelMessage(e) { + if (e.data.checkingPresence && connection.channels[e.userid]) { + connection.channels[e.userid].send({ + presenceDetected: true + }); + return; + } + + if (e.data.presenceDetected && connection.peers[e.userid]) { + connection.peers[e.userid].connected = true; + return; + } + + if (e.data.type === 'text') { + textReceiver.receive(e.data, e.userid, e.extra); + } else { + if (connection.autoTranslateText) { + e.original = e.data; + connection.Translator.TranslateText(e.data, function(translatedText) { + e.data = translatedText; + connection.onmessage(e); + }); + } else connection.onmessage(e); + } + } + + function onNewSession(session) { + if (connection.skipOnNewSession) return; + + if (!session.session) session.session = {}; + if (!session.extra) session.extra = {}; + + // todo: make sure this works as expected. + // i.e. "onNewSession" should be fired only for + // sessionid that is passed over "connect" method. + if (connection.sessionid && session.sessionid != connection.sessionid) return; + + if (connection.onNewSession) { + session.join = function(forceSession) { + if (!forceSession) return connection.join(session); + + for (var f in forceSession) { + session.session[f] = forceSession[f]; + } + + // keeping previous state + var isDontCaptureUserMedia = connection.dontCaptureUserMedia; + + connection.dontCaptureUserMedia = false; + connection.captureUserMedia(function() { + connection.dontCaptureUserMedia = true; + connection.join(session); + + // returning back previous state + connection.dontCaptureUserMedia = isDontCaptureUserMedia; + }, forceSession); + }; + if (!session.extra) session.extra = {}; + + return connection.onNewSession(session); + } + + connection.join(session); + } + + function updateSocketForLocalStreams(socket) { + for (var i = 0; i < connection.localStreamids.length; i++) { + var streamid = connection.localStreamids[i]; + if (connection.streams[streamid]) { + // using "sockets" array to keep references of all sockets using + // this media stream; so we can fire "onStreamEndedHandler" among all users. + connection.streams[streamid].sockets.push(socket); + } + } + } + + function newPrivateSocket(_config) { + var socketConfig = { + channel: _config.channel, + onmessage: socketResponse, + onopen: function(_socket) { + if (_socket) socket = _socket; + + if (isofferer && !peer) { + peerConfig.session = connection.session; + if (!peer) peer = new PeerConnection(); + peer.create('offer', peerConfig); + } + + _config.socketIndex = socket.index = sockets.length; + socketObjects[socketConfig.channel] = socket; + sockets[_config.socketIndex] = socket; + + updateSocketForLocalStreams(socket); + + if (!socket.__push) { + socket.__push = socket.send; + socket.send = function(message) { + message.userid = message.userid || connection.userid; + message.extra = message.extra || connection.extra || {}; + + socket.__push(message); + }; + } + } + }; + + socketConfig.callback = function(_socket) { + socket = _socket; + socketConfig.onopen(); + }; + + var socket = connection.openSignalingChannel(socketConfig); + if (socket) socketConfig.onopen(socket); + + var isofferer = _config.isofferer, + peer; + + var peerConfig = { + onopen: onChannelOpened, + onicecandidate: function(candidate) { + if (!connection.candidates) throw 'ICE candidates are mandatory.'; + if (!connection.iceProtocols) throw 'At least one must be true; UDP or TCP.'; + + var iceCandidates = connection.candidates; + + var stun = iceCandidates.stun; + var turn = iceCandidates.turn; + + if (!isNull(iceCandidates.reflexive)) stun = iceCandidates.reflexive; + if (!isNull(iceCandidates.relay)) turn = iceCandidates.relay; + + if (!iceCandidates.host && !!candidate.candidate.match(/a=candidate.*typ host/g)) return; + if (!turn && !!candidate.candidate.match(/a=candidate.*typ relay/g)) return; + if (!stun && !!candidate.candidate.match(/a=candidate.*typ srflx/g)) return; + + var protocol = connection.iceProtocols; + + if (!protocol.udp && !!candidate.candidate.match(/a=candidate.* udp/g)) return; + if (!protocol.tcp && !!candidate.candidate.match(/a=candidate.* tcp/g)) return; + + if (!window.selfNPObject) window.selfNPObject = candidate; + + socket && socket.send({ + candidate: JSON.stringify({ + candidate: candidate.candidate, + sdpMid: candidate.sdpMid, + sdpMLineIndex: candidate.sdpMLineIndex + }) + }); + }, + onmessage: function(data) { + if (!data) return; + + var abToStr = ab2str(data); + if (abToStr.indexOf('"userid":') != -1) { + abToStr = JSON.parse(abToStr); + onDataChannelMessage(abToStr); + } else if (data instanceof ArrayBuffer || data instanceof DataView) { + if (!connection.enableFileSharing) { + throw 'It seems that receiving data is either "Blob" or "File" but file sharing is disabled.'; + } + + if (!rtcMultiSession.fileBufferReader) { + var that = this; + initFileBufferReader(connection, function(fbr) { + rtcMultiSession.fileBufferReader = fbr; + that.onmessage(data); + }); + return; + } + + var fileBufferReader = rtcMultiSession.fileBufferReader; + + fileBufferReader.convertToObject(data, function(chunk) { + if (chunk.maxChunks || chunk.readyForNextChunk) { + // if target peer requested next chunk + if (chunk.readyForNextChunk) { + fileBufferReader.getNextChunk(chunk.uuid, function(nextChunk, isLastChunk, extra) { + rtcMultiSession.send(nextChunk); + }); + return; + } + + // if chunk is received + fileBufferReader.addChunk(chunk, function(promptNextChunk) { + // request next chunk + rtcMultiSession.send(promptNextChunk); + }); + return; + } + + connection.onmessage({ + data: chunk, + userid: _config.userid, + extra: _config.extra + }); + }); + return; + } + }, + onaddstream: function(stream, session) { + session = session || _config.renegotiate || connection.session; + + // if it is data-only connection; then return. + if (isData(session)) return; + + if (session.screen && (session.audio || session.video)) { + if (!_config.gotAudioOrVideo) { + // audio/video are fired earlier than screen + _config.gotAudioOrVideo = true; + session.screen = false; + } else { + // screen stream is always fired later + session.audio = false; + session.video = false; + } + } + + var preMuted = {}; + + if (_config.streaminfo) { + var streaminfo = _config.streaminfo.split('----'); + var strInfo = JSON.parse(streaminfo[streaminfo.length - 1]); + + if (!isIE) { + stream.streamid = strInfo.streamid; + stream.isScreen = !!strInfo.isScreen; + stream.isVideo = !!strInfo.isVideo; + stream.isAudio = !!strInfo.isAudio; + preMuted = strInfo.preMuted; + } + + streaminfo.pop(); + _config.streaminfo = streaminfo.join('----'); + } + + var mediaElement = createMediaElement(stream, merge({ + remote: true + }, session)); + + if (connection.setDefaultEventsForMediaElement) { + connection.setDefaultEventsForMediaElement(mediaElement, stream.streamid); + } + + if (!isPluginRTC && !stream.getVideoTracks().length) { + function eventListener() { + setTimeout(function() { + mediaElement.muted = false; + afterRemoteStreamStartedFlowing({ + mediaElement: mediaElement, + session: session, + stream: stream, + preMuted: preMuted + }); + }, 3000); + + mediaElement.removeEventListener('play', eventListener); + } + return mediaElement.addEventListener('play', eventListener, false); + } + + waitUntilRemoteStreamStartsFlowing({ + mediaElement: mediaElement, + session: session, + stream: stream, + preMuted: preMuted + }); + }, + + onremovestream: function(stream) { + if (stream && stream.streamid) { + stream = connection.streams[stream.streamid]; + if (stream) { + log('on:stream:ended via on:remove:stream', stream); + onStreamEndedHandler(stream, connection); + } + } else log('on:remove:stream', stream); + }, + + onclose: function(e) { + e.extra = _config.extra; + e.userid = _config.userid; + connection.onclose(e); + + // suggested in #71 by "efaj" + if (connection.channels[e.userid]) { + delete connection.channels[e.userid]; + } + }, + onerror: function(e) { + e.extra = _config.extra; + e.userid = _config.userid; + connection.onerror(e); + }, + + oniceconnectionstatechange: function(event) { + log('oniceconnectionstatechange', toStr(event)); + + if (peer.connection && peer.connection.iceConnectionState == 'connected' && peer.connection.iceGatheringState == 'complete' && peer.connection.signalingState == 'stable' && connection.numberOfConnectedUsers == 1) { + connection.onconnected({ + userid: _config.userid, + extra: _config.extra, + peer: connection.peers[_config.userid], + targetuser: _config.userinfo + }); + } + + if (!connection.isInitiator && peer.connection && peer.connection.iceConnectionState == 'connected' && peer.connection.iceGatheringState == 'complete' && peer.connection.signalingState == 'stable' && connection.numberOfConnectedUsers == 1) { + connection.onstatechange({ + userid: _config.userid, + extra: _config.extra, + name: 'connected-with-initiator', + reason: 'ICE connection state seems connected; gathering state is completed; and signaling state is stable.' + }); + } + + if (connection.peers[_config.userid] && connection.peers[_config.userid].oniceconnectionstatechange) { + connection.peers[_config.userid].oniceconnectionstatechange(event); + } + + // if ICE connectivity check is failed; renegotiate or redial + if (connection.peers[_config.userid] && connection.peers[_config.userid].peer.connection.iceConnectionState == 'failed') { + connection.onfailed({ + userid: _config.userid, + extra: _config.extra, + peer: connection.peers[_config.userid], + targetuser: _config.userinfo + }); + } + + if (connection.peers[_config.userid] && connection.peers[_config.userid].peer.connection.iceConnectionState == 'disconnected') { + !peer.connection.renegotiate && connection.ondisconnected({ + userid: _config.userid, + extra: _config.extra, + peer: connection.peers[_config.userid], + targetuser: _config.userinfo + }); + peer.connection.renegotiate = false; + } + + if (!connection.autoReDialOnFailure) return; + + if (connection.peers[_config.userid]) { + if (connection.peers[_config.userid].peer.connection.iceConnectionState != 'disconnected') { + _config.redialing = false; + } + + if (connection.peers[_config.userid].peer.connection.iceConnectionState == 'disconnected' && !_config.redialing) { + _config.redialing = true; + warn('Peer connection is closed.', toStr(connection.peers[_config.userid].peer.connection), 'ReDialing..'); + connection.peers[_config.userid].socket.send({ + redial: true + }); + + // to make sure all old "remote" streams are also removed! + connection.streams.remove({ + remote: true, + userid: _config.userid + }); + } + } + }, + + onsignalingstatechange: function(event) { + log('onsignalingstatechange', toStr(event)); + }, + + attachStreams: connection.dontAttachStream ? [] : connection.attachStreams, + iceServers: connection.iceServers, + rtcConfiguration: connection.rtcConfiguration, + bandwidth: connection.bandwidth, + sdpConstraints: connection.sdpConstraints, + optionalArgument: connection.optionalArgument, + disableDtlsSrtp: connection.disableDtlsSrtp, + dataChannelDict: connection.dataChannelDict, + preferSCTP: connection.preferSCTP, + + onSessionDescription: function(sessionDescription, streaminfo) { + sendsdp({ + sdp: sessionDescription, + socket: socket, + streaminfo: streaminfo + }); + }, + trickleIce: connection.trickleIce, + processSdp: connection.processSdp, + sendStreamId: function(stream) { + socket && socket.send({ + streamid: stream.streamid, + isScreen: !!stream.isScreen, + isAudio: !!stream.isAudio, + isVideo: !!stream.isVideo + }); + }, + rtcMultiConnection: connection + }; + + function waitUntilRemoteStreamStartsFlowing(args) { + // chrome for android may have some features missing + if (isMobileDevice || isPluginRTC || (isNull(connection.waitUntilRemoteStreamStartsFlowing) || !connection.waitUntilRemoteStreamStartsFlowing)) { + return afterRemoteStreamStartedFlowing(args); + } + + if (!args.numberOfTimes) args.numberOfTimes = 0; + args.numberOfTimes++; + + if (!(args.mediaElement.readyState <= HTMLMediaElement.HAVE_CURRENT_DATA || args.mediaElement.paused || args.mediaElement.currentTime <= 0)) { + return afterRemoteStreamStartedFlowing(args); + } + + if (args.numberOfTimes >= 60) { // wait 60 seconds while video is delivered! + return socket.send({ + failedToReceiveRemoteVideo: true, + streamid: args.stream.streamid + }); + } + + setTimeout(function() { + log('Waiting for incoming remote stream to be started flowing: ' + args.numberOfTimes + ' seconds.'); + waitUntilRemoteStreamStartsFlowing(args); + }, 900); + } + + function initFakeChannel() { + if (!connection.fakeDataChannels || connection.channels[_config.userid]) return; + + // for non-data connections; allow fake data sender! + if (!connection.session.data) { + var fakeChannel = { + send: function(data) { + socket.send({ + fakeData: data + }); + }, + readyState: 'open' + }; + // connection.channels['user-id'].send(data); + connection.channels[_config.userid] = { + channel: fakeChannel, + send: function(data) { + this.channel.send(data); + } + }; + peerConfig.onopen(fakeChannel); + } + } + + function afterRemoteStreamStartedFlowing(args) { + var mediaElement = args.mediaElement; + var session = args.session; + var stream = args.stream; + + stream.onended = function() { + if (streamedObject.mediaElement && !streamedObject.mediaElement.parentNode && document.getElementById(stream.streamid)) { + streamedObject.mediaElement = document.getElementById(stream.streamid); + } + + onStreamEndedHandler(streamedObject, connection); + }; + + var streamedObject = { + mediaElement: mediaElement, + + stream: stream, + streamid: stream.streamid, + session: session || connection.session, + + blobURL: isPluginRTC ? '' : mediaElement.mozSrcObject ? URL.createObjectURL(stream) : mediaElement.src, + type: 'remote', + + extra: _config.extra, + userid: _config.userid, + + isVideo: isPluginRTC ? !!session.video : !!stream.isVideo, + isAudio: isPluginRTC ? !!session.audio && !session.video : !!stream.isAudio, + isScreen: !!stream.isScreen, + isInitiator: !!_config.isInitiator, + + rtcMultiConnection: connection, + socket: socket + }; + + // connection.streams['stream-id'].mute({audio:true}) + connection.streams[stream.streamid] = connection._getStream(streamedObject); + connection.onstream(streamedObject); + + if (!isEmpty(args.preMuted) && (args.preMuted.audio || args.preMuted.video)) { + var fakeObject = merge({}, streamedObject); + fakeObject.session = merge(fakeObject.session, args.preMuted); + + fakeObject.isAudio = !!fakeObject.session.audio && !fakeObject.session.video; + fakeObject.isVideo = !!fakeObject.session.video; + fakeObject.isScreen = false; + + connection.onmute(fakeObject); + } + + log('on:add:stream', streamedObject); + + onSessionOpened(); + + if (connection.onspeaking) { + initHark({ + stream: stream, + streamedObject: streamedObject, + connection: connection + }); + } + } + + function onChannelOpened(channel) { + _config.channel = channel; + + // connection.channels['user-id'].send(data); + connection.channels[_config.userid] = { + channel: _config.channel, + send: function(data) { + connection.send(data, this.channel); + } + }; + + connection.onopen({ + extra: _config.extra, + userid: _config.userid, + channel: channel + }); + + // fetch files from file-queue + for (var q in connection.fileQueue) { + connection.send(connection.fileQueue[q], channel); + } + + if (isData(connection.session)) onSessionOpened(); + + if (connection.partOfScreen && connection.partOfScreen.sharing) { + connection.peers[_config.userid].sharePartOfScreen(connection.partOfScreen); + } + } + + function updateSocket() { + // todo: need to check following {if-block} MUST not affect "redial" process + if (socket.userid == _config.userid) + return; + + socket.userid = _config.userid; + sockets[_config.socketIndex] = socket; + + connection.numberOfConnectedUsers++; + // connection.peers['user-id'].addStream({audio:true}) + connection.peers[_config.userid] = { + socket: socket, + peer: peer, + userid: _config.userid, + extra: _config.extra, + userinfo: _config.userinfo, + addStream: function(session00) { + // connection.peers['user-id'].addStream({audio: true, video: true); + + connection.addStream(session00, this.socket); + }, + removeStream: function(streamid) { + if (!connection.streams[streamid]) + return warn('No such stream exists. Stream-id:', streamid); + + this.peer.connection.removeStream(connection.streams[streamid].stream); + this.renegotiate(); + }, + renegotiate: function(stream, session) { + // connection.peers['user-id'].renegotiate(); + + connection.renegotiate(stream, session); + }, + changeBandwidth: function(bandwidth) { + // connection.peers['user-id'].changeBandwidth(); + + if (!bandwidth) throw 'You MUST pass bandwidth object.'; + if (isString(bandwidth)) throw 'Pass object for bandwidth instead of string; e.g. {audio:10, video:20}'; + + // set bandwidth for self + this.peer.bandwidth = bandwidth; + + // ask remote user to synchronize bandwidth + this.socket.send({ + changeBandwidth: true, + bandwidth: bandwidth + }); + }, + sendCustomMessage: function(message) { + // connection.peers['user-id'].sendCustomMessage(); + + this.socket.send({ + customMessage: true, + message: message + }); + }, + onCustomMessage: function(message) { + log('Received "private" message from', this.userid, + isString(message) ? message : toStr(message)); + }, + drop: function(dontSendMessage) { + // connection.peers['user-id'].drop(); + + for (var stream in connection.streams) { + if (connection._skip.indexOf(stream) == -1) { + stream = connection.streams[stream]; + + if (stream.userid == connection.userid && stream.type == 'local') { + this.peer.connection.removeStream(stream.stream); + onStreamEndedHandler(stream, connection); + } + + if (stream.type == 'remote' && stream.userid == this.userid) { + onStreamEndedHandler(stream, connection); + } + } + } + + !dontSendMessage && this.socket.send({ + drop: true + }); + }, + hold: function(holdMLine) { + // connection.peers['user-id'].hold(); + + if (peer.prevCreateType == 'answer') { + this.socket.send({ + unhold: true, + holdMLine: holdMLine || 'both', + takeAction: true + }); + return; + } + + this.socket.send({ + hold: true, + holdMLine: holdMLine || 'both' + }); + + this.peer.hold = true; + this.fireHoldUnHoldEvents({ + kind: holdMLine, + isHold: true, + userid: connection.userid, + remoteUser: this.userid + }); + }, + unhold: function(holdMLine) { + // connection.peers['user-id'].unhold(); + + if (peer.prevCreateType == 'answer') { + this.socket.send({ + unhold: true, + holdMLine: holdMLine || 'both', + takeAction: true + }); + return; + } + + this.socket.send({ + unhold: true, + holdMLine: holdMLine || 'both' + }); + + this.peer.hold = false; + this.fireHoldUnHoldEvents({ + kind: holdMLine, + isHold: false, + userid: connection.userid, + remoteUser: this.userid + }); + }, + fireHoldUnHoldEvents: function(e) { + // this method is for inner usages only! + + var isHold = e.isHold; + var kind = e.kind; + var userid = e.remoteUser || e.userid; + + // hold means inactive a specific media line! + // a media line can contain multiple synced sources (ssrc) + // i.e. a media line can reference multiple tracks! + // that's why hold will affect all relevant tracks in a specific media line! + for (var stream in connection.streams) { + if (connection._skip.indexOf(stream) == -1) { + stream = connection.streams[stream]; + + if (stream.userid == userid) { + // www.RTCMultiConnection.org/docs/onhold/ + if (isHold) + connection.onhold(merge({ + kind: kind + }, stream)); + + // www.RTCMultiConnection.org/docs/onunhold/ + if (!isHold) + connection.onunhold(merge({ + kind: kind + }, stream)); + } + } + } + }, + redial: function() { + // connection.peers['user-id'].redial(); + + // 1st of all; remove all relevant remote media streams + for (var stream in connection.streams) { + if (connection._skip.indexOf(stream) == -1) { + stream = connection.streams[stream]; + + if (stream.userid == this.userid && stream.type == 'remote') { + onStreamEndedHandler(stream, connection); + } + } + } + + log('ReDialing...'); + + socket.send({ + recreatePeer: true + }); + + peer = new PeerConnection(); + peer.create('offer', peerConfig); + }, + sharePartOfScreen: function(args) { + // www.RTCMultiConnection.org/docs/onpartofscreen/ + var that = this; + var lastScreenshot = ''; + + function partOfScreenCapturer() { + // if stopped + if (that.stopPartOfScreenSharing) { + that.stopPartOfScreenSharing = false; + + if (connection.onpartofscreenstopped) { + connection.onpartofscreenstopped(); + } + return; + } + + // if paused + if (that.pausePartOfScreenSharing) { + if (connection.onpartofscreenpaused) { + connection.onpartofscreenpaused(); + } + + return setTimeout(partOfScreenCapturer, args.interval || 200); + } + + capturePartOfScreen({ + element: args.element, + connection: connection, + callback: function(screenshot) { + if (!connection.channels[that.userid]) { + throw 'No such data channel exists.'; + } + + // don't share repeated content + if (screenshot != lastScreenshot) { + lastScreenshot = screenshot; + connection.channels[that.userid].send({ + screenshot: screenshot, + isPartOfScreen: true + }); + } + + // "once" can be used to share single screenshot + !args.once && setTimeout(partOfScreenCapturer, args.interval || 200); + } + }); + } + + partOfScreenCapturer(); + }, + getConnectionStats: function(callback, interval) { + if (!callback) throw 'callback is mandatory.'; + + if (!window.getConnectionStats) { + loadScript(connection.resources.getConnectionStats, invoker); + } else invoker(); + + function invoker() { + RTCPeerConnection.prototype.getConnectionStats = window.getConnectionStats; + peer.connection && peer.connection.getConnectionStats(callback, interval); + } + }, + takeSnapshot: function(callback) { + takeSnapshot({ + userid: this.userid, + connection: connection, + callback: callback + }); + } + }; + } + + function onSessionOpened() { + // original conferencing infrastructure! + if (connection.isInitiator && getLength(participants) && getLength(participants) <= connection.maxParticipantsAllowed) { + if (!connection.session.oneway && !connection.session.broadcast) { + defaultSocket.send({ + sessionid: connection.sessionid, + newParticipant: _config.userid || socket.channel, + userData: { + userid: _config.userid || socket.channel, + extra: _config.extra + } + }); + } + } + + // 1st: renegotiation is supported only on chrome + // 2nd: must not renegotiate same media multiple times + // 3rd: todo: make sure that target-user has no such "renegotiated" media. + if (_config.userinfo.browser == 'chrome' && !_config.renegotiatedOnce) { + // this code snippet is added to make sure that "previously-renegotiated" streams are also + // renegotiated to this new user + for (var rSession in connection.renegotiatedSessions) { + _config.renegotiatedOnce = true; + + if (connection.renegotiatedSessions[rSession] && connection.renegotiatedSessions[rSession].stream) { + connection.peers[_config.userid].renegotiate(connection.renegotiatedSessions[rSession].stream, connection.renegotiatedSessions[rSession].session); + } + } + } + } + + function socketResponse(response) { + if (isRMSDeleted) return; + + if (response.userid == connection.userid) + return; + + if (response.sdp) { + _config.userid = response.userid; + _config.extra = response.extra || {}; + _config.renegotiate = response.renegotiate; + _config.streaminfo = response.streaminfo; + _config.isInitiator = response.isInitiator; + _config.userinfo = response.userinfo; + + var sdp = JSON.parse(response.sdp); + + if (sdp.type == 'offer') { + // to synchronize SCTP or RTP + peerConfig.preferSCTP = !!response.preferSCTP; + connection.fakeDataChannels = !!response.fakeDataChannels; + } + + // initializing fake channel + initFakeChannel(); + + sdpInvoker(sdp, response.labels); + } + + if (response.candidate) { + peer && peer.addIceCandidate(JSON.parse(response.candidate)); + } + + if (response.streamid) { + if (!rtcMultiSession.streamids) { + rtcMultiSession.streamids = {}; + } + if (!rtcMultiSession.streamids[response.streamid]) { + rtcMultiSession.streamids[response.streamid] = response.streamid; + connection.onstreamid(response); + } + } + + if (response.mute || response.unmute) { + if (response.promptMuteUnmute) { + if (!connection.privileges.canMuteRemoteStream) { + connection.onstatechange({ + userid: response.userid, + extra: response.extra, + name: 'mute-request-denied', + reason: response.userid + ' tried to mute your stream; however "privileges.canMuteRemoteStream" is "false".' + }); + return; + } + + if (connection.streams[response.streamid]) { + if (response.mute && !connection.streams[response.streamid].muted) { + connection.streams[response.streamid].mute(response.session); + } + if (response.unmute && connection.streams[response.streamid].muted) { + connection.streams[response.streamid].unmute(response.session); + } + } + } else { + var streamObject = {}; + if (connection.streams[response.streamid]) { + streamObject = connection.streams[response.streamid]; + } + + var session = response.session; + var fakeObject = merge({}, streamObject); + fakeObject.session = session; + + fakeObject.isAudio = !!fakeObject.session.audio && !fakeObject.session.video; + fakeObject.isVideo = !!fakeObject.session.video; + fakeObject.isScreen = !!fakeObject.session.screen; + + if (response.mute) connection.onmute(fakeObject || response); + if (response.unmute) connection.onunmute(fakeObject || response); + } + } + + if (response.isVolumeChanged) { + log('Volume of stream: ' + response.streamid + ' has changed to: ' + response.volume); + if (connection.streams[response.streamid]) { + var mediaElement = connection.streams[response.streamid].mediaElement; + if (mediaElement) mediaElement.volume = response.volume; + } + } + + // to stop local stream + if (response.stopped) { + if (connection.streams[response.streamid]) { + onStreamEndedHandler(connection.streams[response.streamid], connection); + } + } + + // to stop remote stream + if (response.promptStreamStop /* && !connection.isInitiator */ ) { + if (!connection.privileges.canStopRemoteStream) { + connection.onstatechange({ + userid: response.userid, + extra: response.extra, + name: 'stop-request-denied', + reason: response.userid + ' tried to stop your stream; however "privileges.canStopRemoteStream" is "false".' + }); + return; + } + warn('Remote stream has been manually stopped!'); + if (connection.streams[response.streamid]) { + connection.streams[response.streamid].stop(); + } + } + + if (response.left) { + // firefox is unable to stop remote streams + // firefox doesn't auto stop streams when peer.close() is called. + if (isFirefox) { + var userLeft = response.userid; + for (var stream in connection.streams) { + stream = connection.streams[stream]; + if (stream.userid == userLeft) { + connection.stopMediaStream(stream); + onStreamEndedHandler(stream, connection); + } + } + } + + if (peer && peer.connection) { + // todo: verify if-block's 2nd condition + if (peer.connection.signalingState != 'closed' && peer.connection.iceConnectionState.search(/disconnected|failed/gi) == -1) { + peer.connection.close(); + } + peer.connection = null; + } + + if (participants[response.userid]) delete participants[response.userid]; + + if (response.closeEntireSession) { + connection.onSessionClosed(response); + connection.leave(); + return; + } + + connection.remove(response.userid); + + onLeaveHandler({ + userid: response.userid, + extra: response.extra || {}, + entireSessionClosed: !!response.closeEntireSession + }, connection); + } + + // keeping session active even if initiator leaves + if (response.playRoleOfBroadcaster) { + if (response.extra) { + // clone extra-data from initial moderator + connection.extra = merge(connection.extra, response.extra); + } + if (response.participants) { + participants = response.participants; + + // make sure that if 2nd initiator leaves; control is shifted to 3rd person. + if (participants[connection.userid]) { + delete participants[connection.userid]; + } + + if (sockets[0] && sockets[0].userid == response.userid) { + delete sockets[0]; + sockets = swap(sockets); + } + + if (socketObjects[response.userid]) { + delete socketObjects[response.userid]; + } + } + + setTimeout(connection.playRoleOfInitiator, 2000); + } + + if (response.changeBandwidth) { + if (!connection.peers[response.userid]) throw 'No such peer exists.'; + + // synchronize bandwidth + connection.peers[response.userid].peer.bandwidth = response.bandwidth; + + // renegotiate to apply bandwidth + connection.peers[response.userid].renegotiate(); + } + + if (response.customMessage) { + if (!connection.peers[response.userid]) throw 'No such peer exists.'; + if (response.message.ejected) { + if (connection.sessionDescriptions[connection.sessionid].userid != response.userid) { + throw 'only initiator can eject a user.'; + } + // initiator ejected this user + connection.leave(); + + connection.onSessionClosed({ + userid: response.userid, + extra: response.extra || _config.extra, + isEjected: true + }); + } else connection.peers[response.userid].onCustomMessage(response.message); + } + + if (response.drop) { + if (!connection.peers[response.userid]) throw 'No such peer exists.'; + connection.peers[response.userid].drop(true); + connection.peers[response.userid].renegotiate(); + + connection.ondrop(response.userid); + } + + if (response.hold || response.unhold) { + if (!connection.peers[response.userid]) throw 'No such peer exists.'; + + if (response.takeAction) { + connection.peers[response.userid][!!response.hold ? 'hold' : 'unhold'](response.holdMLine); + return; + } + + connection.peers[response.userid].peer.hold = !!response.hold; + connection.peers[response.userid].peer.holdMLine = response.holdMLine; + + socket.send({ + isRenegotiate: true + }); + + connection.peers[response.userid].fireHoldUnHoldEvents({ + kind: response.holdMLine, + isHold: !!response.hold, + userid: response.userid + }); + } + + if (response.isRenegotiate) { + connection.peers[response.userid].renegotiate(null, connection.peers[response.userid].peer.session); + } + + // fake data channels! + if (response.fakeData) { + peerConfig.onmessage(response.fakeData); + } + + // sometimes we don't need to renegotiate e.g. when peers are disconnected + // or if it is firefox + if (response.recreatePeer) { + peer = new PeerConnection(); + } + + // remote video failed either out of ICE gathering process or ICE connectivity check-up + // or IceAgent was unable to locate valid candidates/ports. + if (response.failedToReceiveRemoteVideo) { + log('Remote peer hasn\'t received stream: ' + response.streamid + '. Renegotiating...'); + if (connection.peers[response.userid]) { + connection.peers[response.userid].renegotiate(); + } + } + + if (response.redial) { + if (connection.peers[response.userid]) { + if (connection.peers[response.userid].peer.connection.iceConnectionState != 'disconnected') { + _config.redialing = false; + } + + if (connection.peers[response.userid].peer.connection.iceConnectionState == 'disconnected' && !_config.redialing) { + _config.redialing = true; + + warn('Peer connection is closed.', toStr(connection.peers[response.userid].peer.connection), 'ReDialing..'); + connection.peers[response.userid].redial(); + } + } + } + } + + connection.playRoleOfInitiator = function() { + connection.dontCaptureUserMedia = true; + connection.open(); + sockets = swap(sockets); + connection.dontCaptureUserMedia = false; + }; + + connection.askToShareParticipants = function() { + defaultSocket && defaultSocket.send({ + askToShareParticipants: true + }); + }; + + connection.shareParticipants = function(args) { + var message = { + joinUsers: participants, + userid: connection.userid, + extra: connection.extra + }; + + if (args) { + if (args.dontShareWith) message.dontShareWith = args.dontShareWith; + if (args.shareWith) message.shareWith = args.shareWith; + } + + defaultSocket.send(message); + }; + + function sdpInvoker(sdp, labels) { + if (sdp.type == 'answer') { + peer.setRemoteDescription(sdp); + updateSocket(); + return; + } + if (!_config.renegotiate && sdp.type == 'offer') { + peerConfig.offerDescription = sdp; + + peerConfig.session = connection.session; + if (!peer) peer = new PeerConnection(); + peer.create('answer', peerConfig); + + updateSocket(); + return; + } + + var session = _config.renegotiate; + // detach streams + detachMediaStream(labels, peer.connection); + + if (session.oneway || isData(session)) { + createAnswer(); + delete _config.renegotiate; + } else { + if (_config.capturing) + return; + + _config.capturing = true; + + connection.captureUserMedia(function(stream) { + _config.capturing = false; + + peer.addStream(stream); + + connection.renegotiatedSessions[JSON.stringify(_config.renegotiate)] = { + session: _config.renegotiate, + stream: stream + }; + + delete _config.renegotiate; + + createAnswer(); + }, _config.renegotiate); + } + + function createAnswer() { + peer.recreateAnswer(sdp, session, function(_sdp, streaminfo) { + sendsdp({ + sdp: _sdp, + socket: socket, + streaminfo: streaminfo + }); + connection.detachStreams = []; + }); + } + } + } + + function detachMediaStream(labels, peer) { + if (!labels) return; + for (var i = 0; i < labels.length; i++) { + var label = labels[i]; + if (connection.streams[label]) { + peer.removeStream(connection.streams[label].stream); + } + } + } + + function sendsdp(e) { + e.socket.send({ + sdp: JSON.stringify({ + sdp: e.sdp.sdp, + type: e.sdp.type + }), + renegotiate: !!e.renegotiate ? e.renegotiate : false, + streaminfo: e.streaminfo || '', + labels: e.labels || [], + preferSCTP: !!connection.preferSCTP, + fakeDataChannels: !!connection.fakeDataChannels, + isInitiator: !!connection.isInitiator, + userinfo: { + browser: isFirefox ? 'firefox' : 'chrome' + } + }); + } + + // sharing new user with existing participants + + function onNewParticipant(response) { + var channel = response.newParticipant; + + if (!channel || !!participants[channel] || channel == connection.userid) + return; + + var new_channel = connection.token(); + newPrivateSocket({ + channel: new_channel, + extra: response.userData ? response.userData.extra : response.extra, + userid: response.userData ? response.userData.userid : response.userid + }); + + defaultSocket.send({ + participant: true, + targetUser: channel, + channel: new_channel + }); + } + + // if a user leaves + + function clearSession() { + connection.numberOfConnectedUsers--; + + var alertMessage = { + left: true, + extra: connection.extra || {}, + userid: connection.userid, + sessionid: connection.sessionid + }; + + if (connection.isInitiator) { + // if initiator wants to close entire session + if (connection.autoCloseEntireSession) { + alertMessage.closeEntireSession = true; + } else if (sockets[0]) { + // shift initiation control to another user + sockets[0].send({ + playRoleOfBroadcaster: true, + userid: connection.userid, + extra: connection.extra, + participants: participants + }); + } + } + + sockets.forEach(function(socket, i) { + socket.send(alertMessage); + + if (socketObjects[socket.channel]) { + delete socketObjects[socket.channel]; + } + + delete sockets[i]; + }); + + sockets = swap(sockets); + + connection.refresh(); + + webAudioMediaStreamSources.forEach(function(mediaStreamSource) { + // if source is connected; then chrome will crash on unload. + mediaStreamSource.disconnect(); + }); + + webAudioMediaStreamSources = []; + } + + // www.RTCMultiConnection.org/docs/remove/ + connection.remove = function(userid) { + if (rtcMultiSession.requestsFrom && rtcMultiSession.requestsFrom[userid]) delete rtcMultiSession.requestsFrom[userid]; + + if (connection.peers[userid]) { + if (connection.peers[userid].peer && connection.peers[userid].peer.connection) { + if (connection.peers[userid].peer.connection.signalingState != 'closed') { + connection.peers[userid].peer.connection.close(); + } + connection.peers[userid].peer.connection = null; + } + delete connection.peers[userid]; + } + if (participants[userid]) { + delete participants[userid]; + } + + for (var stream in connection.streams) { + stream = connection.streams[stream]; + if (stream.userid == userid) { + onStreamEndedHandler(stream, connection); + delete connection.streams[stream]; + } + } + + if (socketObjects[userid]) { + delete socketObjects[userid]; + } + }; + + // www.RTCMultiConnection.org/docs/refresh/ + connection.refresh = function() { + // if firebase; remove data from firebase servers + if (connection.isInitiator && !!connection.socket && !!connection.socket.remove) { + connection.socket.remove(); + } + + participants = {}; + + // to stop/remove self streams + for (var i = 0; i < connection.attachStreams.length; i++) { + connection.stopMediaStream(connection.attachStreams[i]); + } + + // to allow capturing of identical streams + currentUserMediaRequest = { + streams: [], + mutex: false, + queueRequests: [] + }; + + rtcMultiSession.isOwnerLeaving = true; + + connection.isInitiator = false; + connection.isAcceptNewSession = true; + connection.attachMediaStreams = []; + connection.sessionDescription = null; + connection.sessionDescriptions = {}; + connection.localStreamids = []; + connection.preRecordedMedias = {}; + connection.snapshots = {}; + + connection.numberOfConnectedUsers = 0; + connection.numberOfSessions = 0; + + connection.attachStreams = []; + connection.detachStreams = []; + connection.fileQueue = {}; + connection.channels = {}; + connection.renegotiatedSessions = {}; + + for (var peer in connection.peers) { + if (peer != connection.userid) { + delete connection.peers[peer]; + } + } + + // to make sure remote streams are also removed! + for (var stream in connection.streams) { + if (connection._skip.indexOf(stream) == -1) { + onStreamEndedHandler(connection.streams[stream], connection); + delete connection.streams[stream]; + } + } + + socketObjects = {}; + sockets = []; + participants = {}; + }; + + // www.RTCMultiConnection.org/docs/reject/ + connection.reject = function(userid) { + if (!isString(userid)) userid = userid.userid; + defaultSocket.send({ + rejectedRequestOf: userid + }); + + // remove relevant data to allow him join again + connection.remove(userid); + }; + + rtcMultiSession.leaveHandler = function(e) { + if (!connection.leaveOnPageUnload) return; + + if (isNull(e.keyCode)) { + return clearSession(); + } + + if (e.keyCode == 116) { + clearSession(); + } + }; + + listenEventHandler('beforeunload', rtcMultiSession.leaveHandler); + listenEventHandler('keyup', rtcMultiSession.leaveHandler); + + rtcMultiSession.onLineOffLineHandler = function() { + if (!navigator.onLine) { + rtcMultiSession.isOffLine = true; + } else if (rtcMultiSession.isOffLine) { + rtcMultiSession.isOffLine = !navigator.onLine; + + // defaultSocket = getDefaultSocketRef(); + + // pending tasks should be resumed? + // sockets should be reconnected? + // peers should be re-established? + } + }; + + listenEventHandler('load', rtcMultiSession.onLineOffLineHandler); + listenEventHandler('online', rtcMultiSession.onLineOffLineHandler); + listenEventHandler('offline', rtcMultiSession.onLineOffLineHandler); + + function onSignalingReady() { + if (rtcMultiSession.signalingReady) return; + rtcMultiSession.signalingReady = true; + + setTimeout(callbackForSignalingReady, 1000); + + if (!connection.isInitiator) { + // as soon as signaling gateway is connected; + // user should check existing rooms! + defaultSocket && defaultSocket.send({ + searchingForRooms: true + }); + } + } + + function joinParticipants(joinUsers) { + for (var user in joinUsers) { + if (!participants[joinUsers[user]]) { + onNewParticipant({ + sessionid: connection.sessionid, + newParticipant: joinUsers[user], + userid: connection.userid, + extra: connection.extra + }); + } + } + } + + function getDefaultSocketRef() { + return connection.openSignalingChannel({ + onmessage: function(response) { + // RMS == RTCMultiSession + if (isRMSDeleted) return; + + // if message is sent by same user + if (response.userid == connection.userid) return; + + if (response.sessionid && response.userid) { + if (!connection.sessionDescriptions[response.sessionid]) { + connection.numberOfSessions++; + connection.sessionDescriptions[response.sessionid] = response; + + // fire "onNewSession" only if: + // 1) "isAcceptNewSession" boolean is true + // 2) "sessionDescriptions" object isn't having same session i.e. to prevent duplicate invocations + if (connection.isAcceptNewSession) { + + if (!connection.dontOverrideSession) { + connection.session = response.session; + } + + onNewSession(response); + } + } + } + + if (response.newParticipant && !connection.isAcceptNewSession && rtcMultiSession.broadcasterid === response.userid) { + if (response.newParticipant != connection.userid) { + onNewParticipant(response); + } + } + + if (getLength(participants) < connection.maxParticipantsAllowed && response.targetUser == connection.userid && response.participant) { + if (connection.peers[response.userid] && !connection.peers[response.userid].peer) { + delete participants[response.userid]; + delete connection.peers[response.userid]; + connection.isAcceptNewSession = true; + return acceptRequest(response); + } + + if (!participants[response.userid]) { + acceptRequest(response); + } + } + + if (response.acceptedRequestOf == connection.userid) { + connection.onstatechange({ + userid: response.userid, + extra: response.extra, + name: 'request-accepted', + reason: response.userid + ' accepted your participation request.' + }); + } + + if (response.rejectedRequestOf == connection.userid) { + connection.onstatechange({ + userid: response.userid, + extra: response.extra, + name: 'request-rejected', + reason: response.userid + ' rejected your participation request.' + }); + } + + if (response.customMessage) { + if (response.message.drop) { + connection.ondrop(response.userid); + + connection.attachStreams = []; + // "drop" should detach all local streams + for (var stream in connection.streams) { + if (connection._skip.indexOf(stream) == -1) { + stream = connection.streams[stream]; + if (stream.type == 'local') { + connection.detachStreams.push(stream.streamid); + onStreamEndedHandler(stream, connection); + } else onStreamEndedHandler(stream, connection); + } + } + + if (response.message.renegotiate) { + // renegotiate; so "peer.removeStream" happens. + connection.renegotiate(); + } + } else if (connection.onCustomMessage) { + connection.onCustomMessage(response.message); + } + } + + if (connection.isInitiator && response.searchingForRooms) { + defaultSocket && defaultSocket.send({ + sessionDescription: connection.sessionDescription, + responseFor: response.userid + }); + } + + if (response.sessionDescription && response.responseFor == connection.userid) { + var sessionDescription = response.sessionDescription; + if (!connection.sessionDescriptions[sessionDescription.sessionid]) { + connection.numberOfSessions++; + connection.sessionDescriptions[sessionDescription.sessionid] = sessionDescription; + } + } + + if (connection.isInitiator && response.askToShareParticipants && defaultSocket) { + connection.shareParticipants({ + shareWith: response.userid + }); + } + + // participants are shared with single user + if (response.shareWith == connection.userid && response.dontShareWith != connection.userid && response.joinUsers) { + joinParticipants(response.joinUsers); + } + + // participants are shared with all users + if (!response.shareWith && response.joinUsers) { + if (response.dontShareWith) { + if (connection.userid != response.dontShareWith) { + joinParticipants(response.joinUsers); + } + } else joinParticipants(response.joinUsers); + } + + if (response.messageFor == connection.userid && response.presenceState) { + if (response.presenceState == 'checking') { + defaultSocket.send({ + messageFor: response.userid, + presenceState: 'available', + _config: response._config + }); + log('participant asked for availability'); + } + + if (response.presenceState == 'available') { + rtcMultiSession.presenceState = 'available'; + + connection.onstatechange({ + userid: 'browser', + extra: {}, + name: 'room-available', + reason: 'Initiator is available and room is active.' + }); + + joinSession(response._config); + } + } + + if (response.donotJoin && response.messageFor == connection.userid) { + log(response.userid, 'is not joining your room.'); + } + + // if initiator disconnects sockets, participants should also disconnect + if (response.isDisconnectSockets) { + log('Disconnecting your sockets because initiator also disconnected his sockets.'); + connection.disconnect(); + } + }, + callback: function(socket) { + socket && this.onopen(socket); + }, + onopen: function(socket) { + if (socket) defaultSocket = socket; + if (onSignalingReady) onSignalingReady(); + + rtcMultiSession.defaultSocket = defaultSocket; + + if (!defaultSocket.__push) { + defaultSocket.__push = defaultSocket.send; + defaultSocket.send = function(message) { + message.userid = message.userid || connection.userid; + message.extra = message.extra || connection.extra || {}; + + defaultSocket.__push(message); + }; + } + } + }); + } + + // default-socket is a common socket shared among all users in a specific channel; + // to share participation requests; room descriptions; and other stuff. + var defaultSocket = getDefaultSocketRef(); + + rtcMultiSession.defaultSocket = defaultSocket; + + if (defaultSocket && onSignalingReady) setTimeout(onSignalingReady, 2000); + + if (connection.session.screen) { + loadScreenFrame(); + } + + if (connection.log == false) connection.skipLogs(); + if (connection.onlog) { + log = warn = error = function() { + var log = {}; + var index = 0; + Array.prototype.slice.call(arguments).forEach(function(argument) { + log[index++] = toStr(argument); + }); + toStr = function(str) { + return str; + }; + connection.onlog(log); + }; + } + + function setDirections() { + var userMaxParticipantsAllowed = 0; + + // if user has set a custom max participant setting, remember it + if (connection.maxParticipantsAllowed != 256) { + userMaxParticipantsAllowed = connection.maxParticipantsAllowed; + } + + if (connection.direction == 'one-way') connection.session.oneway = true; + if (connection.direction == 'one-to-one') connection.maxParticipantsAllowed = 1; + if (connection.direction == 'one-to-many') connection.session.broadcast = true; + if (connection.direction == 'many-to-many') { + if (!connection.maxParticipantsAllowed || connection.maxParticipantsAllowed == 1) { + connection.maxParticipantsAllowed = 256; + } + } + + // if user has set a custom max participant setting, set it back + if (userMaxParticipantsAllowed && connection.maxParticipantsAllowed != 1) { + connection.maxParticipantsAllowed = userMaxParticipantsAllowed; + } + } + + // open new session + this.initSession = function(args) { + rtcMultiSession.isOwnerLeaving = false; + + setDirections(); + participants = {}; + + rtcMultiSession.isOwnerLeaving = false; + + if (!isNull(args.transmitRoomOnce)) { + connection.transmitRoomOnce = args.transmitRoomOnce; + } + + function transmit() { + if (defaultSocket && getLength(participants) < connection.maxParticipantsAllowed && !rtcMultiSession.isOwnerLeaving) { + defaultSocket.send(connection.sessionDescription); + } + + if (!connection.transmitRoomOnce && !rtcMultiSession.isOwnerLeaving) + setTimeout(transmit, connection.interval || 3000); + } + + // todo: test and fix next line. + if (!args.dontTransmit /* || connection.transmitRoomOnce */ ) transmit(); + }; + + function joinSession(_config, skipOnStateChange) { + if (rtcMultiSession.donotJoin && rtcMultiSession.donotJoin == _config.sessionid) { + return; + } + + // dontOverrideSession allows you force RTCMultiConnection + // to not override default session for participants; + // by default, session is always overridden and set to the session coming from initiator! + if (!connection.dontOverrideSession) { + connection.session = _config.session || {}; + } + + // make sure that inappropriate users shouldn't receive onNewSession event + rtcMultiSession.broadcasterid = _config.userid; + + if (_config.sessionid) { + // used later to prevent external rooms messages to be used by this user! + connection.sessionid = _config.sessionid; + } + + connection.isAcceptNewSession = false; + + var channel = getRandomString(); + newPrivateSocket({ + channel: channel, + extra: _config.extra || {}, + userid: _config.userid + }); + + var offers = {}; + if (connection.attachStreams.length) { + var stream = connection.attachStreams[connection.attachStreams.length - 1]; + if (!!stream.getAudioTracks && stream.getAudioTracks().length) { + offers.audio = true; + } + if (stream.getVideoTracks().length) { + offers.video = true; + } + } + + if (!isEmpty(offers)) { + log(toStr(offers)); + } else log('Seems data-only connection.'); + + connection.onstatechange({ + userid: _config.userid, + extra: {}, + name: 'connecting-with-initiator', + reason: 'Checking presence of the initiator; and the room.' + }); + + defaultSocket.send({ + participant: true, + channel: channel, + targetUser: _config.userid, + session: connection.session, + offers: { + audio: !!offers.audio, + video: !!offers.video + } + }); + + connection.skipOnNewSession = false; + invokeMediaCaptured(connection); + } + + // join existing session + this.joinSession = function(_config) { + if (!defaultSocket) + return setTimeout(function() { + warn('Default-Socket is not yet initialized.'); + rtcMultiSession.joinSession(_config); + }, 1000); + + _config = _config || {}; + participants = {}; + + rtcMultiSession.presenceState = 'checking'; + + connection.onstatechange({ + userid: _config.userid, + extra: _config.extra || {}, + name: 'detecting-room-presence', + reason: 'Checking presence of the room.' + }); + + function contactInitiator() { + defaultSocket.send({ + messageFor: _config.userid, + presenceState: rtcMultiSession.presenceState, + _config: { + userid: _config.userid, + extra: _config.extra || {}, + sessionid: _config.sessionid, + session: _config.session || false + } + }); + } + contactInitiator(); + + function checker() { + if (rtcMultiSession.presenceState == 'checking') { + warn('Unable to reach initiator. Trying again...'); + contactInitiator(); + setTimeout(function() { + if (rtcMultiSession.presenceState == 'checking') { + connection.onstatechange({ + userid: _config.userid, + extra: _config.extra || {}, + name: 'room-not-available', + reason: 'Initiator seems absent. Waiting for someone to open the room.' + }); + + connection.isAcceptNewSession = true; + setTimeout(checker, 2000); + } + }, 2000); + } + } + + setTimeout(checker, 3000); + }; + + connection.donotJoin = function(sessionid) { + rtcMultiSession.donotJoin = sessionid; + + var session = connection.sessionDescriptions[sessionid]; + if (!session) return; + + defaultSocket.send({ + donotJoin: true, + messageFor: session.userid, + sessionid: sessionid + }); + + participants = {}; + connection.isAcceptNewSession = true; + connection.sessionid = null; + }; + + // send file/data or text message + this.send = function(message, _channel) { + if (!(message instanceof ArrayBuffer || message instanceof DataView)) { + message = str2ab({ + extra: connection.extra, + userid: connection.userid, + data: message + }); + } + + if (_channel) { + if (_channel.readyState == 'open') { + _channel.send(message); + } + return; + } + + for (var dataChannel in connection.channels) { + var channel = connection.channels[dataChannel].channel; + if (channel.readyState == 'open') { + channel.send(message); + } + } + }; + + // leave session + this.leave = function() { + clearSession(); + }; + + // renegotiate new stream + this.addStream = function(e) { + var session = e.renegotiate; + + if (!connection.renegotiatedSessions[JSON.stringify(e.renegotiate)]) { + connection.renegotiatedSessions[JSON.stringify(e.renegotiate)] = { + session: e.renegotiate, + stream: e.stream + }; + } + + if (e.socket) { + if (e.socket.userid != connection.userid) { + addStream(connection.peers[e.socket.userid]); + } + } else { + for (var peer in connection.peers) { + if (peer != connection.userid) { + addStream(connection.peers[peer]); + } + } + } + + function addStream(_peer) { + var socket = _peer.socket; + + if (!socket) { + warn(_peer, 'doesn\'t has socket.'); + return; + } + + updateSocketForLocalStreams(socket); + + if (!_peer || !_peer.peer) { + throw 'No peer to renegotiate.'; + } + + var peer = _peer.peer; + + if (e.stream) { + if (!peer.attachStreams) { + peer.attachStreams = []; + } + + peer.attachStreams.push(e.stream); + } + + // detaching old streams + detachMediaStream(connection.detachStreams, peer.connection); + + if (e.stream && (session.audio || session.video || session.screen)) { + peer.addStream(e.stream); + } + + peer.recreateOffer(session, function(sdp, streaminfo) { + sendsdp({ + sdp: sdp, + socket: socket, + renegotiate: session, + labels: connection.detachStreams, + streaminfo: streaminfo + }); + connection.detachStreams = []; + }); + } + }; + + // www.RTCMultiConnection.org/docs/request/ + connection.request = function(userid, extra) { + connection.captureUserMedia(function() { + // open private socket that will be used to receive offer-sdp + newPrivateSocket({ + channel: connection.userid, + extra: extra || {}, + userid: userid + }); + + // ask other user to create offer-sdp + defaultSocket.send({ + participant: true, + targetUser: userid + }); + }); + }; + + function acceptRequest(response) { + if (!rtcMultiSession.requestsFrom) rtcMultiSession.requestsFrom = {}; + if (rtcMultiSession.requestsFrom[response.userid]) return; + + var obj = { + userid: response.userid, + extra: response.extra, + channel: response.channel || response.userid, + session: response.session || connection.session + }; + + // check how participant is willing to join + if (response.offers) { + if (response.offers.audio && response.offers.video) { + log('target user has both audio/video streams.'); + } else if (response.offers.audio && !response.offers.video) { + log('target user has only audio stream.'); + } else if (!response.offers.audio && response.offers.video) { + log('target user has only video stream.'); + } else { + log('target user has no stream; it seems one-way streaming or data-only connection.'); + } + + var mandatory = connection.sdpConstraints.mandatory; + if (isNull(mandatory.OfferToReceiveAudio)) { + connection.sdpConstraints.mandatory.OfferToReceiveAudio = !!response.offers.audio; + } + if (isNull(mandatory.OfferToReceiveVideo)) { + connection.sdpConstraints.mandatory.OfferToReceiveVideo = !!response.offers.video; + } + + log('target user\'s SDP has?', toStr(connection.sdpConstraints.mandatory)); + } + + rtcMultiSession.requestsFrom[response.userid] = obj; + + // www.RTCMultiConnection.org/docs/onRequest/ + if (connection.onRequest && connection.isInitiator) { + connection.onRequest(obj); + } else _accept(obj); + } + + function _accept(e) { + if (rtcMultiSession.captureUserMediaOnDemand) { + rtcMultiSession.captureUserMediaOnDemand = false; + connection.captureUserMedia(function() { + _accept(e); + + invokeMediaCaptured(connection); + }); + return; + } + + log('accepting request from', e.userid); + participants[e.userid] = e.userid; + newPrivateSocket({ + isofferer: true, + userid: e.userid, + channel: e.channel, + extra: e.extra || {}, + session: e.session || connection.session + }); + } + + // www.RTCMultiConnection.org/docs/accept/ + connection.accept = function(e) { + // for backward compatibility + if (arguments.length > 1 && isString(arguments[0])) { + e = {}; + if (arguments[0]) e.userid = arguments[0]; + if (arguments[1]) e.extra = arguments[1]; + if (arguments[2]) e.channel = arguments[2]; + } + + connection.captureUserMedia(function() { + _accept(e); + }); + }; + + var isRMSDeleted = false; + this.disconnect = function() { + this.isOwnerLeaving = true; + + if (!connection.keepStreamsOpened) { + for (var streamid in connection.localStreams) { + connection.localStreams[streamid].stop(); + } + connection.localStreams = {}; + + currentUserMediaRequest = { + streams: [], + mutex: false, + queueRequests: [] + }; + } + + if (connection.isInitiator) { + defaultSocket.send({ + isDisconnectSockets: true + }); + } + + connection.refresh(); + + rtcMultiSession.defaultSocket = defaultSocket = null; + isRMSDeleted = true; + + connection.ondisconnected({ + userid: connection.userid, + extra: connection.extra, + peer: connection.peers[connection.userid], + isSocketsDisconnected: true + }); + + // if there is any peer still opened; close it. + connection.close(); + + window.removeEventListener('beforeunload', rtcMultiSession.leaveHandler); + window.removeEventListener('keyup', rtcMultiSession.leaveHandler); + + // it will not work, though :) + delete this; + + log('Disconnected your sockets, peers, streams and everything except RTCMultiConnection object.'); + }; + } + + var webAudioMediaStreamSources = []; + + function convertToAudioStream(mediaStream) { + if (!mediaStream) throw 'MediaStream is mandatory.'; + + if (mediaStream.getVideoTracks && !mediaStream.getVideoTracks().length) { + return mediaStream; + } + + var context = new AudioContext(); + var mediaStreamSource = context.createMediaStreamSource(mediaStream); + + var destination = context.createMediaStreamDestination(); + mediaStreamSource.connect(destination); + + webAudioMediaStreamSources.push(mediaStreamSource); + + return destination.stream; + } + + 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; + + var isPluginRTC = isSafari || isIE; + + var isMobileDevice = !!navigator.userAgent.match(/Android|iPhone|iPad|iPod|BlackBerry|IEMobile/i); + + // detect node-webkit + var isNodeWebkit = !!(window.process && (typeof window.process == 'object') && window.process.versions && window.process.versions['node-webkit']); + + window.MediaStream = window.MediaStream || window.webkitMediaStream; + window.AudioContext = window.AudioContext || window.webkitAudioContext; + + function getRandomString() { + // suggested by @rvulpescu from #154 + if (window.crypto && crypto.getRandomValues && navigator.userAgent.indexOf('Safari') == -1) { + var a = window.crypto.getRandomValues(new Uint32Array(3)), + token = ''; + for (var i = 0, l = a.length; i < l; i++) { + token += a[i].toString(36); + } + return token; + } else { + return (Math.random() * new Date().getTime()).toString(36).replace(/\./g, ''); + } + } + + var chromeVersion = 50; + var matchArray = navigator.userAgent.match(/Chrom(e|ium)\/([0-9]+)\./); + if (isChrome && matchArray && matchArray[2]) { + chromeVersion = parseInt(matchArray[2], 10); + } + + var firefoxVersion = 50; + matchArray = navigator.userAgent.match(/Firefox\/(.*)/); + if (isFirefox && matchArray && matchArray[1]) { + firefoxVersion = parseInt(matchArray[1], 10); + } + + function isData(session) { + return !session.audio && !session.video && !session.screen && session.data; + } + + function isNull(obj) { + return typeof obj == 'undefined'; + } + + function isString(obj) { + return typeof obj == 'string'; + } + + function isEmpty(session) { + var length = 0; + for (var s in session) { + length++; + } + return length == 0; + } + + // this method converts array-buffer into string + function ab2str(buf) { + var result = ''; + try { + result = String.fromCharCode.apply(null, new Uint16Array(buf)); + } catch (e) {} + return result; + } + + // this method converts string into array-buffer + function str2ab(str) { + if (!isString(str)) str = JSON.stringify(str); + + var buf = new ArrayBuffer(str.length * 2); // 2 bytes for each char + var bufView = new Uint16Array(buf); + for (var i = 0, strLen = str.length; i < strLen; i++) { + bufView[i] = str.charCodeAt(i); + } + return buf; + } + + function swap(arr) { + var swapped = [], + length = arr.length; + for (var i = 0; i < length; i++) + if (arr[i] && arr[i] !== true) + swapped.push(arr[i]); + return swapped; + } + + function forEach(obj, callback) { + for (var item in obj) { + callback(obj[item], item); + } + } + + var console = window.console || { + log: function() {}, + error: function() {}, + warn: function() {} + }; + + function log() { + console.log(arguments); + } + + function error() { + console.error(arguments); + } + + function warn() { + console.warn(arguments); + } + + if (isChrome || isFirefox || isSafari) { + var log = console.log.bind(console); + var error = console.error.bind(console); + var warn = console.warn.bind(console); + } + + function toStr(obj) { + return JSON.stringify(obj, function(key, value) { + if (value && value.sdp) { + log(value.sdp.type, '\t', value.sdp.sdp); + return ''; + } else return value; + }, '\t'); + } + + function getLength(obj) { + var length = 0; + for (var o in obj) + if (o) length++; + return length; + } + + // Get HTMLAudioElement/HTMLVideoElement accordingly + + function createMediaElement(stream, session) { + var mediaElement = document.createElement(stream.isAudio ? 'audio' : 'video'); + mediaElement.id = stream.streamid; + + if (isPluginRTC) { + var body = (document.body || document.documentElement); + body.insertBefore(mediaElement, body.firstChild); + + setTimeout(function() { + Plugin.attachMediaStream(mediaElement, stream) + }, 1000); + + return Plugin.attachMediaStream(mediaElement, stream); + } + + // "mozSrcObject" is always preferred over "src"!! + mediaElement[isFirefox ? 'mozSrcObject' : 'src'] = isFirefox ? stream : (window.URL || window.webkitURL).createObjectURL(stream); + + mediaElement.controls = true; + mediaElement.autoplay = !!session.remote; + mediaElement.muted = session.remote ? false : true; + + // http://goo.gl/WZ5nFl + // Firefox don't yet support onended for any stream (remote/local) + isFirefox && mediaElement.addEventListener('ended', function() { + stream.onended(); + }, false); + + mediaElement.play(); + + return mediaElement; + } + + var onStreamEndedHandlerFiredFor = {}; + + function onStreamEndedHandler(streamedObject, connection) { + if (streamedObject.mediaElement && !streamedObject.mediaElement.parentNode) return; + + if (onStreamEndedHandlerFiredFor[streamedObject.streamid]) return; + onStreamEndedHandlerFiredFor[streamedObject.streamid] = streamedObject; + connection.onstreamended(streamedObject); + } + + var onLeaveHandlerFiredFor = {}; + + function onLeaveHandler(event, connection) { + if (onLeaveHandlerFiredFor[event.userid]) return; + onLeaveHandlerFiredFor[event.userid] = event; + connection.onleave(event); + } + + function takeSnapshot(args) { + var userid = args.userid; + var connection = args.connection; + + function _takeSnapshot(video) { + var canvas = document.createElement('canvas'); + canvas.width = video.videoWidth || video.clientWidth; + canvas.height = video.videoHeight || video.clientHeight; + + var context = canvas.getContext('2d'); + context.drawImage(video, 0, 0, canvas.width, canvas.height); + + connection.snapshots[userid] = canvas.toDataURL('image/png'); + args.callback && args.callback(connection.snapshots[userid]); + } + + if (args.mediaElement) return _takeSnapshot(args.mediaElement); + + for (var stream in connection.streams) { + stream = connection.streams[stream]; + if (stream.userid == userid && stream.stream && stream.stream.getVideoTracks && stream.stream.getVideoTracks().length) { + _takeSnapshot(stream.mediaElement); + continue; + } + } + } + + function invokeMediaCaptured(connection) { + // to let user know that media resource has been captured + // now, he can share "sessionDescription" using sockets + if (connection.onMediaCaptured) { + connection.onMediaCaptured(); + delete connection.onMediaCaptured; + } + } + + function merge(mergein, mergeto) { + if (!mergein) mergein = {}; + if (!mergeto) return mergein; + + for (var item in mergeto) { + mergein[item] = mergeto[item]; + } + return mergein; + } + + function loadScript(src, onload) { + var script = document.createElement('script'); + script.src = src; + script.onload = function() { + log('loaded resource:', src); + if (onload) onload(); + }; + document.documentElement.appendChild(script); + } + + function capturePartOfScreen(args) { + var connection = args.connection; + var element = args.element; + + if (!window.html2canvas) { + return loadScript(connection.resources.html2canvas, function() { + capturePartOfScreen(args); + }); + } + + if (isString(element)) { + element = document.querySelector(element); + if (!element) element = document.getElementById(element); + } + if (!element) throw 'HTML DOM Element is not accessible!'; + + // todo: store DOM element somewhere to minimize DOM querying issues + + // html2canvas.js is used to take screenshots + html2canvas(element, { + onrendered: function(canvas) { + args.callback(canvas.toDataURL()); + } + }); + } + + function initFileBufferReader(connection, callback) { + if (!window.FileBufferReader) { + loadScript(connection.resources.FileBufferReader, function() { + initFileBufferReader(connection, callback); + }); + return; + } + + function _private(chunk) { + chunk.userid = chunk.extra.userid; + return chunk; + } + + var fileBufferReader = new FileBufferReader(); + fileBufferReader.onProgress = function(chunk) { + connection.onFileProgress(_private(chunk), chunk.uuid); + }; + + fileBufferReader.onBegin = function(file) { + connection.onFileStart(_private(file)); + }; + + fileBufferReader.onEnd = function(file) { + connection.onFileEnd(_private(file)); + }; + + callback(fileBufferReader); + } + + var screenFrame, loadedScreenFrame; + + function loadScreenFrame(skip) { + if (DetectRTC.screen.extensionid != ReservedExtensionID) { + return; + } + + if (loadedScreenFrame) return; + if (!skip) return loadScreenFrame(true); + + loadedScreenFrame = true; + + var iframe = document.createElement('iframe'); + iframe.onload = function() { + iframe.isLoaded = true; + log('Screen Capturing frame is loaded.'); + }; + iframe.src = 'https://www.webrtc-experiment.com/getSourceId/'; + iframe.style.display = 'none'; + (document.body || document.documentElement).appendChild(iframe); + + screenFrame = { + postMessage: function() { + if (!iframe.isLoaded) { + setTimeout(screenFrame.postMessage, 100); + return; + } + iframe.contentWindow.postMessage({ + captureSourceId: true + }, '*'); + } + }; + } + + function muteOrUnmute(e) { + var stream = e.stream, + root = e.root, + session = e.session || {}, + enabled = e.enabled; + + if (!session.audio && !session.video) { + if (!isString(session)) { + session = merge(session, { + audio: true, + video: true + }); + } else { + session = { + audio: true, + video: true + }; + } + } + + // implementation from #68 + if (session.type) { + if (session.type == 'remote' && root.type != 'remote') return; + if (session.type == 'local' && root.type != 'local') return; + } + + log(enabled ? 'Muting' : 'UnMuting', 'session', toStr(session)); + + // enable/disable audio/video tracks + + if (root.type == 'local' && session.audio && !!stream.getAudioTracks) { + var audioTracks = stream.getAudioTracks()[0]; + if (audioTracks) + audioTracks.enabled = !enabled; + } + + if (root.type == 'local' && (session.video || session.screen) && !!stream.getVideoTracks) { + var videoTracks = stream.getVideoTracks()[0]; + if (videoTracks) + videoTracks.enabled = !enabled; + } + + root.sockets.forEach(function(socket) { + if (root.type == 'local') { + socket.send({ + streamid: root.streamid, + mute: !!enabled, + unmute: !enabled, + session: session + }); + } + + if (root.type == 'remote') { + socket.send({ + promptMuteUnmute: true, + streamid: root.streamid, + mute: !!enabled, + unmute: !enabled, + session: session + }); + } + }); + + if (root.type == 'remote') return; + + // According to issue #135, onmute/onumute must be fired for self + // "fakeObject" is used because we need to keep session for renegotiated streams; + // and MUST pass exact session over onStreamEndedHandler/onmute/onhold/etc. events. + var fakeObject = merge({}, root); + fakeObject.session = session; + + fakeObject.isAudio = !!fakeObject.session.audio && !fakeObject.session.video; + fakeObject.isVideo = !!fakeObject.session.video; + fakeObject.isScreen = !!fakeObject.session.screen; + + if (!!enabled) { + // if muted stream is negotiated + stream.preMuted = { + audio: stream.getAudioTracks().length && !stream.getAudioTracks()[0].enabled, + video: stream.getVideoTracks().length && !stream.getVideoTracks()[0].enabled + }; + root.rtcMultiConnection.onmute(fakeObject); + } + + if (!enabled) { + stream.preMuted = {}; + root.rtcMultiConnection.onunmute(fakeObject); + } + } + + var Firefox_Screen_Capturing_Warning = 'Make sure that you are using Firefox Nightly and you enabled: media.getusermedia.screensharing.enabled flag from about:config page. You also need to add your domain in "media.getusermedia.screensharing.allowed_domains" flag. If you are using WinXP then also enable "media.getusermedia.screensharing.allow_on_old_platforms" flag. NEVER forget to use "only" HTTPs for screen capturing!'; + var SCREEN_COMMON_FAILURE = 'HTTPs i.e. SSL-based URI is mandatory to use screen capturing.'; + var ReservedExtensionID = 'ajhifddimkapgcifgcodmmfdlknahffk'; + + // if application-developer deployed his own extension on Google App Store + var useCustomChromeExtensionForScreenCapturing = document.domain.indexOf('webrtc-experiment.com') != -1; + + function initHark(args) { + if (!window.hark) { + loadScript(args.connection.resources.hark, function() { + initHark(args); + }); + return; + } + + var connection = args.connection; + var streamedObject = args.streamedObject; + var stream = args.stream; + + var options = {}; + var speechEvents = hark(stream, options); + + speechEvents.on('speaking', function() { + if (connection.onspeaking) { + connection.onspeaking(streamedObject); + } + }); + + speechEvents.on('stopped_speaking', function() { + if (connection.onsilence) { + connection.onsilence(streamedObject); + } + }); + + speechEvents.on('volume_change', function(volume, threshold) { + if (connection.onvolumechange) { + connection.onvolumechange(merge({ + volume: volume, + threshold: threshold + }, streamedObject)); + } + }); + } + + attachEventListener = function(video, type, listener, useCapture) { + video.addEventListener(type, listener, useCapture); + }; + + var Plugin = window.PluginRTC || {}; + window.onPluginRTCInitialized = function(pluginRTCObject) { + Plugin = pluginRTCObject; + MediaStreamTrack = Plugin.MediaStreamTrack; + RTCPeerConnection = Plugin.RTCPeerConnection; + RTCIceCandidate = Plugin.RTCIceCandidate; + RTCSessionDescription = Plugin.RTCSessionDescription; + + log(isPluginRTC ? 'Java-Applet' : 'ActiveX', 'plugin has been loaded.'); + }; + if (!isEmpty(Plugin)) window.onPluginRTCInitialized(Plugin); + + // if IE or Safari + if (isPluginRTC) { + loadScript('https://cdn.webrtc-experiment.com/Plugin.EveryWhere.js'); + // loadScript('https://cdn.webrtc-experiment.com/Plugin.Temasys.js'); + } + + var MediaStream = window.MediaStream; + + if (typeof MediaStream === 'undefined' && typeof webkitMediaStream !== 'undefined') { + MediaStream = webkitMediaStream; + } + + /*global MediaStream:true */ + if (typeof MediaStream !== 'undefined' && !('stop' in MediaStream.prototype)) { + MediaStream.prototype.stop = function() { + this.getAudioTracks().forEach(function(track) { + track.stop(); + }); + + this.getVideoTracks().forEach(function(track) { + track.stop(); + }); + }; + } + + var defaultConstraints = { + mandatory: {}, + optional: [] + }; + + /* by @FreCap pull request #41 */ + var currentUserMediaRequest = { + streams: [], + mutex: false, + queueRequests: [] + }; + + function getUserMedia(options) { + if (isPluginRTC) { + if (!Plugin.getUserMedia) { + setTimeout(function() { + getUserMedia(options); + }, 1000); + return; + } + + return Plugin.getUserMedia(options.constraints || { + audio: true, + video: true + }, options.onsuccess, options.onerror); + } + + if (currentUserMediaRequest.mutex === true) { + currentUserMediaRequest.queueRequests.push(options); + return; + } + currentUserMediaRequest.mutex = true; + + var connection = options.connection; + + // tools.ietf.org/html/draft-alvestrand-constraints-resolution-00 + var mediaConstraints = options.mediaConstraints || {}; + var videoConstraints = typeof mediaConstraints.video == 'boolean' ? mediaConstraints.video : mediaConstraints.video || mediaConstraints; + var audioConstraints = typeof mediaConstraints.audio == 'boolean' ? mediaConstraints.audio : mediaConstraints.audio || defaultConstraints; + + var n = navigator; + var hints = options.constraints || { + audio: defaultConstraints, + video: defaultConstraints + }; + + if (hints.video && hints.video.mozMediaSource) { + // "mozMediaSource" is redundant + // need to check "mediaSource" instead. + videoConstraints = {}; + } + + if (hints.video == true) hints.video = defaultConstraints; + if (hints.audio == true) hints.audio = defaultConstraints; + + // connection.mediaConstraints.audio = false; + if (typeof audioConstraints == 'boolean' && hints.audio) { + hints.audio = audioConstraints; + } + + // connection.mediaConstraints.video = false; + if (typeof videoConstraints == 'boolean' && hints.video) { + hints.video = videoConstraints; + } + + // connection.mediaConstraints.audio.mandatory = {prop:true}; + var audioMandatoryConstraints = audioConstraints.mandatory; + if (!isEmpty(audioMandatoryConstraints)) { + hints.audio.mandatory = merge(hints.audio.mandatory, audioMandatoryConstraints); + } + + // connection.media.min(320,180); + // connection.media.max(1920,1080); + var videoMandatoryConstraints = videoConstraints.mandatory; + if (videoMandatoryConstraints) { + var mandatory = {}; + + if (videoMandatoryConstraints.minWidth) { + mandatory.minWidth = videoMandatoryConstraints.minWidth; + } + + if (videoMandatoryConstraints.minHeight) { + mandatory.minHeight = videoMandatoryConstraints.minHeight; + } + + if (videoMandatoryConstraints.maxWidth) { + mandatory.maxWidth = videoMandatoryConstraints.maxWidth; + } + + if (videoMandatoryConstraints.maxHeight) { + mandatory.maxHeight = videoMandatoryConstraints.maxHeight; + } + + if (videoMandatoryConstraints.minAspectRatio) { + mandatory.minAspectRatio = videoMandatoryConstraints.minAspectRatio; + } + + if (videoMandatoryConstraints.maxFrameRate) { + mandatory.maxFrameRate = videoMandatoryConstraints.maxFrameRate; + } + + if (videoMandatoryConstraints.minFrameRate) { + mandatory.minFrameRate = videoMandatoryConstraints.minFrameRate; + } + + if (mandatory.minWidth && mandatory.minHeight) { + // http://goo.gl/IZVYsj + var allowed = ['1920:1080', '1280:720', '960:720', '640:360', '640:480', '320:240', '320:180']; + + if (allowed.indexOf(mandatory.minWidth + ':' + mandatory.minHeight) == -1 || + allowed.indexOf(mandatory.maxWidth + ':' + mandatory.maxHeight) == -1) { + error('The min/max width/height constraints you passed "seems" NOT supported.', toStr(mandatory)); + } + + if (mandatory.minWidth > mandatory.maxWidth || mandatory.minHeight > mandatory.maxHeight) { + error('Minimum value must not exceed maximum value.', toStr(mandatory)); + } + + if (mandatory.minWidth >= 1280 && mandatory.minHeight >= 720) { + warn('Enjoy HD video! min/' + mandatory.minWidth + ':' + mandatory.minHeight + ', max/' + mandatory.maxWidth + ':' + mandatory.maxHeight); + } + } + + hints.video.mandatory = merge(hints.video.mandatory, mandatory); + } + + if (videoMandatoryConstraints) { + hints.video.mandatory = merge(hints.video.mandatory, videoMandatoryConstraints); + } + + // videoConstraints.optional = [{prop:true}]; + if (videoConstraints.optional && videoConstraints.optional instanceof Array && videoConstraints.optional.length) { + hints.video.optional = hints.video.optional ? hints.video.optional.concat(videoConstraints.optional) : videoConstraints.optional; + } + + // audioConstraints.optional = [{prop:true}]; + if (audioConstraints.optional && audioConstraints.optional instanceof Array && audioConstraints.optional.length) { + hints.audio.optional = hints.audio.optional ? hints.audio.optional.concat(audioConstraints.optional) : audioConstraints.optional; + } + + if (hints.video.mandatory && !isEmpty(hints.video.mandatory) && connection._mediaSources.video) { + hints.video.optional.forEach(function(video, index) { + if (video.sourceId == connection._mediaSources.video) { + delete hints.video.optional[index]; + } + }); + + hints.video.optional = swap(hints.video.optional); + + hints.video.optional.push({ + sourceId: connection._mediaSources.video + }); + } + + if (hints.audio.mandatory && !isEmpty(hints.audio.mandatory) && connection._mediaSources.audio) { + hints.audio.optional.forEach(function(audio, index) { + if (audio.sourceId == connection._mediaSources.audio) { + delete hints.audio.optional[index]; + } + }); + + hints.audio.optional = swap(hints.audio.optional); + + hints.audio.optional.push({ + sourceId: connection._mediaSources.audio + }); + } + + if (hints.video && !hints.video.mozMediaSource && hints.video.optional && hints.video.mandatory) { + if (!hints.video.optional.length && isEmpty(hints.video.mandatory)) { + hints.video = true; + } + } + + if (isMobileDevice) { + // Android fails for some constraints + // so need to force {audio:true,video:true} + hints = { + audio: !!hints.audio, + video: !!hints.video + }; + } + + // connection.mediaConstraints always overrides constraints + // passed from "captureUserMedia" function. + // todo: need to verify all possible situations + log('invoked getUserMedia with constraints:', toStr(hints)); + + // easy way to match + var idInstance = JSON.stringify(hints); + + function streaming(stream, returnBack, streamid) { + if (!streamid) streamid = getRandomString(); + + // localStreams object will store stream + // until it is removed using native-stop method. + connection.localStreams[streamid] = stream; + + var video = options.video; + if (video) { + video[isFirefox ? 'mozSrcObject' : 'src'] = isFirefox ? stream : (window.URL || window.webkitURL).createObjectURL(stream); + video.play(); + } + + options.onsuccess(stream, returnBack, idInstance, streamid); + currentUserMediaRequest.streams[idInstance] = { + stream: stream, + streamid: streamid + }; + currentUserMediaRequest.mutex = false; + if (currentUserMediaRequest.queueRequests.length) + getUserMedia(currentUserMediaRequest.queueRequests.shift()); + } + + if (currentUserMediaRequest.streams[idInstance]) { + streaming(currentUserMediaRequest.streams[idInstance].stream, true, currentUserMediaRequest.streams[idInstance].streamid); + } else { + n.getMedia = n.webkitGetUserMedia || n.mozGetUserMedia; + + // http://goo.gl/eETIK4 + n.getMedia(hints, streaming, function(error) { + options.onerror(error, hints); + }); + } + } + + // IceServersHandler.js + + var IceServersHandler = (function() { + function getIceServers(connection) { + var iceServers = []; + + iceServers.push(getSTUNObj('stun:stun.l.google.com:19302')); + + iceServers.push(getTURNObj('stun:webrtcweb.com:7788', 'muazkh', 'muazkh')); // coTURN + iceServers.push(getTURNObj('turn:webrtcweb.com:7788', 'muazkh', 'muazkh')); // coTURN + iceServers.push(getTURNObj('turn:webrtcweb.com:8877', 'muazkh', 'muazkh')); // coTURN + + iceServers.push(getTURNObj('turns:webrtcweb.com:7788', 'muazkh', 'muazkh')); // coTURN + iceServers.push(getTURNObj('turns:webrtcweb.com:8877', 'muazkh', 'muazkh')); // coTURN + + // iceServers.push(getTURNObj('turn:webrtcweb.com:3344', 'muazkh', 'muazkh')); // resiprocate + // iceServers.push(getTURNObj('turn:webrtcweb.com:4433', 'muazkh', 'muazkh')); // resiprocate + + // check if restund is still active: http://webrtcweb.com:4050/ + iceServers.push(getTURNObj('stun:webrtcweb.com:4455', 'muazkh', 'muazkh')); // restund + iceServers.push(getTURNObj('turn:webrtcweb.com:4455', 'muazkh', 'muazkh')); // restund + iceServers.push(getTURNObj('turn:webrtcweb.com:5544?transport=tcp', 'muazkh', 'muazkh')); // restund + + return iceServers; + } + + function getSTUNObj(stunStr) { + var urlsParam = 'urls'; + if (typeof isPluginRTC !== 'undefined') { + urlsParam = 'url'; + } + + var obj = {}; + obj[urlsParam] = stunStr; + return obj; + } + + function getTURNObj(turnStr, username, credential) { + var urlsParam = 'urls'; + if (typeof isPluginRTC !== 'undefined') { + urlsParam = 'url'; + } + + var obj = { + username: username, + credential: credential + }; + obj[urlsParam] = turnStr; + return obj; + } + + return { + getIceServers: getIceServers + }; + })(); + + var RTCSessionDescription = window.RTCSessionDescription || window.mozRTCSessionDescription; + var RTCIceCandidate = window.RTCIceCandidate || window.mozRTCIceCandidate; + + var RTCPeerConnection; + if (typeof mozRTCPeerConnection !== 'undefined') { + RTCPeerConnection = mozRTCPeerConnection; + } else if (typeof webkitRTCPeerConnection !== 'undefined') { + RTCPeerConnection = webkitRTCPeerConnection; + } else if (typeof window.RTCPeerConnection !== 'undefined') { + RTCPeerConnection = window.RTCPeerConnection; + } else { + console.error('WebRTC 1.0 (RTCPeerConnection) API seems NOT available in this browser.'); + } + + function setSdpConstraints(config) { + var sdpConstraints; + + var sdpConstraints_mandatory = { + OfferToReceiveAudio: !!config.OfferToReceiveAudio, + OfferToReceiveVideo: !!config.OfferToReceiveVideo + }; + + sdpConstraints = { + mandatory: sdpConstraints_mandatory, + optional: [{ + VoiceActivityDetection: false + }] + }; + + if (!!navigator.mozGetUserMedia && firefoxVersion > 34) { + sdpConstraints = { + OfferToReceiveAudio: !!config.OfferToReceiveAudio, + OfferToReceiveVideo: !!config.OfferToReceiveVideo + }; + } + + return sdpConstraints; + } + + function PeerConnection() { + return { + create: function(type, options) { + merge(this, options); + + var self = this; + + this.type = type; + this.init(); + this.attachMediaStreams(); + + if (isFirefox && this.session.data) { + if (this.session.data && type == 'offer') { + this.createDataChannel(); + } + + this.getLocalDescription(type); + + if (this.session.data && type == 'answer') { + this.createDataChannel(); + } + } else self.getLocalDescription(type); + + return this; + }, + getLocalDescription: function(createType) { + log('(getLocalDescription) peer createType is', createType); + + if (this.session.inactive && isNull(this.rtcMultiConnection.waitUntilRemoteStreamStartsFlowing)) { + // inactive session returns blank-stream + this.rtcMultiConnection.waitUntilRemoteStreamStartsFlowing = false; + } + + var self = this; + + if (createType == 'answer') { + this.setRemoteDescription(this.offerDescription, createDescription); + } else createDescription(); + + function createDescription() { + self.connection[createType == 'offer' ? 'createOffer' : 'createAnswer'](function(sessionDescription) { + sessionDescription.sdp = connection.processSdp(sessionDescription.sdp); + self.connection.setLocalDescription(sessionDescription); + + if (self.trickleIce) { + self.onSessionDescription(sessionDescription, self.streaminfo); + } + + if (sessionDescription.type == 'offer') { + log('offer sdp', sessionDescription.sdp); + } + + self.prevCreateType = createType; + }, self.onSdpError, self.constraints); + } + }, + init: function() { + this.setConstraints(); + this.connection = new RTCPeerConnection(this.iceServers, this.optionalArgument); + + if (this.session.data) { + log('invoked: createDataChannel'); + this.createDataChannel(); + } + + this.connection.onicecandidate = function(event) { + if (!event.candidate) { + if (!self.trickleIce) { + returnSDP(); + } + + return; + } + + if (!self.trickleIce) return; + + self.onicecandidate(event.candidate); + }; + + function returnSDP() { + if (self.returnedSDP) { + self.returnedSDP = false; + return; + }; + self.returnedSDP = true; + + self.onSessionDescription(self.connection.localDescription, self.streaminfo); + } + + this.connection.onaddstream = function(e) { + log('onaddstream', isPluginRTC ? e.stream : toStr(e.stream)); + + self.onaddstream(e.stream, self.session); + }; + + this.connection.onremovestream = function(e) { + self.onremovestream(e.stream); + }; + + this.connection.onsignalingstatechange = function() { + self.connection && self.oniceconnectionstatechange({ + iceConnectionState: self.connection.iceConnectionState, + iceGatheringState: self.connection.iceGatheringState, + signalingState: self.connection.signalingState + }); + }; + + this.connection.oniceconnectionstatechange = function() { + if (!self.connection) return; + + self.oniceconnectionstatechange({ + iceConnectionState: self.connection.iceConnectionState, + iceGatheringState: self.connection.iceGatheringState, + signalingState: self.connection.signalingState + }); + + if (self.trickleIce) return; + + if (self.connection.iceGatheringState == 'complete') { + log('iceGatheringState', self.connection.iceGatheringState); + returnSDP(); + } + }; + + var self = this; + }, + setConstraints: function() { + var sdpConstraints = setSdpConstraints({ + OfferToReceiveAudio: !!this.session.audio, + OfferToReceiveVideo: !!this.session.video || !!this.session.screen + }); + + if (this.sdpConstraints.mandatory) { + sdpConstraints = setSdpConstraints(this.sdpConstraints.mandatory); + } + + this.constraints = sdpConstraints; + + if (this.constraints) { + log('sdp-constraints', toStr(this.constraints)); + } + + this.optionalArgument = { + optional: this.optionalArgument.optional || [], + mandatory: this.optionalArgument.mandatory || {} + }; + + if (!this.preferSCTP) { + this.optionalArgument.optional.push({ + RtpDataChannels: true + }); + } + + log('optional-argument', toStr(this.optionalArgument)); + + if (!isNull(this.iceServers)) { + var iceCandidates = this.rtcMultiConnection.candidates; + + var stun = iceCandidates.stun; + var turn = iceCandidates.turn; + var host = iceCandidates.host; + + if (!isNull(iceCandidates.reflexive)) stun = iceCandidates.reflexive; + if (!isNull(iceCandidates.relay)) turn = iceCandidates.relay; + + if (!host && !stun && turn) { + this.rtcConfiguration.iceTransports = 'relay'; + } else if (!host && !stun && !turn) { + this.rtcConfiguration.iceTransports = 'none'; + } + + this.iceServers = { + iceServers: this.iceServers, + // bundlePolicy: 'max-bundle', // or balanced or max-compat + // rtcpMuxPolicy: 'negotiate', // or require + iceTransportPolicy: 'all' // or relay or none + }; + } else this.iceServers = null; + + log('rtc-configuration', toStr(this.iceServers)); + }, + onSdpError: function(e) { + var message = toStr(e); + + if (message && message.indexOf('RTP/SAVPF Expects at least 4 fields') != -1) { + message = 'It seems that you are trying to interop RTP-datachannels with SCTP. It is not supported!'; + } + error('onSdpError:', message); + }, + onSdpSuccess: function() { + log('sdp success'); + }, + onMediaError: function(err) { + error(toStr(err)); + }, + setRemoteDescription: function(sessionDescription, onSdpSuccess) { + if (!sessionDescription) throw 'Remote session description should NOT be NULL.'; + + if (!this.connection) return; + + sessionDescription.sdp = connection.processSdp(sessionDescription.sdp); + + log('setting remote description', sessionDescription.type, sessionDescription.sdp); + + var self = this; + this.connection.setRemoteDescription( + new RTCSessionDescription(sessionDescription), + onSdpSuccess || this.onSdpSuccess, + function(error) { + if (error.search(/STATE_SENTINITIATE|STATE_INPROGRESS/gi) == -1) { + self.onSdpError(error); + } + } + ); + }, + addIceCandidate: function(candidate) { + var self = this; + if (isPluginRTC) { + RTCIceCandidate(candidate, function(iceCandidate) { + onAddIceCandidate(iceCandidate); + }); + } else onAddIceCandidate(new RTCIceCandidate(candidate)); + + function onAddIceCandidate(iceCandidate) { + self.connection.addIceCandidate(iceCandidate, function() { + log('added:', candidate.sdpMid, candidate.candidate); + }, function() { + error('onIceFailure', arguments, candidate.candidate); + }); + } + }, + createDataChannel: function(channelIdentifier) { + // skip 2nd invocation of createDataChannel + if (this.channels && this.channels.length) return; + + var self = this; + + if (!this.channels) this.channels = []; + + // protocol: 'text/chat', preset: true, stream: 16 + // maxRetransmits:0 && ordered:false && outOfOrderAllowed: false + var dataChannelDict = {}; + + if (this.dataChannelDict) dataChannelDict = this.dataChannelDict; + + if (isChrome && !this.preferSCTP) { + dataChannelDict.reliable = false; // Deprecated! + } + + log('dataChannelDict', toStr(dataChannelDict)); + + if (this.type == 'answer' || isFirefox) { + this.connection.ondatachannel = function(event) { + self.setChannelEvents(event.channel); + }; + } + + if ((isChrome && this.type == 'offer') || isFirefox) { + this.setChannelEvents( + this.connection.createDataChannel(channelIdentifier || 'channel', dataChannelDict) + ); + } + }, + setChannelEvents: function(channel) { + var self = this; + + channel.binaryType = 'arraybuffer'; + + if (this.dataChannelDict.binaryType) { + channel.binaryType = this.dataChannelDict.binaryType; + } + + channel.onmessage = function(event) { + self.onmessage(event.data); + }; + + var numberOfTimes = 0; + channel.onopen = function() { + channel.push = channel.send; + channel.send = function(data) { + if (self.connection.iceConnectionState == 'disconnected') { + return; + } + + if (channel.readyState.search(/closing|closed/g) != -1) { + return; + } + + if (channel.readyState.search(/connecting|open/g) == -1) { + return; + } + + if (channel.readyState == 'connecting') { + numberOfTimes++; + return setTimeout(function() { + if (numberOfTimes < 20) { + channel.send(data); + } else throw 'Number of times exceeded to wait for WebRTC data connection to be opened.'; + }, 1000); + } + try { + channel.push(data); + } catch (e) { + numberOfTimes++; + warn('Data transmission failed. Re-transmitting..', numberOfTimes, toStr(e)); + if (numberOfTimes >= 20) throw 'Number of times exceeded to resend data packets over WebRTC data channels.'; + setTimeout(function() { + channel.send(data); + }, 100); + } + }; + self.onopen(channel); + }; + + channel.onerror = function(event) { + self.onerror(event); + }; + + channel.onclose = function(event) { + self.onclose(event); + }; + + this.channels.push(channel); + }, + addStream: function(stream) { + if (!stream.streamid && !isIE) { + stream.streamid = getRandomString(); + } + + // todo: maybe need to add isAudio/isVideo/isScreen if missing? + + log('attaching stream:', stream.streamid, isPluginRTC ? stream : toStr(stream)); + + this.connection.addStream(stream); + + this.sendStreamId(stream); + this.getStreamInfo(); + }, + attachMediaStreams: function() { + var streams = this.attachStreams; + for (var i = 0; i < streams.length; i++) { + this.addStream(streams[i]); + } + }, + getStreamInfo: function() { + this.streaminfo = ''; + var streams = this.connection.getLocalStreams(); + for (var i = 0; i < streams.length; i++) { + if (i == 0) { + this.streaminfo = JSON.stringify({ + streamid: streams[i].streamid || '', + isScreen: !!streams[i].isScreen, + isAudio: !!streams[i].isAudio, + isVideo: !!streams[i].isVideo, + preMuted: streams[i].preMuted || {} + }); + } else { + this.streaminfo += '----' + JSON.stringify({ + streamid: streams[i].streamid || '', + isScreen: !!streams[i].isScreen, + isAudio: !!streams[i].isAudio, + isVideo: !!streams[i].isVideo, + preMuted: streams[i].preMuted || {} + }); + } + } + }, + recreateOffer: function(renegotiate, callback) { + log('recreating offer'); + + this.type = 'offer'; + this.session = renegotiate; + + // todo: make sure this doesn't affect renegotiation scenarios + // this.setConstraints(); + + this.onSessionDescription = callback; + this.getStreamInfo(); + + // one can renegotiate data connection in existing audio/video/screen connection! + if (this.session.data) { + this.createDataChannel(); + } + + this.getLocalDescription('offer'); + }, + recreateAnswer: function(sdp, session, callback) { + // if(isFirefox) this.create(this.type, this); + + log('recreating answer'); + + this.type = 'answer'; + this.session = session; + + // todo: make sure this doesn't affect renegotiation scenarios + // this.setConstraints(); + + this.onSessionDescription = callback; + this.offerDescription = sdp; + this.getStreamInfo(); + + // one can renegotiate data connection in existing audio/video/screen connection! + if (this.session.data) { + this.createDataChannel(); + } + + this.getLocalDescription('answer'); + } + }; + } + + var FileSaver = { + SaveToDisk: invokeSaveAsDialog + }; + + + function invokeSaveAsDialog(fileUrl, fileName) { + /* + if (typeof navigator.msSaveOrOpenBlob !== 'undefined') { + return navigator.msSaveOrOpenBlob(file, fileFullName); + } else if (typeof navigator.msSaveBlob !== 'undefined') { + return navigator.msSaveBlob(file, fileFullName); + } + */ + + var hyperlink = document.createElement('a'); + hyperlink.href = fileUrl; + hyperlink.target = '_blank'; + hyperlink.download = fileName || fileUrl; + + if (!!navigator.mozGetUserMedia) { + hyperlink.onclick = function() { + (document.body || document.documentElement).removeChild(hyperlink); + }; + (document.body || document.documentElement).appendChild(hyperlink); + } + + var evt = new MouseEvent('click', { + view: window, + bubbles: true, + cancelable: true + }); + + hyperlink.dispatchEvent(evt); + + if (!navigator.mozGetUserMedia) { + URL.revokeObjectURL(hyperlink.href); + } + } + + var TextSender = { + send: function(config) { + var connection = config.connection; + + if (config.text instanceof ArrayBuffer || config.text instanceof DataView) { + return config.channel.send(config.text, config._channel); + } + + var channel = config.channel, + _channel = config._channel, + initialText = config.text, + packetSize = connection.chunkSize || 1000, + textToTransfer = '', + isobject = false; + + if (!isString(initialText)) { + isobject = true; + initialText = JSON.stringify(initialText); + } + + // uuid is used to uniquely identify sending instance + var uuid = getRandomString(); + var sendingTime = new Date().getTime(); + + sendText(initialText); + + function sendText(textMessage, text) { + var data = { + type: 'text', + uuid: uuid, + sendingTime: sendingTime + }; + + if (textMessage) { + text = textMessage; + data.packets = parseInt(text.length / packetSize); + } + + if (text.length > packetSize) + data.message = text.slice(0, packetSize); + else { + data.message = text; + data.last = true; + data.isobject = isobject; + } + + channel.send(data, _channel); + + textToTransfer = text.slice(data.message.length); + + if (textToTransfer.length) { + setTimeout(function() { + sendText(null, textToTransfer); + }, connection.chunkInterval || 100); + } + } + } + }; + + function TextReceiver(connection) { + var content = {}; + + function receive(data, userid, extra) { + // uuid is used to uniquely identify sending instance + var uuid = data.uuid; + if (!content[uuid]) content[uuid] = []; + + content[uuid].push(data.message); + if (data.last) { + var message = content[uuid].join(''); + if (data.isobject) message = JSON.parse(message); + + // latency detection + var receivingTime = new Date().getTime(); + var latency = receivingTime - data.sendingTime; + + var e = { + data: message, + userid: userid, + extra: extra, + latency: latency + }; + + if (message.preRecordedMediaChunk) { + if (!connection.preRecordedMedias[message.streamerid]) { + connection.shareMediaFile(null, null, message.streamerid); + } + connection.preRecordedMedias[message.streamerid].onData(message.chunk); + } else if (connection.autoTranslateText) { + e.original = e.data; + connection.Translator.TranslateText(e.data, function(translatedText) { + e.data = translatedText; + connection.onmessage(e); + }); + } else if (message.isPartOfScreen) { + connection.onpartofscreen(message); + } else connection.onmessage(e); + + delete content[uuid]; + } + } + + return { + receive: receive + }; + } + + // Last time updated at Sep 25, 2015, 08:32:23 + + // Latest file can be found here: https://cdn.webrtc-experiment.com/DetectRTC.js + + // Muaz Khan - www.MuazKhan.com + // MIT License - www.WebRTC-Experiment.com/licence + // Documentation - github.com/muaz-khan/DetectRTC + // ____________ + // DetectRTC.js + + // DetectRTC.hasWebcam (has webcam device!) + // DetectRTC.hasMicrophone (has microphone device!) + // DetectRTC.hasSpeakers (has speakers!) + + (function() { + + 'use strict'; + + var navigator = window.navigator; + + 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); + }; + } + + 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() {} + }; + } + + var isMobileDevice = !!navigator.userAgent.match(/Android|iPhone|iPad|iPod|BlackBerry|IEMobile/i); + var isEdge = navigator.userAgent.indexOf('Edge') !== -1 && (!!navigator.msSaveOrOpenBlob || !!navigator.msSaveBlob); + + // 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 ((verOffset = nAgt.indexOf('Opera')) !== -1) { + browserName = 'Opera'; + fullVersion = nAgt.substring(verOffset + 6); + + if ((verOffset = nAgt.indexOf('Version')) !== -1) { + fullVersion = nAgt.substring(verOffset + 8); + } + } + // In MSIE, the true version is after 'MSIE' in userAgent + else if ((verOffset = nAgt.indexOf('MSIE')) !== -1) { + browserName = 'IE'; + fullVersion = nAgt.substring(verOffset + 5); + } + // In Chrome, the true version is after 'Chrome' + else if ((verOffset = nAgt.indexOf('Chrome')) !== -1) { + browserName = 'Chrome'; + fullVersion = nAgt.substring(verOffset + 7); + } + // In Safari, the true version is after 'Safari' or after 'Version' + else if ((verOffset = nAgt.indexOf('Safari')) !== -1) { + 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 ((verOffset = nAgt.indexOf('Firefox')) !== -1) { + 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); + } + + // 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 + }; + } + + 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; + } + }; + + var osName = 'Unknown OS'; + + if (isMobile.any()) { + osName = isMobile.getOsName(); + } else { + if (navigator.appVersion.indexOf('Win') !== -1) { + osName = 'Windows'; + } + + if (navigator.appVersion.indexOf('Mac') !== -1) { + osName = 'MacOS'; + } + + if (navigator.appVersion.indexOf('X11') !== -1) { + osName = 'UNIX'; + } + + if (navigator.appVersion.indexOf('Linux') !== -1) { + osName = 'Linux'; + } + } + + + var isCanvasSupportsStreamCapturing = false; + var isVideoSupportsStreamCapturing = false; + ['captureStream', 'mozCaptureStream', 'webkitCaptureStream'].forEach(function(item) { + // asdf + if (item in document.createElement('canvas')) { + isCanvasSupportsStreamCapturing = true; + } + + if (item in document.createElement('video')) { + isVideoSupportsStreamCapturing = true; + } + }); + + // via: https://github.com/diafygi/webrtc-ips + function DetectLocalIPAddress(callback) { + 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; + } + + //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 ipAddress = ipRegex.exec(candidate)[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 = []; + + // ---------- 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 = canEnumerate; + var hasSpeakers = canEnumerate; + var hasWebcam = canEnumerate; + + // http://dev.w3.org/2011/webrtc/editor/getusermedia.html#mediadevices + // todo: switch to enumerateDevices when landed in canary. + function checkDeviceSupport(callback) { + // 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 = []; + navigator.enumerateDevices(function(devices) { + devices.forEach(function(_device) { + var device = {}; + for (var d in _device) { + device[d] = _device[d]; + } + + var skip; + MediaDevices.forEach(function(d) { + if (d.id === device.id) { + skip = true; + } + }); + + if (skip) { + return; + } + + // if it is MediaStreamTrack.getSources + if (device.kind === 'audio') { + device.kind = 'audioinput'; + } + + if (device.kind === 'video') { + device.kind = 'videoinput'; + } + + if (!device.deviceId) { + device.deviceId = device.id; + } + + if (!device.id) { + device.id = device.deviceId; + } + + if (!device.label) { + device.label = 'Please invoke getUserMedia once.'; + if (!isHTTPs) { + device.label = 'HTTPs is required to get label of this ' + device.kind + ' device.'; + } + } + + if (device.kind === 'audioinput' || device.kind === 'audio') { + hasMicrophone = true; + } + + if (device.kind === 'audiooutput') { + hasSpeakers = true; + } + + if (device.kind === 'videoinput' || device.kind === 'video') { + hasWebcam = true; + } + + // there is no 'videoouput' in the spec. + + MediaDevices.push(device); + }); + + if (typeof DetectRTC !== 'undefined') { + DetectRTC.MediaDevices = MediaDevices; + DetectRTC.hasMicrophone = hasMicrophone; + DetectRTC.hasSpeakers = hasSpeakers; + DetectRTC.hasWebcam = hasWebcam; + } + + if (callback) { + callback(); + } + }); + } + + // check for microphone/camera support! + checkDeviceSupport(); + + var DetectRTC = {}; + + // ---------- + // DetectRTC.browser.name || DetectRTC.browser.version || DetectRTC.browser.fullVersion + DetectRTC.browser = getBrowserInfo(); + + // DetectRTC.isChrome || DetectRTC.isFirefox || DetectRTC.isEdge + DetectRTC.browser['is' + DetectRTC.browser.name] = true; + + var isHTTPs = location.protocol === 'https:'; + 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; + ['webkitRTCPeerConnection', 'mozRTCPeerConnection', 'RTCIceGatherer'].forEach(function(item) { + 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 (!isHTTPs) { + isScreenCapturingSupported = false; + } + DetectRTC.isScreenCapturingSupported = isScreenCapturingSupported; + + // --------- Detect if WebAudio API are supported + var webAudio = {}; + ['AudioContext', 'webkitAudioContext', 'mozAudioContext', 'msAudioContext'].forEach(function(item) { + if (webAudio.isSupported && webAudio.isCreateMediaStreamSourceSupported) { + 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" + + // ------ + + DetectRTC.isWebSocketsSupported = 'WebSocket' in window && 2 === window.WebSocket.CLOSING; + DetectRTC.isWebSocketsBlocked = 'Checking'; + + if (DetectRTC.isWebSocketsSupported) { + var websocket = new WebSocket('wss://echo.websocket.org:443/'); + websocket.onopen = function() { + DetectRTC.isWebSocketsBlocked = false; + + if (DetectRTC.loadCallback) { + DetectRTC.loadCallback(); + } + }; + websocket.onerror = function() { + DetectRTC.isWebSocketsBlocked = true; + + if (DetectRTC.loadCallback) { + DetectRTC.loadCallback(); + } + }; + } + + // ------ + var isGetUserMediaSupported = false; + if (navigator.getUserMedia) { + isGetUserMediaSupported = true; + } else if (navigator.mediaDevices && navigator.mediaDevices.getUserMedia) { + isGetUserMediaSupported = true; + } + if (DetectRTC.browser.isChrome && DetectRTC.browser.version >= 47 && !isHTTPs) { + DetectRTC.isGetUserMediaSupported = 'Requires HTTPs'; + } + DetectRTC.isGetUserMediaSupported = isGetUserMediaSupported; + + // ----------- + DetectRTC.osName = osName; // "osName" is defined in "detectOSName.js" + + // ---------- + DetectRTC.isCanvasSupportsStreamCapturing = isCanvasSupportsStreamCapturing; + DetectRTC.isVideoSupportsStreamCapturing = isVideoSupportsStreamCapturing; + + // ------ + DetectRTC.DetectLocalIPAddress = DetectLocalIPAddress; + + // ------- + DetectRTC.load = function(callback) { + this.loadCallback = callback; + + checkDeviceSupport(callback); + }; + + DetectRTC.MediaDevices = MediaDevices; + DetectRTC.hasMicrophone = hasMicrophone; + DetectRTC.hasSpeakers = hasSpeakers; + DetectRTC.hasWebcam = hasWebcam; + + // ------ + 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) { + /*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; + + window.DetectRTC = DetectRTC; + + })(); + + // DetectRTC extender + var screenCallback; + + DetectRTC.screen = { + chromeMediaSource: 'screen', + extensionid: ReservedExtensionID, + getSourceId: function(callback) { + if (!callback) throw '"callback" parameter is mandatory.'; + + // make sure that chrome extension is installed. + if (!!DetectRTC.screen.status) { + onstatus(DetectRTC.screen.status); + } else DetectRTC.screen.getChromeExtensionStatus(onstatus); + + function onstatus(status) { + if (status == 'installed-enabled') { + screenCallback = callback; + window.postMessage('get-sourceId', '*'); + return; + } + + DetectRTC.screen.chromeMediaSource = 'screen'; + callback('No-Response'); // chrome extension isn't available + } + }, + onMessageCallback: function(data) { + if (!(isString(data) || !!data.sourceId)) return; + + log('chrome message', data); + + // "cancel" button is clicked + if (data == 'PermissionDeniedError') { + DetectRTC.screen.chromeMediaSource = 'PermissionDeniedError'; + if (screenCallback) return screenCallback('PermissionDeniedError'); + else throw new Error('PermissionDeniedError'); + } + + // extension notified his presence + if (data == 'rtcmulticonnection-extension-loaded') { + DetectRTC.screen.chromeMediaSource = 'desktop'; + if (DetectRTC.screen.onScreenCapturingExtensionAvailable) { + DetectRTC.screen.onScreenCapturingExtensionAvailable(); + + // make sure that this event isn't fired multiple times + DetectRTC.screen.onScreenCapturingExtensionAvailable = null; + } + } + + // extension shared temp sourceId + if (data.sourceId) { + DetectRTC.screen.sourceId = data.sourceId; + if (screenCallback) screenCallback(DetectRTC.screen.sourceId); + } + }, + getChromeExtensionStatus: function(extensionid, callback) { + function _callback(status) { + DetectRTC.screen.status = status; + callback(status); + } + + if (isFirefox) return _callback('not-chrome'); + + if (arguments.length != 2) { + callback = extensionid; + extensionid = this.extensionid; + } + + var image = document.createElement('img'); + image.src = 'chrome-extension://' + extensionid + '/icon.png'; + image.onload = function() { + DetectRTC.screen.chromeMediaSource = 'screen'; + window.postMessage('are-you-there', '*'); + setTimeout(function() { + if (DetectRTC.screen.chromeMediaSource == 'screen') { + _callback( + DetectRTC.screen.chromeMediaSource == 'desktop' ? 'installed-enabled' : 'installed-disabled' /* if chrome extension isn't permitted for current domain, then it will be installed-disabled all the time even if extension is enabled. */ + ); + } else _callback('installed-enabled'); + }, 2000); + }; + image.onerror = function() { + _callback('not-installed'); + }; + } + }; + + // if IE + if (!window.addEventListener) { + window.addEventListener = function(el, eventName, eventHandler) { + if (!el.attachEvent) return; + el.attachEvent('on' + eventName, eventHandler); + }; + } + + function listenEventHandler(eventName, eventHandler) { + window.removeEventListener(eventName, eventHandler); + window.addEventListener(eventName, eventHandler, false); + } + + window.addEventListener('message', function(event) { + if (event.origin != window.location.origin) { + return; + } + + DetectRTC.screen.onMessageCallback(event.data); + }); + + function setDefaults(connection) { + // www.RTCMultiConnection.org/docs/userid/ + connection.userid = getRandomString(); + + // www.RTCMultiConnection.org/docs/session/ + connection.session = { + audio: true, + video: true + }; + + // www.RTCMultiConnection.org/docs/maxParticipantsAllowed/ + connection.maxParticipantsAllowed = 256; + + // www.RTCMultiConnection.org/docs/direction/ + // 'many-to-many' / 'one-to-many' / 'one-to-one' / 'one-way' + connection.direction = 'many-to-many'; + + // www.RTCMultiConnection.org/docs/mediaConstraints/ + connection.mediaConstraints = { + mandatory: {}, // kept for backward compatibility + optional: [], // kept for backward compatibility + audio: { + mandatory: {}, + optional: [] + }, + video: { + mandatory: {}, + optional: [] + } + }; + + // www.RTCMultiConnection.org/docs/candidates/ + connection.candidates = { + host: true, + stun: true, + turn: true + }; + + connection.sdpConstraints = {}; + + // as @serhanters proposed in #225 + // it will auto fix "all" renegotiation scenarios + connection.sdpConstraints.mandatory = { + OfferToReceiveAudio: true, + OfferToReceiveVideo: true + }; + + connection.privileges = { + canStopRemoteStream: false, // user can stop remote streams + canMuteRemoteStream: false // user can mute remote streams + }; + + connection.iceProtocols = { + tcp: true, + udp: true + }; + + // www.RTCMultiConnection.org/docs/preferSCTP/ + connection.preferSCTP = isFirefox || chromeVersion >= 32 ? true : false; + connection.chunkInterval = isFirefox || chromeVersion >= 32 ? 100 : 500; // 500ms for RTP and 100ms for SCTP + connection.chunkSize = isFirefox || chromeVersion >= 32 ? 13 * 1000 : 1000; // 1000 chars for RTP and 13000 chars for SCTP + + // www.RTCMultiConnection.org/docs/fakeDataChannels/ + connection.fakeDataChannels = false; + + connection.waitUntilRemoteStreamStartsFlowing = null; // NULL == true + + // auto leave on page unload + connection.leaveOnPageUnload = true; + + // get ICE-servers from XirSys + connection.getExternalIceServers = isChrome; + + // www.RTCMultiConnection.org/docs/UA/ + connection.UA = { + isFirefox: isFirefox, + isChrome: isChrome, + isMobileDevice: isMobileDevice, + version: isChrome ? chromeVersion : firefoxVersion, + isNodeWebkit: isNodeWebkit, + isSafari: isSafari, + isIE: isIE, + isOpera: isOpera + }; + + // file queue: to store previous file objects in memory; + // and stream over newly connected peers + // www.RTCMultiConnection.org/docs/fileQueue/ + connection.fileQueue = {}; + + // this array is aimed to store all renegotiated streams' session-types + connection.renegotiatedSessions = {}; + + // www.RTCMultiConnection.org/docs/channels/ + connection.channels = {}; + + // www.RTCMultiConnection.org/docs/extra/ + connection.extra = {}; + + // www.RTCMultiConnection.org/docs/bandwidth/ + connection.bandwidth = { + screen: 300 // 300kbps (dirty workaround) + }; + + // www.RTCMultiConnection.org/docs/caniuse/ + connection.caniuse = { + RTCPeerConnection: DetectRTC.isWebRTCSupported, + getUserMedia: !!navigator.webkitGetUserMedia || !!navigator.mozGetUserMedia, + AudioContext: DetectRTC.isAudioContextSupported, + + // there is no way to check whether "getUserMedia" flag is enabled or not! + ScreenSharing: DetectRTC.isScreenCapturingSupported, + RtpDataChannels: DetectRTC.isRtpDataChannelsSupported, + SctpDataChannels: DetectRTC.isSctpDataChannelsSupported + }; + + // www.RTCMultiConnection.org/docs/snapshots/ + connection.snapshots = {}; + + // www.WebRTC-Experiment.com/demos/MediaStreamTrack.getSources.html + connection._mediaSources = {}; + + // www.RTCMultiConnection.org/docs/devices/ + connection.devices = {}; + + // www.RTCMultiConnection.org/docs/language/ (to see list of all supported languages) + connection.language = 'en'; + + // www.RTCMultiConnection.org/docs/autoTranslateText/ + connection.autoTranslateText = false; + + // please use your own Google Translate API key + // Google Translate is a paid service. + connection.googKey = 'AIzaSyCgB5hmFY74WYB-EoWkhr9cAGr6TiTHrEE'; + + connection.localStreamids = []; + connection.localStreams = {}; + + // this object stores pre-recorded media streaming uids + // multiple pre-recorded media files can be streamed concurrently. + connection.preRecordedMedias = {}; + + // www.RTCMultiConnection.org/docs/attachStreams/ + connection.attachStreams = []; + + // www.RTCMultiConnection.org/docs/detachStreams/ + connection.detachStreams = []; + + connection.optionalArgument = { + optional: [{ + DtlsSrtpKeyAgreement: true + }, { + googImprovedWifiBwe: true + }, { + googScreencastMinBitrate: 300 + }], + mandatory: {} + }; + + connection.dataChannelDict = {}; + + // www.RTCMultiConnection.org/docs/dontAttachStream/ + connection.dontAttachStream = false; + + // www.RTCMultiConnection.org/docs/dontCaptureUserMedia/ + connection.dontCaptureUserMedia = false; + + // this feature added to keep users privacy and + // make sure HTTPs pages NEVER auto capture users media + // isChrome && location.protocol == 'https:' + connection.preventSSLAutoAllowed = false; + + connection.autoReDialOnFailure = true; + connection.isInitiator = false; + + // access DetectRTC.js features directly! + connection.DetectRTC = DetectRTC; + + // you can falsify it to merge all ICE in SDP and share only SDP! + // such mechanism is useful for SIP/XMPP and XMLHttpRequest signaling + // bug: renegotiation fails if "trickleIce" is false + connection.trickleIce = true; + + // this object stores list of all sessions in current channel + connection.sessionDescriptions = {}; + + // this object stores current user's session-description + // it is set only for initiator + // it is set as soon as "open" method is invoked. + connection.sessionDescription = null; + + // resources used in RTCMultiConnection + connection.resources = { + RecordRTC: 'https://cdn.webrtc-experiment.com/RecordRTC.js', + PreRecordedMediaStreamer: 'https://cdn.webrtc-experiment.com/PreRecordedMediaStreamer.js', + customGetUserMediaBar: 'https://cdn.webrtc-experiment.com/navigator.customGetUserMediaBar.js', + html2canvas: 'https://cdn.webrtc-experiment.com/screenshot.js', + hark: 'https://cdn.webrtc-experiment.com/hark.js', + firebase: 'https://cdn.webrtc-experiment.com/firebase.js', + firebaseio: 'https://webrtc.firebaseIO.com/', + muted: 'https://cdn.webrtc-experiment.com/images/muted.png', + getConnectionStats: 'https://cdn.webrtc-experiment.com/getConnectionStats.js', + FileBufferReader: 'https://cdn.webrtc-experiment.com/FileBufferReader.js' + }; + + // www.RTCMultiConnection.org/docs/body/ + connection.body = document.body || document.documentElement; + + // www.RTCMultiConnection.org/docs/peers/ + connection.peers = {}; + + // www.RTCMultiConnection.org/docs/firebase/ + connection.firebase = 'chat'; + + connection.numberOfSessions = 0; + connection.numberOfConnectedUsers = 0; + + // by default, data-connections will always be getting + // FileBufferReader.js if absent. + connection.enableFileSharing = true; + + // www.RTCMultiConnection.org/docs/autoSaveToDisk/ + // to make sure file-saver dialog is not invoked. + connection.autoSaveToDisk = false; + + connection.processSdp = function(sdp) { + // process sdp here + return sdp; + }; + + // www.RTCMultiConnection.org/docs/onmessage/ + connection.onmessage = function(e) { + log('onmessage', toStr(e)); + }; + + // www.RTCMultiConnection.org/docs/onopen/ + connection.onopen = function(e) { + log('Data connection is opened between you and', e.userid); + }; + + // www.RTCMultiConnection.org/docs/onerror/ + connection.onerror = function(e) { + error(onerror, toStr(e)); + }; + + // www.RTCMultiConnection.org/docs/onclose/ + connection.onclose = function(e) { + warn('onclose', toStr(e)); + + // todo: should we use "stop" or "remove"? + // BTW, it is remote user! + connection.streams.remove({ + userid: e.userid + }); + }; + + var progressHelper = {}; + + // www.RTCMultiConnection.org/docs/onFileStart/ + connection.onFileStart = function(file) { + var div = document.createElement('div'); + div.title = file.name; + div.innerHTML = ' '; + connection.body.insertBefore(div, connection.body.firstChild); + progressHelper[file.uuid] = { + div: div, + progress: div.querySelector('progress'), + label: div.querySelector('label') + }; + progressHelper[file.uuid].progress.max = file.maxChunks; + }; + + // www.RTCMultiConnection.org/docs/onFileProgress/ + connection.onFileProgress = function(chunk) { + var helper = progressHelper[chunk.uuid]; + if (!helper) return; + helper.progress.value = chunk.currentPosition || chunk.maxChunks || helper.progress.max; + updateLabel(helper.progress, helper.label); + }; + + // www.RTCMultiConnection.org/docs/onFileEnd/ + connection.onFileEnd = function(file) { + if (progressHelper[file.uuid]) progressHelper[file.uuid].div.innerHTML = '' + file.name + ''; + + // for backward compatibility + if (connection.onFileSent || connection.onFileReceived) { + if (connection.onFileSent) connection.onFileSent(file, file.uuid); + if (connection.onFileReceived) connection.onFileReceived(file.name, file); + } + }; + + function updateLabel(progress, label) { + if (progress.position == -1) return; + var position = +progress.position.toFixed(2).split('.')[1] || 100; + label.innerHTML = position + '%'; + } + + // www.RTCMultiConnection.org/docs/onstream/ + connection.onstream = function(e) { + connection.body.insertBefore(e.mediaElement, connection.body.firstChild); + }; + + // www.RTCMultiConnection.org/docs/onStreamEndedHandler/ + connection.onstreamended = function(e) { + log('onStreamEndedHandler:', e); + + if (!e.mediaElement) { + return warn('Event.mediaElement is undefined', e); + } + if (!e.mediaElement.parentNode) { + e.mediaElement = document.getElementById(e.streamid); + + if (!e.mediaElement) { + return warn('Event.mediaElement is undefined', e); + } + + if (!e.mediaElement.parentNode) { + return warn('Event.mediElement.parentNode is null.', e); + } + } + + e.mediaElement.parentNode.removeChild(e.mediaElement); + }; + + // todo: need to write documentation link + connection.onSessionClosed = function(session) { + if (session.isEjected) { + warn(session.userid, 'ejected you.'); + } else warn('Session has been closed.', session); + }; + + // www.RTCMultiConnection.org/docs/onmute/ + connection.onmute = function(e) { + if (e.isVideo && e.mediaElement) { + e.mediaElement.pause(); + e.mediaElement.setAttribute('poster', e.snapshot || connection.resources.muted); + } + if (e.isAudio && e.mediaElement) { + e.mediaElement.muted = true; + } + }; + + // www.RTCMultiConnection.org/docs/onunmute/ + connection.onunmute = function(e) { + if (e.isVideo && e.mediaElement) { + e.mediaElement.play(); + e.mediaElement.removeAttribute('poster'); + } + if (e.isAudio && e.mediaElement) { + e.mediaElement.muted = false; + } + }; + + // www.RTCMultiConnection.org/docs/onleave/ + connection.onleave = function(e) { + log('onleave', toStr(e)); + }; + + connection.token = getRandomString; + + connection.peers[connection.userid] = { + drop: function() { + connection.drop(); + }, + renegotiate: function() {}, + addStream: function() {}, + hold: function() {}, + unhold: function() {}, + changeBandwidth: function() {}, + sharePartOfScreen: function() {} + }; + + connection._skip = ['stop', 'mute', 'unmute', '_private', '_selectStreams', 'selectFirst', 'selectAll', 'remove']; + + // www.RTCMultiConnection.org/docs/streams/ + connection.streams = { + mute: function(session) { + this._private(session, true); + }, + unmute: function(session) { + this._private(session, false); + }, + _private: function(session, enabled) { + if (session && !isString(session)) { + for (var stream in this) { + if (connection._skip.indexOf(stream) == -1) { + _muteOrUnMute(this[stream], session, enabled); + } + } + + function _muteOrUnMute(stream, session, isMute) { + if (session.local && stream.type != 'local') return; + if (session.remote && stream.type != 'remote') return; + + if (session.isScreen && !stream.isScreen) return; + if (session.isAudio && !stream.isAudio) return; + if (session.isVideo && !stream.isVideo) return; + + if (isMute) stream.mute(session); + else stream.unmute(session); + } + return; + } + + // implementation from #68 + for (var stream in this) { + if (connection._skip.indexOf(stream) == -1) { + this[stream]._private(session, enabled); + } + } + }, + stop: function(type) { + var _stream; + for (var stream in this) { + if (connection._skip.indexOf(stream) == -1) { + _stream = this[stream]; + + if (!type) _stream.stop(); + + else if (isString(type)) { + // connection.streams.stop('screen'); + var config = {}; + config[type] = true; + _stopStream(_stream, config); + } else _stopStream(_stream, type); + } + } + + function _stopStream(_stream, config) { + // connection.streams.stop({ remote: true, userid: 'remote-userid' }); + if (config.userid && _stream.userid != config.userid) return; + + if (config.local && _stream.type != 'local') return; + if (config.remote && _stream.type != 'remote') return; + + if (config.screen && !!_stream.isScreen) { + _stream.stop(); + } + + if (config.audio && !!_stream.isAudio) { + _stream.stop(); + } + + if (config.video && !!_stream.isVideo) { + _stream.stop(); + } + + // connection.streams.stop('local'); + if (!config.audio && !config.video && !config.screen) { + _stream.stop(); + } + } + }, + remove: function(type) { + var _stream; + for (var stream in this) { + if (connection._skip.indexOf(stream) == -1) { + _stream = this[stream]; + + if (!type) _stopAndRemoveStream(_stream, { + local: true, + remote: true + }); + + else if (isString(type)) { + // connection.streams.stop('screen'); + var config = {}; + config[type] = true; + _stopAndRemoveStream(_stream, config); + } else _stopAndRemoveStream(_stream, type); + } + } + + function _stopAndRemoveStream(_stream, config) { + // connection.streams.remove({ remote: true, userid: 'remote-userid' }); + if (config.userid && _stream.userid != config.userid) return; + + if (config.local && _stream.type != 'local') return; + if (config.remote && _stream.type != 'remote') return; + + if (config.screen && !!_stream.isScreen) { + endStream(_stream); + } + + if (config.audio && !!_stream.isAudio) { + endStream(_stream); + } + + if (config.video && !!_stream.isVideo) { + endStream(_stream); + } + + // connection.streams.remove('local'); + if (!config.audio && !config.video && !config.screen) { + endStream(_stream); + } + } + + function endStream(_stream) { + onStreamEndedHandler(_stream, connection); + delete connection.streams[_stream.streamid]; + } + }, + selectFirst: function(args) { + return this._selectStreams(args, false); + }, + selectAll: function(args) { + return this._selectStreams(args, true); + }, + _selectStreams: function(args, all) { + if (!args || isString(args) || isEmpty(args)) throw 'Invalid arguments.'; + + // if userid is used then both local/remote shouldn't be auto-set + if (isNull(args.local) && isNull(args.remote) && isNull(args.userid)) { + args.local = args.remote = true; + } + + if (!args.isAudio && !args.isVideo && !args.isScreen) { + args.isAudio = args.isVideo = args.isScreen = true; + } + + var selectedStreams = []; + for (var stream in this) { + if (connection._skip.indexOf(stream) == -1 && (stream = this[stream]) && ((args.local && stream.type == 'local') || (args.remote && stream.type == 'remote') || (args.userid && stream.userid == args.userid))) { + if (args.isVideo && stream.isVideo) { + selectedStreams.push(stream); + } + + if (args.isAudio && stream.isAudio) { + selectedStreams.push(stream); + } + + if (args.isScreen && stream.isScreen) { + selectedStreams.push(stream); + } + } + } + + return !!all ? selectedStreams : selectedStreams[0]; + } + }; + + connection.iceServers = IceServersHandler.getIceServers(); + + connection.rtcConfiguration = { + iceServers: null, + iceTransports: 'all', // none || relay || all - ref: http://goo.gl/40I39K + peerIdentity: false + }; + + // www.RTCMultiConnection.org/docs/media/ + connection.media = { + min: function(width, height) { + if (!connection.mediaConstraints.video) return; + + if (!connection.mediaConstraints.video.mandatory) { + connection.mediaConstraints.video.mandatory = {}; + } + connection.mediaConstraints.video.mandatory.minWidth = width; + connection.mediaConstraints.video.mandatory.minHeight = height; + }, + max: function(width, height) { + if (!connection.mediaConstraints.video) return; + + if (!connection.mediaConstraints.video.mandatory) { + connection.mediaConstraints.video.mandatory = {}; + } + + connection.mediaConstraints.video.mandatory.maxWidth = width; + connection.mediaConstraints.video.mandatory.maxHeight = height; + } + }; + + connection._getStream = function(event) { + var resultingObject = merge({ + sockets: event.socket ? [event.socket] : [] + }, event); + + resultingObject.stop = function() { + var self = this; + + self.sockets.forEach(function(socket) { + if (self.type == 'local') { + socket.send({ + streamid: self.streamid, + stopped: true + }); + } + + if (self.type == 'remote') { + socket.send({ + promptStreamStop: true, + streamid: self.streamid + }); + } + }); + + if (self.type == 'remote') return; + + var stream = self.stream; + if (stream) self.rtcMultiConnection.stopMediaStream(stream); + }; + + resultingObject.mute = function(session) { + this.muted = true; + this._private(session, true); + }; + + resultingObject.unmute = function(session) { + this.muted = false; + this._private(session, false); + }; + + function muteOrUnmuteLocally(session, isPause, mediaElement) { + if (!mediaElement) return; + var lastPauseState = mediaElement.onpause; + var lastPlayState = mediaElement.onplay; + mediaElement.onpause = mediaElement.onplay = function() {}; + + if (isPause) mediaElement.pause(); + else mediaElement.play(); + + mediaElement.onpause = lastPauseState; + mediaElement.onplay = lastPlayState; + } + + resultingObject._private = function(session, enabled) { + if (session && !isNull(session.sync) && session.sync == false) { + muteOrUnmuteLocally(session, enabled, this.mediaElement); + return; + } + + muteOrUnmute({ + root: this, + session: session, + enabled: enabled, + stream: this.stream + }); + }; + + resultingObject.startRecording = function(session) { + var self = this; + + if (!session) { + session = { + audio: true, + video: true + }; + } + + if (isString(session)) { + session = { + audio: session == 'audio', + video: session == 'video' + }; + } + + if (!window.RecordRTC) { + return loadScript(self.rtcMultiConnection.resources.RecordRTC, function() { + self.startRecording(session); + }); + } + + log('started recording session', session); + + self.videoRecorder = self.audioRecorder = null; + + if (isFirefox) { + // firefox supports both audio/video recording in single webm file + if (self.stream.getAudioTracks().length && self.stream.getVideoTracks().length) { + self.videoRecorder = RecordRTC(self.stream, { + type: 'video' + }); + } else if (session.video) { + self.videoRecorder = RecordRTC(self.stream, { + type: 'video' + }); + } else if (session.audio) { + self.audioRecorder = RecordRTC(self.stream, { + type: 'audio' + }); + } + } else if (isChrome) { + // chrome >= 48 supports MediaRecorder API + // MediaRecorder API can record remote audio+video streams as well! + + if (isMediaRecorderCompatible() && connection.DetectRTC.browser.version >= 50 && self.stream.getAudioTracks().length && self.stream.getVideoTracks().length) { + self.videoRecorder = RecordRTC(self.stream, { + type: 'video' + }); + } else if (isMediaRecorderCompatible() && connection.DetectRTC.browser.version >= 50) { + if (session.video) { + self.videoRecorder = RecordRTC(self.stream, { + type: 'video' + }); + } else if (session.audio) { + self.audioRecorder = RecordRTC(self.stream, { + type: 'audio' + }); + } + } else { + // chrome supports recording in two separate files: WAV and WebM + if (session.video) { + self.videoRecorder = RecordRTC(self.stream, { + type: 'video' + }); + } + + if (session.audio) { + self.audioRecorder = RecordRTC(self.stream, { + type: 'audio' + }); + } + } + } + + if (self.audioRecorder) { + self.audioRecorder.startRecording(); + } + + if (self.videoRecorder) self.videoRecorder.startRecording(); + }; + + resultingObject.stopRecording = function(callback, session) { + if (!session) { + session = { + audio: true, + video: true + }; + } + + if (isString(session)) { + session = { + audio: session == 'audio', + video: session == 'video' + }; + } + + log('stopped recording session', session); + + var self = this; + + if (session.audio && self.audioRecorder) { + self.audioRecorder.stopRecording(function() { + if (session.video && self.videoRecorder) { + self.videoRecorder.stopRecording(function() { + callback({ + audio: self.audioRecorder.getBlob(), + video: self.videoRecorder.getBlob() + }); + }); + } else callback({ + audio: self.audioRecorder.getBlob() + }); + }); + } else if (session.video && self.videoRecorder) { + self.videoRecorder.stopRecording(function() { + callback({ + video: self.videoRecorder.getBlob() + }); + }); + } + }; + + resultingObject.takeSnapshot = function(callback) { + takeSnapshot({ + mediaElement: this.mediaElement, + userid: this.userid, + connection: connection, + callback: callback + }); + }; + + // redundant: kept only for backward compatibility + resultingObject.streamObject = resultingObject; + + return resultingObject; + }; + + // new RTCMultiConnection().set({properties}).connect() + connection.set = function(properties) { + for (var property in properties) { + this[property] = properties[property]; + } + return this; + }; + + // www.RTCMultiConnection.org/docs/onMediaError/ + connection.onMediaError = function(event) { + error('name', event.name); + error('constraintName', toStr(event.constraintName)); + error('message', event.message); + error('original session', event.session); + }; + + // www.RTCMultiConnection.org/docs/takeSnapshot/ + connection.takeSnapshot = function(userid, callback) { + takeSnapshot({ + userid: userid, + connection: connection, + callback: callback + }); + }; + + connection.saveToDisk = function(blob, fileName) { + if (blob.size && blob.type) FileSaver.SaveToDisk(URL.createObjectURL(blob), fileName || blob.name || blob.type.replace('/', '-') + blob.type.split('/')[1]); + else FileSaver.SaveToDisk(blob, fileName); + }; + + // www.RTCMultiConnection.org/docs/selectDevices/ + connection.selectDevices = function(device1, device2) { + if (device1) select(this.devices[device1]); + if (device2) select(this.devices[device2]); + + function select(device) { + if (!device) return; + connection._mediaSources[device.kind] = device.id; + } + }; + + // www.RTCMultiConnection.org/docs/getDevices/ + connection.getDevices = function(callback) { + // if, not yet fetched. + if (!DetectRTC.MediaDevices.length) { + return setTimeout(function() { + connection.getDevices(callback); + }, 1000); + } + + // loop over all audio/video input/output devices + DetectRTC.MediaDevices.forEach(function(device) { + connection.devices[device.deviceId] = device; + }); + + if (callback) callback(connection.devices); + }; + + connection.getMediaDevices = connection.enumerateDevices = function(callback) { + if (!callback) throw 'callback is mandatory.'; + connection.getDevices(function() { + callback(connection.DetectRTC.MediaDevices); + }); + }; + + // www.RTCMultiConnection.org/docs/onCustomMessage/ + connection.onCustomMessage = function(message) { + log('Custom message', message); + }; + + // www.RTCMultiConnection.org/docs/ondrop/ + connection.ondrop = function(droppedBy) { + log('Media connection is dropped by ' + droppedBy); + }; + + // www.RTCMultiConnection.org/docs/drop/ + connection.drop = function(config) { + config = config || {}; + connection.attachStreams = []; + + // "drop" should detach all local streams + for (var stream in connection.streams) { + if (connection._skip.indexOf(stream) == -1) { + stream = connection.streams[stream]; + if (stream.type == 'local') { + connection.detachStreams.push(stream.streamid); + onStreamEndedHandler(stream, connection); + } else onStreamEndedHandler(stream, connection); + } + } + + // www.RTCMultiConnection.org/docs/sendCustomMessage/ + connection.sendCustomMessage({ + drop: true, + dontRenegotiate: isNull(config.renegotiate) ? true : config.renegotiate + }); + }; + + // www.RTCMultiConnection.org/docs/Translator/ + connection.Translator = { + TranslateText: function(text, callback) { + // if(location.protocol === 'https:') return callback(text); + + var newScript = document.createElement('script'); + newScript.type = 'text/javascript'; + + var sourceText = encodeURIComponent(text); // escape + + var randomNumber = 'method' + connection.token(); + window[randomNumber] = function(response) { + if (response.data && response.data.translations[0] && callback) { + callback(response.data.translations[0].translatedText); + } + + if (response.error && response.error.message == 'Daily Limit Exceeded') { + warn('Text translation failed. Error message: "Daily Limit Exceeded."'); + + // returning original text + callback(text); + } + }; + + var source = 'https://www.googleapis.com/language/translate/v2?key=' + connection.googKey + '&target=' + (connection.language || 'en-US') + '&callback=window.' + randomNumber + '&q=' + sourceText; + newScript.src = source; + document.getElementsByTagName('head')[0].appendChild(newScript); + } + }; + + // you can easily override it by setting it NULL! + connection.setDefaultEventsForMediaElement = function(mediaElement, streamid) { + mediaElement.onpause = function() { + if (connection.streams[streamid] && !connection.streams[streamid].muted) { + connection.streams[streamid].mute(); + } + }; + + // todo: need to make sure that "onplay" EVENT doesn't play self-voice! + mediaElement.onplay = function() { + if (connection.streams[streamid] && connection.streams[streamid].muted) { + connection.streams[streamid].unmute(); + } + }; + + var volumeChangeEventFired = false; + mediaElement.onvolumechange = function() { + if (!volumeChangeEventFired) { + volumeChangeEventFired = true; + connection.streams[streamid] && setTimeout(function() { + var root = connection.streams[streamid]; + connection.streams[streamid].sockets.forEach(function(socket) { + socket.send({ + streamid: root.streamid, + isVolumeChanged: true, + volume: mediaElement.volume + }); + }); + volumeChangeEventFired = false; + }, 2000); + } + }; + }; + + // www.RTCMultiConnection.org/docs/onMediaFile/ + connection.onMediaFile = function(e) { + log('onMediaFile', e); + connection.body.appendChild(e.mediaElement); + }; + + // www.RTCMultiConnection.org/docs/shareMediaFile/ + // this method handles pre-recorded media streaming + connection.shareMediaFile = function(file, video, streamerid) { + streamerid = streamerid || connection.token(); + + if (!PreRecordedMediaStreamer) { + loadScript(connection.resources.PreRecordedMediaStreamer, function() { + connection.shareMediaFile(file, video, streamerid); + }); + return streamerid; + } + + return PreRecordedMediaStreamer.shareMediaFile({ + file: file, + video: video, + streamerid: streamerid, + connection: connection + }); + }; + + // www.RTCMultiConnection.org/docs/onpartofscreen/ + connection.onpartofscreen = function(e) { + var image = document.createElement('img'); + image.src = e.screenshot; + connection.body.appendChild(image); + }; + + connection.skipLogs = function() { + log = error = warn = function() {}; + }; + + // www.RTCMultiConnection.org/docs/hold/ + connection.hold = function(mLine) { + for (var peer in connection.peers) { + connection.peers[peer].hold(mLine); + } + }; + + // www.RTCMultiConnection.org/docs/onhold/ + connection.onhold = function(track) { + log('onhold', track); + + if (track.kind != 'audio') { + track.mediaElement.pause(); + track.mediaElement.setAttribute('poster', track.screenshot || connection.resources.muted); + } + if (track.kind == 'audio') { + track.mediaElement.muted = true; + } + }; + + // www.RTCMultiConnection.org/docs/unhold/ + connection.unhold = function(mLine) { + for (var peer in connection.peers) { + connection.peers[peer].unhold(mLine); + } + }; + + // www.RTCMultiConnection.org/docs/onunhold/ + connection.onunhold = function(track) { + log('onunhold', track); + + if (track.kind != 'audio') { + track.mediaElement.play(); + track.mediaElement.removeAttribute('poster'); + } + if (track.kind != 'audio') { + track.mediaElement.muted = false; + } + }; + + connection.sharePartOfScreen = function(args) { + var lastScreenshot = ''; + + function partOfScreenCapturer() { + // if stopped + if (connection.partOfScreen && !connection.partOfScreen.sharing) { + return; + } + + capturePartOfScreen({ + element: args.element, + connection: connection, + callback: function(screenshot) { + // don't share repeated content + if (screenshot != lastScreenshot) { + lastScreenshot = screenshot; + + for (var channel in connection.channels) { + connection.channels[channel].send({ + screenshot: screenshot, + isPartOfScreen: true + }); + } + } + + // "once" can be used to share single screenshot + !args.once && setTimeout(partOfScreenCapturer, args.interval || 200); + } + }); + } + + partOfScreenCapturer(); + + connection.partOfScreen = merge({ + sharing: true + }, args); + }; + + connection.pausePartOfScreenSharing = function() { + for (var peer in connection.peers) { + connection.peers[peer].pausePartOfScreenSharing = true; + } + + if (connection.partOfScreen) { + connection.partOfScreen.sharing = false; + } + }; + + connection.resumePartOfScreenSharing = function() { + for (var peer in connection.peers) { + connection.peers[peer].pausePartOfScreenSharing = false; + } + + if (connection.partOfScreen) { + connection.partOfScreen.sharing = true; + } + }; + + connection.stopPartOfScreenSharing = function() { + for (var peer in connection.peers) { + connection.peers[peer].stopPartOfScreenSharing = true; + } + + if (connection.partOfScreen) { + connection.partOfScreen.sharing = false; + } + }; + + connection.takeScreenshot = function(element, callback) { + if (!element || !callback) throw 'Invalid number of arguments.'; + + if (!window.html2canvas) { + return loadScript(connection.resources.html2canvas, function() { + connection.takeScreenshot(element); + }); + } + + if (isString(element)) { + element = document.querySelector(element); + if (!element) element = document.getElementById(element); + } + if (!element) throw 'HTML Element is inaccessible!'; + + // html2canvas.js is used to take screenshots + html2canvas(element, { + onrendered: function(canvas) { + callback(canvas.toDataURL()); + } + }); + }; + + // this event is fired when RTCMultiConnection detects that chrome extension + // for screen capturing is installed and available + connection.onScreenCapturingExtensionAvailable = function() { + log('It seems that screen capturing extension is installed and available on your system!'); + }; + + if (!isPluginRTC && DetectRTC.screen.onScreenCapturingExtensionAvailable) { + DetectRTC.screen.onScreenCapturingExtensionAvailable = function() { + connection.onScreenCapturingExtensionAvailable(); + }; + } + + connection.changeBandwidth = function(bandwidth) { + for (var peer in connection.peers) { + connection.peers[peer].changeBandwidth(bandwidth); + } + }; + + connection.convertToAudioStream = function(mediaStream) { + convertToAudioStream(mediaStream); + }; + + connection.onstatechange = function(state) { + log('on:state:change (' + state.userid + '):', state.name + ':', state.reason || ''); + }; + + connection.onfailed = function(event) { + if (!event.peer.numOfRetries) event.peer.numOfRetries = 0; + event.peer.numOfRetries++; + + error('ICE connectivity check is failed. Renegotiating peer connection.'); + event.peer.numOfRetries < 2 && event.peer.renegotiate(); + + if (event.peer.numOfRetries >= 2) event.peer.numOfRetries = 0; + }; + + connection.onconnected = function(event) { + // event.peer.addStream || event.peer.getConnectionStats + log('Peer connection has been established between you and', event.userid); + }; + + connection.ondisconnected = function(event) { + error('Peer connection seems has been disconnected between you and', event.userid); + + if (isEmpty(connection.channels)) return; + if (!connection.channels[event.userid]) return; + + // use WebRTC data channels to detect user's presence + connection.channels[event.userid].send({ + checkingPresence: true + }); + + // wait 5 seconds, if target peer didn't response, simply disconnect + setTimeout(function() { + // iceConnectionState == 'disconnected' occurred out of low-bandwidth + // or internet connectivity issues + if (connection.peers[event.userid].connected) { + delete connection.peers[event.userid].connected; + return; + } + + // to make sure this user's all remote streams are removed. + connection.streams.remove({ + remote: true, + userid: event.userid + }); + + connection.remove(event.userid); + }, 3000); + }; + + connection.onstreamid = function(event) { + // event.isScreen || event.isVideo || event.isAudio + log('got remote streamid', event.streamid, 'from', event.userid); + }; + + connection.stopMediaStream = function(mediaStream) { + if (!mediaStream) throw 'MediaStream argument is mandatory.'; + + if (connection.keepStreamsOpened) { + if (mediaStream.onended) mediaStream.onended(); + return; + } + + // remove stream from "localStreams" object + // when native-stop method invoked. + if (connection.localStreams[mediaStream.streamid]) { + delete connection.localStreams[mediaStream.streamid]; + } + + if (isFirefox) { + // Firefox don't yet support onended for any stream (remote/local) + if (mediaStream.onended) mediaStream.onended(); + } + + // Latest firefox does support mediaStream.getAudioTrack but doesn't support stop on MediaStreamTrack + var checkForMediaStreamTrackStop = Boolean( + (mediaStream.getAudioTracks || mediaStream.getVideoTracks) && ( + (mediaStream.getAudioTracks()[0] && !mediaStream.getAudioTracks()[0].stop) || + (mediaStream.getVideoTracks()[0] && !mediaStream.getVideoTracks()[0].stop) + ) + ); + + if (!mediaStream.getAudioTracks || checkForMediaStreamTrackStop) { + if (mediaStream.stop) { + mediaStream.stop(); + } + return; + } + + if (mediaStream.getAudioTracks().length && mediaStream.getAudioTracks()[0].stop) { + mediaStream.getAudioTracks().forEach(function(track) { + track.stop(); + }); + } + + if (mediaStream.getVideoTracks().length && mediaStream.getVideoTracks()[0].stop) { + mediaStream.getVideoTracks().forEach(function(track) { + track.stop(); + }); + } + }; + + connection.changeBandwidth = function(bandwidth) { + if (!bandwidth || isString(bandwidth) || isEmpty(bandwidth)) { + throw 'Invalid "bandwidth" arguments.'; + } + + forEach(connection.peers, function(peer) { + peer.peer.bandwidth = bandwidth; + }); + + connection.renegotiate(); + }; + + // www.RTCMultiConnection.org/docs/openSignalingChannel/ + // http://goo.gl/uvoIcZ + connection.openSignalingChannel = function(config) { + // make sure firebase.js is loaded + if (!window.Firebase) { + return loadScript(connection.resources.firebase, function() { + connection.openSignalingChannel(config); + }); + } + + var channel = config.channel || connection.channel; + + if (connection.firebase) { + // for custom firebase instances + connection.resources.firebaseio = connection.resources.firebaseio.replace('//chat.', '//' + connection.firebase + '.'); + } + + var firebase = new Firebase(connection.resources.firebaseio + channel); + firebase.channel = channel; + firebase.on('child_added', function(data) { + config.onmessage(data.val()); + }); + + firebase.send = function(data) { + // a quick dirty workaround to make sure firebase + // shouldn't fail for NULL values. + for (var prop in data) { + if (isNull(data[prop]) || typeof data[prop] == 'function') { + data[prop] = false; + } + } + + this.push(data); + }; + + if (!connection.socket) + connection.socket = firebase; + + firebase.onDisconnect().remove(); + + setTimeout(function() { + config.callback(firebase); + }, 1); + }; + + connection.Plugin = Plugin; + } + +})(); \ No newline at end of file diff --git a/camera-mic.html b/camera-mic.html new file mode 100755 index 0000000..7a1363d --- /dev/null +++ b/camera-mic.html @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/camera-mic.js b/camera-mic.js new file mode 100755 index 0000000..596f8ff --- /dev/null +++ b/camera-mic.js @@ -0,0 +1,15 @@ +document.write('

The purpose of this page is to access your camera and microphone.

'); +document.write('

You can REMOVE i.e. DELETE camera permissions anytime on this page:

'); +document.write('
chrome://settings/content/camera?search=camera');
+
+var constraints = {
+    audio: true,
+    video: true
+};
+
+navigator.mediaDevices.getUserMedia(constraints).then(function(stream) {
+    document.write('

'); + document.querySelector('h1').innerHTML = 'Now you can close this page and click extension icon again.' +}).catch(function() { + document.querySelector('h1').innerHTML = 'Unable to capture your camera and microphone.'; +}); diff --git a/desktop-capturing.js b/desktop-capturing.js new file mode 100755 index 0000000..4c5d6ec --- /dev/null +++ b/desktop-capturing.js @@ -0,0 +1,925 @@ +// Muaz Khan - https://github.com/muaz-khan +// MIT License - https://www.WebRTC-Experiment.com/licence/ +// Source Code - https://github.com/muaz-khan/Chrome-Extensions + +// this page is using desktopCapture API to capture and share desktop +// http://developer.chrome.com/extensions/desktopCapture.html + +// chrome.browserAction.onClicked.addListener(captureDesktop); + +var runtimePort; +var websocket; +chrome.runtime.onConnect.addListener(function(port) { + runtimePort = port; + + runtimePort.onMessage.addListener(function(message) { + if (!message || !message.messageFromContentScript1234) { + return; + } + + if (message.startSharing || message.stopSharing) { + captureDesktop(); + return; + } + }); +}); + + +window.addEventListener('offline', function() { + if (!connection || !connection.attachStreams.length) return; + + setDefaults(); + chrome.runtime.reload(); +}, false); + +window.addEventListener('online', function() { + if (!connection) return; + + setDefaults(); + chrome.runtime.reload(); +}, false); + +function captureDesktop() { + if (connection && connection.attachStreams[0]) { + setDefaults(); + + connection.attachStreams.forEach(function(stream) { + stream.getTracks().forEach(function(track) { + track.stop(); + }); + }); + + chrome.storage.sync.set({ + enableTabCaptureAPI: 'false', + enableMicrophone: 'false', + enableCamera: 'false', + enableScreen: 'false', + isSharingOn: 'false', + enableSpeakers: 'false' + }); + return; + } + + chrome.browserAction.setTitle({ + title: 'Capturing Desktop' + }); + + chrome.storage.sync.get(null, function(items) { + var resolutions = {}; + + if (items['room_password']) { + room_password = items['room_password']; + } + + if (items['room_id']) { + room_id = items['room_id']; + } + + if (items['codecs']) { + codecs = items['codecs']; + } + + if (items['bandwidth']) { + bandwidth = items['bandwidth']; + } + + if (items['enableTabCaptureAPI'] == 'true') { + enableTabCaptureAPI = items['enableTabCaptureAPI']; + } + + if (items['enableMicrophone'] == 'true') { + enableMicrophone = items['enableMicrophone']; + } + + if (items['enableSpeakers'] == 'true') { + enableSpeakers = items['enableSpeakers']; + } + + if (items['enableCamera'] == 'true') { + enableCamera = items['enableCamera']; + } + + if (items['enableScreen'] == 'true') { + enableScreen = items['enableScreen']; + } + + if (items['enableTabCaptureAPI'] == 'true') { + enableTabCaptureAPI = items['enableTabCaptureAPI']; + } + + if (items['isSharingOn'] == 'true') { + isSharingOn = items['isSharingOn']; + } + + var _resolutions = items['resolutions']; + if (!_resolutions) { + _resolutions = 'fit-screen'; + chrome.storage.sync.set({ + resolutions: 'fit-screen' + }, function() {}); + } + + if (_resolutions === 'fit-screen') { + // resolutions.maxWidth = screen.availWidth; + // resolutions.maxHeight = screen.availHeight; + + resolutions.maxWidth = screen.width; + resolutions.maxHeight = screen.height; + } + + if (_resolutions === '4K') { + resolutions.maxWidth = 3840; + resolutions.maxHeight = 2160; + } + + if (_resolutions === '1080p') { + resolutions.maxWidth = 1920; + resolutions.maxHeight = 1080; + } + + if (_resolutions === '720p') { + resolutions.maxWidth = 1280; + resolutions.maxHeight = 720; + } + + if (_resolutions === '360p') { + resolutions.maxWidth = 640; + resolutions.maxHeight = 360; + } + + if (_resolutions === '4K') { + alert('"4K" resolutions is not stable in Chrome. Please try "fit-screen" instead.'); + } + + var sources = ['screen', 'window', 'tab']; + + if (enableSpeakers) { + sources.push('audio'); + } + + if (enableTabCaptureAPI) { + captureTabUsingTabCapture(resolutions); + return; + } + + if (enableCamera || enableMicrophone) { + captureCamera(function(stream) { + if (!enableScreen) { + gotCustomStream(stream); + //CHANGES + //gotStream(stream); + return; + } + + desktop_id = chrome.desktopCapture.chooseDesktopMedia(sources, function(chromeMediaSourceId, opts) { + opts = opts || {}; + opts.resolutions = resolutions; + opts.stream = stream; + onAccessApproved(chromeMediaSourceId, opts); + }); + }); + return; + } + + desktop_id = chrome.desktopCapture.chooseDesktopMedia(sources, function(chromeMediaSourceId, opts) { + opts = opts || {}; + opts.resolutions = resolutions; + onAccessApproved(chromeMediaSourceId, opts); + }); + }); +} + +function captureTabUsingTabCapture(resolutions) { + chrome.tabs.query({ + active: true, + currentWindow: true + }, function(arrayOfTabs) { + var activeTab = arrayOfTabs[0]; + var activeTabId = activeTab.id; // or do whatever you need + + var constraints = { + video: true, + videoConstraints: { + mandatory: { + chromeMediaSource: 'tab', + maxWidth: resolutions.maxWidth, + maxHeight: resolutions.maxHeight, + minWidth: resolutions.minWidth, + minHeight: resolutions.minHeight, + minAspectRatio: getAspectRatio(resolutions.maxWidth, resolutions.maxHeight), + maxAspectRatio: getAspectRatio(resolutions.maxWidth, resolutions.maxHeight), + minFrameRate: 64, + maxFrameRate: 128 + } + } + }; + + if (!!enableSpeakers) { + constraints.audio = true; + constraints.audioConstraints = { + mandatory: { + echoCancellation: true + } + }; + } + + // chrome.tabCapture.onStatusChanged.addListener(function(event) { /* event.status */ }); + + chrome.tabCapture.capture(constraints, function(stream) { + gotTabCaptureStream(stream, constraints); + }); + }); +} + +function gotTabCaptureStream(stream, constraints) { + if (!stream) { + if (constraints.audio === true) { + enableSpeakers = false; + captureTabUsingTabCapture(resolutions); + return; + } + return alert('still no tabCapture stream'); + chrome.runtime.reload(); + return; + } + + var newStream = new MediaStream(); + + stream.getTracks().forEach(function(track) { + newStream.addTrack(track); + }); + + initVideoPlayer(newStream); + + gotCustomStream(newStream); + // CHANGES + //gotStream(newStream); +} + +var desktop_id; +var constraints; +var room_password = ''; +var room_id = ''; +var codecs = 'default'; +var bandwidth; + +var enableTabCaptureAPI; +var enableMicrophone; +var enableSpeakers; +var enableCamera; +var enableScreen; +var isSharingOn; + +// Array of blobs +var recordedBlobs; +// Socket for sending data +var socket; + + +function getAspectRatio(w, h) { + function gcd(a, b) { + return (b == 0) ? a : gcd(b, a % b); + } + var r = gcd(w, h); + return (w / r) / (h / r); +} + +function onAccessApproved(chromeMediaSourceId, opts) { + if (!chromeMediaSourceId) { + setDefaults(); + return; + } + + var resolutions = opts.resolutions; + + // CHANGES + chrome.storage.sync.get(null, function(items) { + constraints = { + audio: false, + video: { + mandatory: { + chromeMediaSource: 'desktop', + chromeMediaSourceId: chromeMediaSourceId, + maxWidth: 1280, //resolutions.maxWidth, + maxHeight: 720,//resolutions.maxHeight, + minWidth: resolutions.minWidth, + minHeight: resolutions.minHeight, + minAspectRatio: getAspectRatio(resolutions.maxWidth, resolutions.maxHeight), + maxAspectRatio: getAspectRatio(resolutions.maxWidth, resolutions.maxHeight), + minFrameRate: 25, //64, + maxFrameRate: 60 //128 + }, + optional: [] + } + }; + + if (opts.canRequestAudioTrack === true) { + constraints.audio = { + mandatory: { + chromeMediaSource: 'desktop', + chromeMediaSourceId: chromeMediaSourceId, + echoCancellation: true + }, + optional: [] + }; + } + + navigator.webkitGetUserMedia(constraints, function(screenStream) { + var win; + addStreamStopListener(screenStream, function() { + if (win && !win.closed) { + win.close(); + } else { + captureDesktop(); + } + }); + + if (opts.stream) { + if (enableCamera && opts.stream.getVideoTracks().length) { + var cameraStream = opts.stream; + + screenStream.fullcanvas = true; + screenStream.width = screen.width; // or 3840 + screenStream.height = screen.height; // or 2160 + + cameraStream.width = parseInt((15 / 100) * screenStream.width); + cameraStream.height = parseInt((15 / 100) * screenStream.height); + cameraStream.top = screenStream.height - cameraStream.height - 20; + cameraStream.left = screenStream.width - cameraStream.width - 20; + + var mixer = new MultiStreamsMixer([screenStream, cameraStream]); + + mixer.frameInterval = 1; + mixer.startDrawingFrames(); + + screenStream = mixer.getMixedStream(); + // win = openVideoPreview(screenStream); + } else if (enableMicrophone && opts.stream.getAudioTracks().length) { + var speakers = new MediaStream(); + screenStream.getAudioTracks().forEach(function(track) { + speakers.addTrack(track); + screenStream.removeTrack(track); + }); + + var mixer = new MultiStreamsMixer([speakers, opts.stream]); + mixer.getMixedStream().getAudioTracks().forEach(function(track) { + screenStream.addTrack(track); + }); + + screenStream.getVideoTracks().forEach(function(track) { + track.onended = function() { + if (win && !win.closed) { + win.close(); + } else { + captureDesktop(); + } + }; + }) + } + } + gotCustomStream(screenStream); + // CHANGES + //gotStream(screenStream); + }, getUserMediaError); + }); +} + +function openVideoPreview(stream) { + var win = window.open("video.html?src=" + URL.createObjectURL(stream), "_blank", "top=0,left=0"); + var timer = setInterval(function() { + if (win.closed) { + clearInterval(timer); + captureDesktop(); + } + }, 1000); + return win; +} + +function addStreamStopListener(stream, callback) { + var streamEndedEvent = 'ended'; + if ('oninactive' in stream) { + streamEndedEvent = 'inactive'; + } + stream.addEventListener(streamEndedEvent, function() { + callback(); + callback = function() {}; + }, false); + stream.getAudioTracks().forEach(function(track) { + track.addEventListener(streamEndedEvent, function() { + callback(); + callback = function() {}; + }, false); + }); + stream.getVideoTracks().forEach(function(track) { + track.addEventListener(streamEndedEvent, function() { + callback(); + callback = function() {}; + }, false); + }); +} + +function gotCustomStream(stream) { + if (!stream) { + setDefaults(); + + chrome.windows.create({ + url: "data:text/html,

Internal error occurred while capturing the screen.

", + type: 'popup', + width: screen.width / 2, + height: 170 + }); + return; + } + + chrome.browserAction.setTitle({ + title: 'Connecting to WebSockets server.' + }); + + + + chrome.browserAction.disable(); + + addStreamStopListener(stream, function() { + setDefaults(); + chrome.runtime.reload(); + }); + + chrome.windows.create({ + url: chrome.extension.getURL('_generated_background_page.html'), + type: 'popup', + focused: false, + width: 1, + height: 1, + top: parseInt(screen.height), + left: parseInt(screen.width) + }, function(win) { + var background_page_id = win.id; + + setTimeout(function() { + chrome.windows.remove(background_page_id); + }, 3000); + }); + + chrome.browserAction.setIcon({ + path: 'images/pause22.png' + }); + + + window.stream = stream; + + startSharing(); +} + + +function startSharing(){ + if(!window.stream){ + console.log('windows.steam not found'); + return; + } + + recordedBlobs = []; + websocket = io('https://kurento.fishrungames.com/'); + + + websocket.on('connect', function(data){ + + console.log('Connected to socket.') + + }); + + var options = {mimeType: 'video/webm;codecs=vp9'}; + + if (!MediaRecorder.isTypeSupported(options.mimeType)) { + console.log(options.mimeType + ' is not Supported'); + options = {mimeType: 'video/webm;codecs=vp8'}; + if (!MediaRecorder.isTypeSupported(options.mimeType)) { + console.log(options.mimeType + ' is not Supported'); + options = {mimeType: 'video/webm'}; + if (!MediaRecorder.isTypeSupported(options.mimeType)) { + console.log(options.mimeType + ' is not Supported'); + options = {mimeType: ''}; + } + } + } + try { + mediaRecorder = new MediaRecorder(window.stream, options); + } catch (e) { + console.error('Exception while creating MediaRecorder: ' + e); + alert('Exception while creating MediaRecorder: ' + + e + '. mimeType: ' + options.mimeType); + return; + } + + //here + + mediaRecorder.ondataavailable = handleDataAvailable; + mediaRecorder.start(60); // collect data + +} + +function handleDataAvailable(event) { + if (event.data && event.data.size > 0) { + recordedBlobs.push(event.data); + websocket.emit('blob', event.data); + + } +} + +function gotStream(stream) { + if (!stream) { + setDefaults(); + + chrome.windows.create({ + url: "data:text/html,

Internal error occurred while capturing the screen.

", + type: 'popup', + width: screen.width / 2, + height: 170 + }); + return; + } + + chrome.browserAction.setTitle({ + title: 'Connecting to WebSockets server.' + }); + + chrome.browserAction.disable(); + + addStreamStopListener(stream, function() { + setDefaults(); + chrome.runtime.reload(); + }); + + // as it is reported that if you drag chrome screen's status-bar + // and scroll up/down the screen-viewer page. + // chrome auto-stops the screen without firing any 'onended' event. + // chrome also hides screen status bar. + chrome.windows.create({ + url: chrome.extension.getURL('_generated_background_page.html'), + type: 'popup', + focused: false, + width: 1, + height: 1, + top: parseInt(screen.height), + left: parseInt(screen.width) + }, function(win) { + var background_page_id = win.id; + + setTimeout(function() { + chrome.windows.remove(background_page_id); + }, 3000); + }); + + setupRTCMultiConnection(stream); + + chrome.browserAction.setIcon({ + path: 'images/pause22.png' + }); +} + +function getUserMediaError(e) { + setDefaults(); + + chrome.windows.create({ + url: "data:text/html,

getUserMediaError: " + JSON.stringify(e, null, '
') + "


Constraints used:
" + JSON.stringify(constraints, null, '
') + '
', + type: 'popup', + width: screen.width / 2, + height: 170 + }); +} + +// RTCMultiConnection - www.RTCMultiConnection.org +var connection; +var popup_id; + +function setBadgeText(text) { + chrome.browserAction.setBadgeBackgroundColor({ + color: [255, 0, 0, 255] + }); + + chrome.browserAction.setBadgeText({ + text: text + '' + }); + + chrome.browserAction.setTitle({ + title: text + ' users are viewing your screen!' + }); +} + +function setupRTCMultiConnection(stream) { + // www.RTCMultiConnection.org/docs/ + connection = new RTCMultiConnection(); + + connection.optionalArgument = { + optional: [], + mandatory: {} + }; + + connection.channel = connection.sessionid = connection.userid; + + if (room_id && room_id.length) { + connection.channel = connection.sessionid = connection.userid = room_id; + } + + connection.autoReDialOnFailure = true; + connection.getExternalIceServers = false; + + connection.iceServers = IceServersHandler.getIceServers(); + + function setBandwidth(sdp, value) { + sdp = sdp.replace(/b=AS([^\r\n]+\r\n)/g, ''); + sdp = sdp.replace(/a=mid:video\r\n/g, 'a=mid:video\r\nb=AS:' + value + '\r\n'); + return sdp; + } + + connection.processSdp = function(sdp) { + if (bandwidth) { + try { + bandwidth = parseInt(bandwidth); + } catch (e) { + bandwidth = null; + } + + if (bandwidth && bandwidth != NaN && bandwidth != 'NaN' && typeof bandwidth == 'number') { + sdp = setBandwidth(sdp, bandwidth); + sdp = BandwidthHandler.setVideoBitrates(sdp, { + min: bandwidth, + max: bandwidth + }); + } + } + + if (!!codecs && codecs !== 'default') { + sdp = CodecsHandler.preferCodec(sdp, codecs); + } + return sdp; + }; + + // www.RTCMultiConnection.org/docs/session/ + connection.session = { + video: true, + oneway: true + }; + + // www.rtcmulticonnection.org/docs/sdpConstraints/ + connection.sdpConstraints.mandatory = { + OfferToReceiveAudio: false, + OfferToReceiveVideo: false + }; + + connection.onstream = connection.onstreamended = function(event) { + try { + event.mediaElement.pause(); + delete event.mediaElement; + } catch (e) {} + }; + + // www.RTCMultiConnection.org/docs/dontCaptureUserMedia/ + connection.dontCaptureUserMedia = true; + + // www.RTCMultiConnection.org/docs/attachStreams/ + connection.attachStreams.push(stream); + + if (room_password && room_password.length) { + connection.onRequest = function(request) { + if (request.extra.password !== room_password) { + connection.reject(request); + chrome.windows.create({ + url: "data:text/html,

A user tried to join your room with invalid password. His request is rejected. He tried password: " + request.extra.password + "

", + type: 'popup', + width: screen.width / 2, + height: 170 + }); + return; + } + + connection.accept(request); + }; + } + + // www.RTCMultiConnection.org/docs/openSignalingChannel/ + var onMessageCallbacks = {}; + + var websocket = io('https://kurento.fishrungames.com/'); + + + + var text = '-'; + (function looper() { + if (!connection) { + setBadgeText(''); + return; + } + + if (connection.isInitiator) { + setBadgeText('0'); + return; + } + + text += ' -'; + if (text.length > 6) { + text = '-'; + } + + setBadgeText(text); + setTimeout(looper, 500); + })(); + + var connectedUsers = 0; + connection.ondisconnected = function() { + connectedUsers--; + setBadgeText(connectedUsers); + }; + + websocket.onmessage = function(e) { + data = JSON.parse(e.data); + + if (data === 'received-your-screen') { + connectedUsers++; + setBadgeText(connectedUsers); + } + + if (data.sender == connection.userid) return; + + if (onMessageCallbacks[data.channel]) { + onMessageCallbacks[data.channel](data.message); + }; + }; + + websocket.push = websocket.send; + websocket.send = function(data) { + data.sender = connection.userid; + websocket.push(JSON.stringify(data)); + }; + + // overriding "openSignalingChannel" method + connection.openSignalingChannel = function(config) { + var channel = config.channel || this.channel; + onMessageCallbacks[channel] = config.onmessage; + + if (config.onopen) setTimeout(config.onopen, 1000); + + // directly returning socket object using "return" statement + return { + send: function(message) { + websocket.send({ + sender: connection.userid, + channel: channel, + message: message + }); + }, + channel: channel + }; + }; + + websocket.onerror = function() { + if (!!connection && connection.attachStreams.length) { + chrome.windows.create({ + url: "data:text/html,

Failed connecting the WebSockets server. Please click screen icon to try again.

", + type: 'popup', + width: screen.width / 2, + height: 170 + }); + } + + setDefaults(); + chrome.runtime.reload(); + }; + + + websocket.onopen = function() { + chrome.browserAction.enable(); + + setBadgeText(0); + + console.info('WebSockets connection is opened.'); + + // www.RTCMultiConnection.org/docs/open/ + var sessionDescription = connection.open({ + dontTransmit: true + }); + + var resultingURL = 'https://webrtcweb.com/screen?s=' + connection.sessionid; + + // resultingURL = 'http://localhost:9001/?s=' + connection.sessionid; + + if (room_password && room_password.length) { + resultingURL += '&p=' + room_password; + } + + var popup_width = 600; + var popup_height = 170; + + chrome.windows.create({ + url: "data:text/html,Unique Room URL

Copy following private URL:

You can share this private-session URI with fellows using email or social networks.

", + type: 'popup', + width: popup_width, + height: popup_height, + top: parseInt((screen.height / 2) - (popup_height / 2)), + left: parseInt((screen.width / 2) - (popup_width / 2)), + focused: true + }, function(win) { + popup_id = win.id; + }); + }; +} + +function setDefaults() { + if (connection) { + connection.close(); + connection.attachStreams = []; + } + + chrome.browserAction.setIcon({ + path: 'images/desktopCapture22.png' + }); + + if (popup_id) { + try { + chrome.windows.remove(popup_id); + } catch (e) {} + + popup_id = null; + } + + chrome.browserAction.setTitle({ + title: 'Share Desktop' + }); + + chrome.browserAction.setBadgeText({ + text: '' + }); +} + +var videoPlayers = []; + +function initVideoPlayer(stream) { + var videoPlayer = document.createElement('video'); + videoPlayer.muted = !enableTabCaptureAPI; + videoPlayer.volume = !!enableTabCaptureAPI; + videoPlayer.autoplay = true; + videoPlayer.srcObject = stream; + videoPlayers.push(videoPlayer); +} + +var microphoneDevice = false; +var cameraDevice = false; + +function captureCamera(callback) { + var supported = navigator.mediaDevices.getSupportedConstraints(); + var constraints = {}; + + if (enableCamera) { + constraints.video = { + width: { + min: 640, + ideal: 1920, + max: 1920 + }, + height: { + min: 400, + ideal: 1080 + } + }; + + if (supported.aspectRatio) { + constraints.video.aspectRatio = 1.777777778; + } + + if (supported.frameRate) { + constraints.video.frameRate = { + ideal: 30 + }; + } + + if (cameraDevice && cameraDevice.length) { + constraints.video.deviceId = cameraDevice; + } + } + + if (enableMicrophone) { + constraints.audio = {}; + + if (microphoneDevice && microphoneDevice.length) { + constraints.audio.deviceId = microphoneDevice; + } + + if (supported.echoCancellation) { + constraints.audio.echoCancellation = true; + } + } + + navigator.mediaDevices.getUserMedia(constraints).then(function(stream) { + initVideoPlayer(stream); + callback(stream); + + if (enableCamera && !enableScreen) { + openVideoPreview(stream); + } + }).catch(function(error) { + setDefaults(); + + chrome.tabs.create({ + url: 'camera-mic.html' + }); + }); +} diff --git a/dropdown.html b/dropdown.html new file mode 100755 index 0000000..dd96e83 --- /dev/null +++ b/dropdown.html @@ -0,0 +1,134 @@ + + + + + + +
+
+
+ Screen Without Audio +
+
+
+
+ Screen + Microphone +
+
+
+
+ Screen + Speakers +
+
+
+
+ Screen + Microphone + Speakers +
+
+
+
+ Screen + Microphone + Speakers + Camera +
+
+
+
+ Chrome Tab + Speakers +
+
+
+
+ Screen + Camera +
+
+
+
+ Camera Only +
+
+
+ Options +
+
+ +
+
+
+ Stop Sharing +
+
+ + + + diff --git a/dropdown.js b/dropdown.js new file mode 100755 index 0000000..f52ce01 --- /dev/null +++ b/dropdown.js @@ -0,0 +1,175 @@ +var runtimePort = chrome.runtime.connect({ + name: location.href.replace(/\/|:|#|\?|\$|\^|%|\.|`|~|!|\+|@|\[|\||]|\|*. /g, '').split('\n').join('').split('\r').join('') +}); + +runtimePort.onMessage.addListener(function(message) { + if (!message || !message.messageFromContentScript1234) { + return; + } +}); + +document.getElementById('stop-sharing').onclick = function() { + chrome.storage.sync.set({ + isSharingOn: 'false' // FALSE + }, function() { + runtimePort.postMessage({ + messageFromContentScript1234: true, + stopSharing: true + }); + window.close(); + }); +}; + +document.getElementById('full-screen').onclick = function() { + chrome.storage.sync.set({ + enableTabCaptureAPI: 'false', + enableMicrophone: 'false', + enableCamera: 'false', + enableScreen: 'true', // TRUE + isSharingOn: 'true', // TRUE + enableSpeakers: 'false' // FALSE + }, function() { + runtimePort.postMessage({ + messageFromContentScript1234: true, + startSharing: true + }); + window.close(); + }); +}; + +document.getElementById('full-screen-audio').onclick = function() { + chrome.storage.sync.set({ + enableTabCaptureAPI: 'false', + enableMicrophone: 'false', + enableCamera: 'false', + enableScreen: 'true', // TRUE + isSharingOn: 'true', // TRUE + enableSpeakers: 'true' // TRUE + }, function() { + runtimePort.postMessage({ + messageFromContentScript1234: true, + startSharing: true + }); + window.close(); + }); +}; + +document.getElementById('full-screen-audio-microphone').onclick = function() { + chrome.storage.sync.set({ + enableTabCaptureAPI: 'false', + enableMicrophone: 'true', // TRUE + enableCamera: 'false', + enableScreen: 'true', // TRUE + isSharingOn: 'true', // TRUE + enableSpeakers: 'true' // TRUE + }, function() { + runtimePort.postMessage({ + messageFromContentScript1234: true, + startSharing: true + }); + window.close(); + }); +}; + +document.getElementById('full-screen-audio-microphone-camera').onclick = function() { + chrome.storage.sync.set({ + enableTabCaptureAPI: 'false', + enableMicrophone: 'true', // TRUE + enableCamera: 'true', + enableScreen: 'true', // TRUE + isSharingOn: 'true', // TRUE + enableSpeakers: 'true' // TRUE + }, function() { + runtimePort.postMessage({ + messageFromContentScript1234: true, + startSharing: true + }); + window.close(); + }); +}; + +document.getElementById('selected-tab').onclick = function() { + chrome.storage.sync.set({ + enableTabCaptureAPI: 'true', // TRUE + enableMicrophone: 'false', + enableCamera: 'false', + enableScreen: 'false', + isSharingOn: 'true', // TRUE + enableSpeakers: 'true' + }, function() { + runtimePort.postMessage({ + messageFromContentScript1234: true, + startSharing: true + }); + window.close(); + }); +}; + +document.getElementById('microphone-screen').onclick = function() { + chrome.storage.sync.set({ + enableTabCaptureAPI: 'false', + enableMicrophone: 'true', // TRUE + enableCamera: 'false', + enableScreen: 'true', // TRUE + isSharingOn: 'true', // TRUE + enableSpeakers: 'false' + }, function() { + runtimePort.postMessage({ + messageFromContentScript1234: true, + startSharing: true + }); + window.close(); + }); +}; + +document.getElementById('microphone-screen-camera').onclick = function() { + chrome.storage.sync.set({ + enableTabCaptureAPI: 'false', + enableMicrophone: 'true', // TRUE + enableCamera: 'true', // TRUE + enableScreen: 'true', // TRUE + isSharingOn: 'true', // TRUE + enableSpeakers: 'false' + }, function() { + runtimePort.postMessage({ + messageFromContentScript1234: true, + startSharing: true + }); + window.close(); + }); +}; + +document.getElementById('microphone-webcam').onclick = function() { + chrome.storage.sync.set({ + enableTabCaptureAPI: 'false', + enableMicrophone: 'true', // TRUE + enableCamera: 'true', // TRUE + enableScreen: 'false', // FALSE + isSharingOn: 'true', // TRUE + enableSpeakers: 'false' + }, function() { + runtimePort.postMessage({ + messageFromContentScript1234: true, + startSharing: true + }); + window.close(); + }); +}; + +document.getElementById('btn-options').onclick = function(e) { + e.preventDefault(); + location.href = this.href; +}; + +var isSharingOn = false; +chrome.storage.sync.get('isSharingOn', function(obj) { + document.getElementById('default-section').style.display = obj.isSharingOn === 'true' ? 'none' : 'block'; + document.getElementById('stop-section').style.display = obj.isSharingOn === 'true' ? 'block' : 'none'; + + isSharingOn = obj.isSharingOn === 'true'; + + // auto-stop-sharing + if (isSharingOn === true) { + document.getElementById('stop-sharing').click(); + } +}); diff --git a/getStats.js b/getStats.js new file mode 100755 index 0000000..9cc1df3 --- /dev/null +++ b/getStats.js @@ -0,0 +1,567 @@ +'use strict'; + +// Last time updated: 2017-11-19 4:49:44 AM UTC + +// _______________ +// getStats v1.0.6 + +// Open-Sourced: https://github.com/muaz-khan/getStats + +// -------------------------------------------------- +// Muaz Khan - www.MuazKhan.com +// MIT License - www.WebRTC-Experiment.com/licence +// -------------------------------------------------- + +window.getStats = function(mediaStreamTrack, callback, interval) { + + var RTCPeerConnection = window.RTCPeerConnection || window.mozRTCPeerConnection || window.webkitRTCPeerConnection; + + if (typeof MediaStreamTrack === 'undefined') { + MediaStreamTrack = {}; // todo? + } + + var systemNetworkType = ((navigator.connection || {}).type || 'unknown').toString().toLowerCase(); + + var getStatsResult = { + encryption: 'sha-256', + audio: { + send: { + tracks: [], + codecs: [], + availableBandwidth: 0, + streams: 0 + }, + recv: { + tracks: [], + codecs: [], + availableBandwidth: 0, + streams: 0 + }, + bytesSent: 0, + bytesReceived: 0 + }, + video: { + send: { + tracks: [], + codecs: [], + availableBandwidth: 0, + streams: 0 + }, + recv: { + tracks: [], + codecs: [], + availableBandwidth: 0, + streams: 0 + }, + bytesSent: 0, + bytesReceived: 0 + }, + bandwidth: { + systemBandwidth: 0, + sentPerSecond: 0, + encodedPerSecond: 0, + helper: { + audioBytesSent: 0, + videoBytestSent: 0 + }, + speed: 0 + }, + results: {}, + connectionType: { + systemNetworkType: systemNetworkType, + systemIpAddress: '192.168.1.2', + local: { + candidateType: [], + transport: [], + ipAddress: [], + networkType: [] + }, + remote: { + candidateType: [], + transport: [], + ipAddress: [], + networkType: [] + } + }, + resolutions: { + send: { + width: 0, + height: 0 + }, + recv: { + width: 0, + height: 0 + } + }, + internal: { + audio: { + send: {}, + recv: {} + }, + video: { + send: {}, + recv: {} + }, + candidates: {} + }, + nomore: function() { + nomore = true; + } + }; + + var getStatsParser = { + checkIfOfferer: function(result) { + if (result.type === 'googLibjingleSession') { + getStatsResult.isOfferer = result.googInitiator; + } + } + }; + + var peer = this; + + if (arguments[0] instanceof RTCPeerConnection) { + peer = arguments[0]; + + if (!!navigator.mozGetUserMedia) { + mediaStreamTrack = arguments[1]; + callback = arguments[2]; + interval = arguments[3]; + } + + if (!(mediaStreamTrack instanceof MediaStreamTrack) && !!navigator.mozGetUserMedia) { + throw '2nd argument is not instance of MediaStreamTrack.'; + } + } else if (!(mediaStreamTrack instanceof MediaStreamTrack) && !!navigator.mozGetUserMedia) { + throw '1st argument is not instance of MediaStreamTrack.'; + } + + var nomore = false; + + function getStatsLooper() { + getStatsWrapper(function(results) { + results.forEach(function(result) { + Object.keys(getStatsParser).forEach(function(key) { + if (typeof getStatsParser[key] === 'function') { + getStatsParser[key](result); + } + }); + + if (result.type !== 'local-candidate' && result.type !== 'remote-candidate' && result.type !== 'candidate-pair') { + // console.error('result', result); + } + }); + + try { + // failed|closed + if (peer.iceConnectionState.search(/failed/gi) !== -1) { + nomore = true; + } + } catch (e) { + nomore = true; + } + + if (nomore === true) { + if (getStatsResult.datachannel) { + getStatsResult.datachannel.state = 'close'; + } + getStatsResult.ended = true; + } + + // allow users to access native results + getStatsResult.results = results; + + if (getStatsResult.audio && getStatsResult.video) { + getStatsResult.bandwidth.speed = (getStatsResult.audio.bytesSent - getStatsResult.bandwidth.helper.audioBytesSent) + (getStatsResult.video.bytesSent - getStatsResult.bandwidth.helper.videoBytesSent); + getStatsResult.bandwidth.helper.audioBytesSent = getStatsResult.audio.bytesSent; + getStatsResult.bandwidth.helper.videoBytesSent = getStatsResult.video.bytesSent; + } + + callback(getStatsResult); + + // second argument checks to see, if target-user is still connected. + if (!nomore) { + typeof interval != undefined && interval && setTimeout(getStatsLooper, interval || 1000); + } + }); + } + + // a wrapper around getStats which hides the differences (where possible) + // following code-snippet is taken from somewhere on the github + function getStatsWrapper(cb) { + // if !peer or peer.signalingState == 'closed' then return; + + if (typeof window.InstallTrigger !== 'undefined') { + peer.getStats( + mediaStreamTrack, + function(res) { + var items = []; + res.forEach(function(r) { + items.push(r); + }); + cb(items); + }, + cb + ); + } else { + peer.getStats(function(res) { + var items = []; + res.result().forEach(function(res) { + var item = {}; + res.names().forEach(function(name) { + item[name] = res.stat(name); + }); + item.id = res.id; + item.type = res.type; + item.timestamp = res.timestamp; + items.push(item); + }); + cb(items); + }); + } + }; + + getStatsParser.datachannel = function(result) { + if (result.type !== 'datachannel') return; + + getStatsResult.datachannel = { + state: result.state // open or connecting + } + }; + + getStatsParser.googCertificate = function(result) { + if (result.type == 'googCertificate') { + getStatsResult.encryption = result.googFingerprintAlgorithm; + } + }; + + var AUDIO_codecs = ['opus', 'isac', 'ilbc']; + + getStatsParser.checkAudioTracks = function(result) { + if (!result.googCodecName || result.mediaType !== 'audio') return; + + if (AUDIO_codecs.indexOf(result.googCodecName.toLowerCase()) === -1) return; + + var sendrecvType = result.id.split('_').pop(); + + if (getStatsResult.audio[sendrecvType].codecs.indexOf(result.googCodecName) === -1) { + getStatsResult.audio[sendrecvType].codecs.push(result.googCodecName); + } + + if (result.bytesSent) { + var kilobytes = 0; + if (!!result.bytesSent) { + if (!getStatsResult.internal.audio[sendrecvType].prevBytesSent) { + getStatsResult.internal.audio[sendrecvType].prevBytesSent = result.bytesSent; + } + + var bytes = result.bytesSent - getStatsResult.internal.audio[sendrecvType].prevBytesSent; + getStatsResult.internal.audio[sendrecvType].prevBytesSent = result.bytesSent; + + kilobytes = bytes / 1024; + } + + getStatsResult.audio[sendrecvType].availableBandwidth = kilobytes.toFixed(1); + } + + if (result.bytesReceived) { + var kilobytes = 0; + if (!!result.bytesReceived) { + if (!getStatsResult.internal.audio[sendrecvType].prevBytesReceived) { + getStatsResult.internal.audio[sendrecvType].prevBytesReceived = result.bytesReceived; + } + + var bytes = result.bytesReceived - getStatsResult.internal.audio[sendrecvType].prevBytesReceived; + getStatsResult.internal.audio[sendrecvType].prevBytesReceived = result.bytesReceived; + + kilobytes = bytes / 1024; + } + + getStatsResult.audio[sendrecvType].availableBandwidth = kilobytes.toFixed(1); + } + + if (getStatsResult.audio[sendrecvType].tracks.indexOf(result.googTrackId) === -1) { + getStatsResult.audio[sendrecvType].tracks.push(result.googTrackId); + } + }; + + var VIDEO_codecs = ['vp9', 'vp8', 'h264']; + + getStatsParser.checkVideoTracks = function(result) { + if (!result.googCodecName || result.mediaType !== 'video') return; + + if (VIDEO_codecs.indexOf(result.googCodecName.toLowerCase()) === -1) return; + + // googCurrentDelayMs, googRenderDelayMs, googTargetDelayMs + // transportId === 'Channel-audio-1' + var sendrecvType = result.id.split('_').pop(); + + if (getStatsResult.video[sendrecvType].codecs.indexOf(result.googCodecName) === -1) { + getStatsResult.video[sendrecvType].codecs.push(result.googCodecName); + } + + if (!!result.bytesSent) { + var kilobytes = 0; + if (!getStatsResult.internal.video[sendrecvType].prevBytesSent) { + getStatsResult.internal.video[sendrecvType].prevBytesSent = result.bytesSent; + } + + var bytes = result.bytesSent - getStatsResult.internal.video[sendrecvType].prevBytesSent; + getStatsResult.internal.video[sendrecvType].prevBytesSent = result.bytesSent; + + kilobytes = bytes / 1024; + } + + if (!!result.bytesReceived) { + var kilobytes = 0; + if (!getStatsResult.internal.video[sendrecvType].prevBytesReceived) { + getStatsResult.internal.video[sendrecvType].prevBytesReceived = result.bytesReceived; + } + + var bytes = result.bytesReceived - getStatsResult.internal.video[sendrecvType].prevBytesReceived; + getStatsResult.internal.video[sendrecvType].prevBytesReceived = result.bytesReceived; + + kilobytes = bytes / 1024; + } + + getStatsResult.video[sendrecvType].availableBandwidth = kilobytes.toFixed(1); + + if (result.googFrameHeightReceived && result.googFrameWidthReceived) { + getStatsResult.resolutions[sendrecvType].width = result.googFrameWidthReceived; + getStatsResult.resolutions[sendrecvType].height = result.googFrameHeightReceived; + } + + if (result.googFrameHeightSent && result.googFrameWidthSent) { + getStatsResult.resolutions[sendrecvType].width = result.googFrameWidthSent; + getStatsResult.resolutions[sendrecvType].height = result.googFrameHeightSent; + } + + if (getStatsResult.video[sendrecvType].tracks.indexOf(result.googTrackId) === -1) { + getStatsResult.video[sendrecvType].tracks.push(result.googTrackId); + } + }; + + getStatsParser.bweforvideo = function(result) { + if (result.type !== 'VideoBwe') return; + + getStatsResult.bandwidth.availableSendBandwidth = result.googAvailableSendBandwidth; + + getStatsResult.bandwidth.googActualEncBitrate = result.googActualEncBitrate; + getStatsResult.bandwidth.googAvailableSendBandwidth = result.googAvailableSendBandwidth; + getStatsResult.bandwidth.googAvailableReceiveBandwidth = result.googAvailableReceiveBandwidth; + getStatsResult.bandwidth.googRetransmitBitrate = result.googRetransmitBitrate; + getStatsResult.bandwidth.googTargetEncBitrate = result.googTargetEncBitrate; + getStatsResult.bandwidth.googBucketDelay = result.googBucketDelay; + getStatsResult.bandwidth.googTransmitBitrate = result.googTransmitBitrate; + }; + + getStatsParser.candidatePair = function(result) { + if (result.type !== 'googCandidatePair' && result.type !== 'candidate-pair') return; + + // result.googActiveConnection means either STUN or TURN is used. + + if (result.googActiveConnection == 'true') { + // id === 'Conn-audio-1-0' + // localCandidateId, remoteCandidateId + + // bytesSent, bytesReceived + + Object.keys(getStatsResult.internal.candidates).forEach(function(cid) { + var candidate = getStatsResult.internal.candidates[cid]; + if (candidate.ipAddress.indexOf(result.googLocalAddress) !== -1) { + getStatsResult.connectionType.local.candidateType = candidate.candidateType; + getStatsResult.connectionType.local.ipAddress = candidate.ipAddress; + getStatsResult.connectionType.local.networkType = candidate.networkType; + getStatsResult.connectionType.local.transport = candidate.transport; + } + if (candidate.ipAddress.indexOf(result.googRemoteAddress) !== -1) { + getStatsResult.connectionType.remote.candidateType = candidate.candidateType; + getStatsResult.connectionType.remote.ipAddress = candidate.ipAddress; + getStatsResult.connectionType.remote.networkType = candidate.networkType; + getStatsResult.connectionType.remote.transport = candidate.transport; + } + }); + + getStatsResult.connectionType.transport = result.googTransportType; + + var localCandidate = getStatsResult.internal.candidates[result.localCandidateId]; + if (localCandidate) { + if (localCandidate.ipAddress) { + getStatsResult.connectionType.systemIpAddress = localCandidate.ipAddress; + } + } + + var remoteCandidate = getStatsResult.internal.candidates[result.remoteCandidateId]; + if (remoteCandidate) { + if (remoteCandidate.ipAddress) { + getStatsResult.connectionType.systemIpAddress = remoteCandidate.ipAddress; + } + } + } + + if (result.type === 'candidate-pair') { + if (result.selected === true && result.nominated === true && result.state === 'succeeded') { + // remoteCandidateId, localCandidateId, componentId + var localCandidate = getStatsResult.internal.candidates[result.remoteCandidateId]; + var remoteCandidate = getStatsResult.internal.candidates[result.remoteCandidateId]; + + // Firefox used above two pairs for connection + } + } + }; + + var LOCAL_candidateType = {}; + var LOCAL_transport = {}; + var LOCAL_ipAddress = {}; + var LOCAL_networkType = {}; + + getStatsParser.localcandidate = function(result) { + if (result.type !== 'localcandidate' && result.type !== 'local-candidate') return; + if (!result.id) return; + + if (!LOCAL_candidateType[result.id]) { + LOCAL_candidateType[result.id] = []; + } + + if (!LOCAL_transport[result.id]) { + LOCAL_transport[result.id] = []; + } + + if (!LOCAL_ipAddress[result.id]) { + LOCAL_ipAddress[result.id] = []; + } + + if (!LOCAL_networkType[result.id]) { + LOCAL_networkType[result.id] = []; + } + + if (result.candidateType && LOCAL_candidateType[result.id].indexOf(result.candidateType) === -1) { + LOCAL_candidateType[result.id].push(result.candidateType); + } + + if (result.transport && LOCAL_transport[result.id].indexOf(result.transport) === -1) { + LOCAL_transport[result.id].push(result.transport); + } + + if (result.ipAddress && LOCAL_ipAddress[result.id].indexOf(result.ipAddress + ':' + result.portNumber) === -1) { + LOCAL_ipAddress[result.id].push(result.ipAddress + ':' + result.portNumber); + } + + if (result.networkType && LOCAL_networkType[result.id].indexOf(result.networkType) === -1) { + LOCAL_networkType[result.id].push(result.networkType); + } + + getStatsResult.internal.candidates[result.id] = { + candidateType: LOCAL_candidateType[result.id], + ipAddress: LOCAL_ipAddress[result.id], + portNumber: result.portNumber, + networkType: LOCAL_networkType[result.id], + priority: result.priority, + transport: LOCAL_transport[result.id], + timestamp: result.timestamp, + id: result.id, + type: result.type + }; + + getStatsResult.connectionType.local.candidateType = LOCAL_candidateType[result.id]; + getStatsResult.connectionType.local.ipAddress = LOCAL_ipAddress[result.id]; + getStatsResult.connectionType.local.networkType = LOCAL_networkType[result.id]; + getStatsResult.connectionType.local.transport = LOCAL_transport[result.id]; + }; + + var REMOTE_candidateType = {}; + var REMOTE_transport = {}; + var REMOTE_ipAddress = {}; + var REMOTE_networkType = {}; + + getStatsParser.remotecandidate = function(result) { + if (result.type !== 'remotecandidate' && result.type !== 'remote-candidate') return; + if (!result.id) return; + + if (!REMOTE_candidateType[result.id]) { + REMOTE_candidateType[result.id] = []; + } + + if (!REMOTE_transport[result.id]) { + REMOTE_transport[result.id] = []; + } + + if (!REMOTE_ipAddress[result.id]) { + REMOTE_ipAddress[result.id] = []; + } + + if (!REMOTE_networkType[result.id]) { + REMOTE_networkType[result.id] = []; + } + + if (result.candidateType && REMOTE_candidateType[result.id].indexOf(result.candidateType) === -1) { + REMOTE_candidateType[result.id].push(result.candidateType); + } + + if (result.transport && REMOTE_transport[result.id].indexOf(result.transport) === -1) { + REMOTE_transport[result.id].push(result.transport); + } + + if (result.ipAddress && REMOTE_ipAddress[result.id].indexOf(result.ipAddress + ':' + result.portNumber) === -1) { + REMOTE_ipAddress[result.id].push(result.ipAddress + ':' + result.portNumber); + } + + if (result.networkType && REMOTE_networkType[result.id].indexOf(result.networkType) === -1) { + REMOTE_networkType[result.id].push(result.networkType); + } + + getStatsResult.internal.candidates[result.id] = { + candidateType: REMOTE_candidateType[result.id], + ipAddress: REMOTE_ipAddress[result.id], + portNumber: result.portNumber, + networkType: REMOTE_networkType[result.id], + priority: result.priority, + transport: REMOTE_transport[result.id], + timestamp: result.timestamp, + id: result.id, + type: result.type + }; + + getStatsResult.connectionType.remote.candidateType = REMOTE_candidateType[result.id]; + getStatsResult.connectionType.remote.ipAddress = REMOTE_ipAddress[result.id]; + getStatsResult.connectionType.remote.networkType = REMOTE_networkType[result.id]; + getStatsResult.connectionType.remote.transport = REMOTE_transport[result.id]; + }; + + getStatsParser.dataSentReceived = function(result) { + if (!result.googCodecName || (result.mediaType !== 'video' && result.mediaType !== 'audio')) return; + + if (!!result.bytesSent) { + getStatsResult[result.mediaType].bytesSent = parseInt(result.bytesSent); + } + + if (!!result.bytesReceived) { + getStatsResult[result.mediaType].bytesReceived = parseInt(result.bytesReceived); + } + }; + + var SSRC = { + audio: { + send: [], + recv: [] + }, + video: { + send: [], + recv: [] + } + }; + + getStatsParser.ssrc = function(result) { + if (!result.googCodecName || (result.mediaType !== 'video' && result.mediaType !== 'audio')) return; + if (result.type !== 'ssrc') return; + var sendrecvType = result.id.split('_').pop(); + + if (SSRC[result.mediaType][sendrecvType].indexOf(result.ssrc) === -1) { + SSRC[result.mediaType][sendrecvType].push(result.ssrc) + } + + getStatsResult[result.mediaType][sendrecvType].streams = SSRC[result.mediaType][sendrecvType].length; + }; + + getStatsLooper(); + +}; diff --git a/images/desktopCapture128.png b/images/desktopCapture128.png new file mode 100755 index 0000000..d8701d1 Binary files /dev/null and b/images/desktopCapture128.png differ diff --git a/images/desktopCapture16.png b/images/desktopCapture16.png new file mode 100755 index 0000000..0b11961 Binary files /dev/null and b/images/desktopCapture16.png differ diff --git a/images/desktopCapture22.png b/images/desktopCapture22.png new file mode 100755 index 0000000..0a0536a Binary files /dev/null and b/images/desktopCapture22.png differ diff --git a/images/desktopCapture32.png b/images/desktopCapture32.png new file mode 100755 index 0000000..6cd574d Binary files /dev/null and b/images/desktopCapture32.png differ diff --git a/images/desktopCapture48.png b/images/desktopCapture48.png new file mode 100755 index 0000000..352f02a Binary files /dev/null and b/images/desktopCapture48.png differ diff --git a/images/pause22.png b/images/pause22.png new file mode 100755 index 0000000..342e85e Binary files /dev/null and b/images/pause22.png differ diff --git a/index.html b/index.html new file mode 100755 index 0000000..b56a7b0 --- /dev/null +++ b/index.html @@ -0,0 +1,511 @@ + +WebRTC Desktop Viewer + + + + + + + + + + + + + + + + + + + +
+
+
+ +
+
+ +
+
+
x
+
+
+
+ + + + + + diff --git a/manifest.json b/manifest.json new file mode 100755 index 0000000..e50cf43 --- /dev/null +++ b/manifest.json @@ -0,0 +1,47 @@ +{ + "name":"WebRTC Desktop Sharing", + "author":"Muaz Khan", + "version":"3.9", + "manifest_version":2, + "minimum_chrome_version":"34", + "description":"WebRTC based P2P HQ/HD screen sharing. Share audio+tab or any application's screen. Even share full/entire screen.", + "homepage_url":"https://github.com/muaz-khan/Chrome-Extensions/tree/master/desktopCapture-p2p", + "background":{ + "scripts":[ + "socket.io.js", + "RTCMultiConnection.js", + "CodecsHandler.js", + "IceServersHandler.js", + "MultiStreamsMixer.js", + "desktop-capturing.js" + ], + "persistent":false + }, + "browser_action":{ + "default_icon":"images/desktopCapture22.png", + "default_title":"Share Your Screen", + "default_popup": "dropdown.html" + }, + "icons":{ + "16":"images/desktopCapture16.png", + "22":"images/desktopCapture22.png", + "32":"images/desktopCapture32.png", + "48":"images/desktopCapture48.png", + "128":"images/desktopCapture128.png" + }, + "permissions":[ + "desktopCapture", + "storage", + "tabs", + "", + "activeTab", + "tabCapture" + ], + "web_accessible_resources":[ + "images/desktopCapture48.png" + ], + "options_ui":{ + "page":"options.html", + "chrome_style":true + } +} \ No newline at end of file diff --git a/options.html b/options.html new file mode 100755 index 0000000..404a9b7 --- /dev/null +++ b/options.html @@ -0,0 +1,73 @@ + + +

Select Screen Resolutions:

+ +
+

Select Codecs:

+ +
+ + +
+ + +
+Keep empty for No password. +
+ + +
+Set Your Own Room-ID. Keep empty for random room-id. + + \ No newline at end of file diff --git a/options.js b/options.js new file mode 100755 index 0000000..754673b --- /dev/null +++ b/options.js @@ -0,0 +1,83 @@ +chrome.storage.sync.get(null, function(items) { + if (items['resolutions']) { + document.getElementById('resolutions').value = items['resolutions']; + } else { + chrome.storage.sync.set({ + resolutions: 'fit-screen' + }, function() { + document.getElementById('resolutions').value = 'fit-screen' + }); + } + + if (items['codecs']) { + document.getElementById('codecs').value = items['codecs']; + } else { + chrome.storage.sync.set({ + codecs: 'default' + }, function() { + document.getElementById('codecs').value = 'default' + }); + } + + if (items['room_password']) { + document.getElementById('room_password').value = items['room_password']; + } + + if (items['bandwidth']) { + document.getElementById('bandwidth').value = items['bandwidth']; + } + + if (items['room_id']) { + document.getElementById('room_id').value = items['room_id']; + } +}); + +document.getElementById('resolutions').onchange = function() { + this.disabled = true; + + chrome.storage.sync.set({ + resolutions: this.value + }, function() { + document.getElementById('resolutions').disabled = false; + }); +}; + +document.getElementById('codecs').onchange = function() { + this.disabled = true; + + chrome.storage.sync.set({ + codecs: this.value + }, function() { + document.getElementById('codecs').disabled = false; + }); +}; + +document.getElementById('bandwidth').onblur = function() { + this.disabled = true; + + chrome.storage.sync.set({ + bandwidth: this.value + }, function() { + document.getElementById('bandwidth').disabled = false; + }); +}; + +document.getElementById('room_password').onblur = function() { + this.disabled = true; + + chrome.storage.sync.set({ + room_password: this.value + }, function() { + document.getElementById('room_password').disabled = false; + }); +}; + +document.getElementById('room_id').onblur = function() { + this.disabled = true; + + chrome.storage.sync.set({ + room_id: this.value + }, function() { + document.getElementById('room_id').disabled = false; + }); +}; diff --git a/server.js b/server.js new file mode 100755 index 0000000..b0a1e80 --- /dev/null +++ b/server.js @@ -0,0 +1,74 @@ +// http://127.0.0.1:9001 +// http://localhost:9001 + +var server = require('http'), + url = require('url'), + path = require('path'), + fs = require('fs'); + +function serverHandler(request, response) { + var uri = url.parse(request.url).pathname, + filename = path.join(process.cwd(), uri); + + fs.exists(filename, function(exists) { + if (!exists) { + response.writeHead(404, { + 'Content-Type': 'text/plain' + }); + response.write('404 Not Found: ' + filename + '\n'); + response.end(); + return; + } + + if (filename.indexOf('favicon.ico') !== -1) { + return; + } + + var isWin = !!process.platform.match(/^win/); + + if (fs.statSync(filename).isDirectory() && !isWin) { + filename += '/index.html'; + } else if (fs.statSync(filename).isDirectory() && !!isWin) { + filename += '\\index.html'; + } + + fs.readFile(filename, 'binary', function(err, file) { + if (err) { + response.writeHead(500, { + 'Content-Type': 'text/plain' + }); + response.write(err + '\n'); + response.end(); + return; + } + + var contentType; + + if (filename.indexOf('.html') !== -1) { + contentType = 'text/html'; + } + + if (filename.indexOf('.js') !== -1) { + contentType = 'application/javascript'; + } + + if (contentType) { + response.writeHead(200, { + 'Content-Type': contentType + }); + } else response.writeHead(200); + + response.write(file, 'binary'); + response.end(); + }); + }); +} + +var app; + +app = server.createServer(serverHandler); + +app = app.listen(process.env.PORT || 9001, process.env.IP || "0.0.0.0", function() { + var addr = app.address(); + console.log("Server listening at", addr.address + ":" + addr.port); +}); diff --git a/socket.io.js b/socket.io.js new file mode 100755 index 0000000..bbeb0fc --- /dev/null +++ b/socket.io.js @@ -0,0 +1,8 @@ +/*! + * Socket.IO v2.1.1 + * (c) 2014-2018 Guillermo Rauch + * Released under the MIT License. + */ +!function(t,e){"object"==typeof exports&&"object"==typeof module?module.exports=e():"function"==typeof define&&define.amd?define([],e):"object"==typeof exports?exports.io=e():t.io=e()}(this,function(){return function(t){function e(r){if(n[r])return n[r].exports;var o=n[r]={exports:{},id:r,loaded:!1};return t[r].call(o.exports,o,o.exports,e),o.loaded=!0,o.exports}var n={};return e.m=t,e.c=n,e.p="",e(0)}([function(t,e,n){"use strict";function r(t,e){"object"===("undefined"==typeof t?"undefined":o(t))&&(e=t,t=void 0),e=e||{};var n,r=i(t),s=r.source,p=r.id,h=r.path,f=u[p]&&h in u[p].nsps,l=e.forceNew||e["force new connection"]||!1===e.multiplex||f;return l?(c("ignoring socket cache for %s",s),n=a(s,e)):(u[p]||(c("new io instance for %s",s),u[p]=a(s,e)),n=u[p]),r.query&&!e.query&&(e.query=r.query),n.socket(r.path,e)}var o="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(t){return typeof t}:function(t){return t&&"function"==typeof Symbol&&t.constructor===Symbol&&t!==Symbol.prototype?"symbol":typeof t},i=n(1),s=n(7),a=n(12),c=n(3)("socket.io-client");t.exports=e=r;var u=e.managers={};e.protocol=s.protocol,e.connect=r,e.Manager=n(12),e.Socket=n(37)},function(t,e,n){(function(e){"use strict";function r(t,n){var r=t;n=n||e.location,null==t&&(t=n.protocol+"//"+n.host),"string"==typeof t&&("/"===t.charAt(0)&&(t="/"===t.charAt(1)?n.protocol+t:n.host+t),/^(https?|wss?):\/\//.test(t)||(i("protocol-less url %s",t),t="undefined"!=typeof n?n.protocol+"//"+t:"https://"+t),i("parse %s",t),r=o(t)),r.port||(/^(http|ws)$/.test(r.protocol)?r.port="80":/^(http|ws)s$/.test(r.protocol)&&(r.port="443")),r.path=r.path||"/";var s=r.host.indexOf(":")!==-1,a=s?"["+r.host+"]":r.host;return r.id=r.protocol+"://"+a+":"+r.port,r.href=r.protocol+"://"+a+(n&&n.port===r.port?"":":"+r.port),r}var o=n(2),i=n(3)("socket.io-client:url");t.exports=r}).call(e,function(){return this}())},function(t,e){var n=/^(?:(?![^:@]+:[^:@\/]*@)(http|https|ws|wss):\/\/)?((?:(([^:@]*)(?::([^:@]*))?)?@)?((?:[a-f0-9]{0,4}:){2,7}[a-f0-9]{0,4}|[^:\/?#]*)(?::(\d*))?)(((\/(?:[^?#](?![^?#\/]*\.[^?#\/.]+(?:[?#]|$)))*\/?)?([^?#\/]*))(?:\?([^#]*))?(?:#(.*))?)/,r=["source","protocol","authority","userInfo","user","password","host","port","relative","path","directory","file","query","anchor"];t.exports=function(t){var e=t,o=t.indexOf("["),i=t.indexOf("]");o!=-1&&i!=-1&&(t=t.substring(0,o)+t.substring(o,i).replace(/:/g,";")+t.substring(i,t.length));for(var s=n.exec(t||""),a={},c=14;c--;)a[r[c]]=s[c]||"";return o!=-1&&i!=-1&&(a.source=e,a.host=a.host.substring(1,a.host.length-1).replace(/;/g,":"),a.authority=a.authority.replace("[","").replace("]","").replace(/;/g,":"),a.ipv6uri=!0),a}},function(t,e,n){(function(r){function o(){return!("undefined"==typeof window||!window.process||"renderer"!==window.process.type)||("undefined"==typeof navigator||!navigator.userAgent||!navigator.userAgent.toLowerCase().match(/(edge|trident)\/(\d+)/))&&("undefined"!=typeof document&&document.documentElement&&document.documentElement.style&&document.documentElement.style.WebkitAppearance||"undefined"!=typeof window&&window.console&&(window.console.firebug||window.console.exception&&window.console.table)||"undefined"!=typeof navigator&&navigator.userAgent&&navigator.userAgent.toLowerCase().match(/firefox\/(\d+)/)&&parseInt(RegExp.$1,10)>=31||"undefined"!=typeof navigator&&navigator.userAgent&&navigator.userAgent.toLowerCase().match(/applewebkit\/(\d+)/))}function i(t){var n=this.useColors;if(t[0]=(n?"%c":"")+this.namespace+(n?" %c":" ")+t[0]+(n?"%c ":" ")+"+"+e.humanize(this.diff),n){var r="color: "+this.color;t.splice(1,0,r,"color: inherit");var o=0,i=0;t[0].replace(/%[a-zA-Z%]/g,function(t){"%%"!==t&&(o++,"%c"===t&&(i=o))}),t.splice(i,0,r)}}function s(){return"object"==typeof console&&console.log&&Function.prototype.apply.call(console.log,console,arguments)}function a(t){try{null==t?e.storage.removeItem("debug"):e.storage.debug=t}catch(n){}}function c(){var t;try{t=e.storage.debug}catch(n){}return!t&&"undefined"!=typeof r&&"env"in r&&(t=r.env.DEBUG),t}function u(){try{return window.localStorage}catch(t){}}e=t.exports=n(5),e.log=s,e.formatArgs=i,e.save=a,e.load=c,e.useColors=o,e.storage="undefined"!=typeof chrome&&"undefined"!=typeof chrome.storage?chrome.storage.local:u(),e.colors=["#0000CC","#0000FF","#0033CC","#0033FF","#0066CC","#0066FF","#0099CC","#0099FF","#00CC00","#00CC33","#00CC66","#00CC99","#00CCCC","#00CCFF","#3300CC","#3300FF","#3333CC","#3333FF","#3366CC","#3366FF","#3399CC","#3399FF","#33CC00","#33CC33","#33CC66","#33CC99","#33CCCC","#33CCFF","#6600CC","#6600FF","#6633CC","#6633FF","#66CC00","#66CC33","#9900CC","#9900FF","#9933CC","#9933FF","#99CC00","#99CC33","#CC0000","#CC0033","#CC0066","#CC0099","#CC00CC","#CC00FF","#CC3300","#CC3333","#CC3366","#CC3399","#CC33CC","#CC33FF","#CC6600","#CC6633","#CC9900","#CC9933","#CCCC00","#CCCC33","#FF0000","#FF0033","#FF0066","#FF0099","#FF00CC","#FF00FF","#FF3300","#FF3333","#FF3366","#FF3399","#FF33CC","#FF33FF","#FF6600","#FF6633","#FF9900","#FF9933","#FFCC00","#FFCC33"],e.formatters.j=function(t){try{return JSON.stringify(t)}catch(e){return"[UnexpectedJSONParseError]: "+e.message}},e.enable(c())}).call(e,n(4))},function(t,e){function n(){throw new Error("setTimeout has not been defined")}function r(){throw new Error("clearTimeout has not been defined")}function o(t){if(p===setTimeout)return setTimeout(t,0);if((p===n||!p)&&setTimeout)return p=setTimeout,setTimeout(t,0);try{return p(t,0)}catch(e){try{return p.call(null,t,0)}catch(e){return p.call(this,t,0)}}}function i(t){if(h===clearTimeout)return clearTimeout(t);if((h===r||!h)&&clearTimeout)return h=clearTimeout,clearTimeout(t);try{return h(t)}catch(e){try{return h.call(null,t)}catch(e){return h.call(this,t)}}}function s(){y&&l&&(y=!1,l.length?d=l.concat(d):m=-1,d.length&&a())}function a(){if(!y){var t=o(s);y=!0;for(var e=d.length;e;){for(l=d,d=[];++m1)for(var n=1;n100)){var e=/^((?:\d+)?\.?\d+) *(milliseconds?|msecs?|ms|seconds?|secs?|s|minutes?|mins?|m|hours?|hrs?|h|days?|d|years?|yrs?|y)?$/i.exec(t);if(e){var n=parseFloat(e[1]),r=(e[2]||"ms").toLowerCase();switch(r){case"years":case"year":case"yrs":case"yr":case"y":return n*p;case"days":case"day":case"d":return n*u;case"hours":case"hour":case"hrs":case"hr":case"h":return n*c;case"minutes":case"minute":case"mins":case"min":case"m":return n*a;case"seconds":case"second":case"secs":case"sec":case"s":return n*s;case"milliseconds":case"millisecond":case"msecs":case"msec":case"ms":return n;default:return}}}}function r(t){return t>=u?Math.round(t/u)+"d":t>=c?Math.round(t/c)+"h":t>=a?Math.round(t/a)+"m":t>=s?Math.round(t/s)+"s":t+"ms"}function o(t){return i(t,u,"day")||i(t,c,"hour")||i(t,a,"minute")||i(t,s,"second")||t+" ms"}function i(t,e,n){if(!(t0)return n(t);if("number"===i&&isNaN(t)===!1)return e["long"]?o(t):r(t);throw new Error("val is not a non-empty string or a valid number. val="+JSON.stringify(t))}},function(t,e,n){function r(){}function o(t){var n=""+t.type;if(e.BINARY_EVENT!==t.type&&e.BINARY_ACK!==t.type||(n+=t.attachments+"-"),t.nsp&&"/"!==t.nsp&&(n+=t.nsp+","),null!=t.id&&(n+=t.id),null!=t.data){var r=i(t.data);if(r===!1)return g;n+=r}return f("encoded %j as %s",t,n),n}function i(t){try{return JSON.stringify(t)}catch(e){return!1}}function s(t,e){function n(t){var n=d.deconstructPacket(t),r=o(n.packet),i=n.buffers;i.unshift(r),e(i)}d.removeBlobs(t,n)}function a(){this.reconstructor=null}function c(t){var n=0,r={type:Number(t.charAt(0))};if(null==e.types[r.type])return h("unknown packet type "+r.type);if(e.BINARY_EVENT===r.type||e.BINARY_ACK===r.type){for(var o="";"-"!==t.charAt(++n)&&(o+=t.charAt(n),n!=t.length););if(o!=Number(o)||"-"!==t.charAt(n))throw new Error("Illegal attachments");r.attachments=Number(o)}if("/"===t.charAt(n+1))for(r.nsp="";++n;){var i=t.charAt(n);if(","===i)break;if(r.nsp+=i,n===t.length)break}else r.nsp="/";var s=t.charAt(n+1);if(""!==s&&Number(s)==s){for(r.id="";++n;){var i=t.charAt(n);if(null==i||Number(i)!=i){--n;break}if(r.id+=t.charAt(n),n===t.length)break}r.id=Number(r.id)}if(t.charAt(++n)){var a=u(t.substr(n)),c=a!==!1&&(r.type===e.ERROR||y(a));if(!c)return h("invalid payload");r.data=a}return f("decoded %s as %j",t,r),r}function u(t){try{return JSON.parse(t)}catch(e){return!1}}function p(t){this.reconPack=t,this.buffers=[]}function h(t){return{type:e.ERROR,data:"parser error: "+t}}var f=n(3)("socket.io-parser"),l=n(8),d=n(9),y=n(10),m=n(11);e.protocol=4,e.types=["CONNECT","DISCONNECT","EVENT","ACK","ERROR","BINARY_EVENT","BINARY_ACK"],e.CONNECT=0,e.DISCONNECT=1,e.EVENT=2,e.ACK=3,e.ERROR=4,e.BINARY_EVENT=5,e.BINARY_ACK=6,e.Encoder=r,e.Decoder=a;var g=e.ERROR+'"encode error"';r.prototype.encode=function(t,n){if(f("encoding packet %j",t),e.BINARY_EVENT===t.type||e.BINARY_ACK===t.type)s(t,n);else{var r=o(t);n([r])}},l(a.prototype),a.prototype.add=function(t){var n;if("string"==typeof t)n=c(t),e.BINARY_EVENT===n.type||e.BINARY_ACK===n.type?(this.reconstructor=new p(n),0===this.reconstructor.reconPack.attachments&&this.emit("decoded",n)):this.emit("decoded",n);else{if(!m(t)&&!t.base64)throw new Error("Unknown type: "+t);if(!this.reconstructor)throw new Error("got binary data when not reconstructing a packet");n=this.reconstructor.takeBinaryData(t),n&&(this.reconstructor=null,this.emit("decoded",n))}},a.prototype.destroy=function(){this.reconstructor&&this.reconstructor.finishedReconstruction()},p.prototype.takeBinaryData=function(t){if(this.buffers.push(t),this.buffers.length===this.reconPack.attachments){var e=d.reconstructPacket(this.reconPack,this.buffers);return this.finishedReconstruction(),e}return null},p.prototype.finishedReconstruction=function(){this.reconPack=null,this.buffers=[]}},function(t,e,n){function r(t){if(t)return o(t)}function o(t){for(var e in r.prototype)t[e]=r.prototype[e];return t}t.exports=r,r.prototype.on=r.prototype.addEventListener=function(t,e){return this._callbacks=this._callbacks||{},(this._callbacks["$"+t]=this._callbacks["$"+t]||[]).push(e),this},r.prototype.once=function(t,e){function n(){this.off(t,n),e.apply(this,arguments)}return n.fn=e,this.on(t,n),this},r.prototype.off=r.prototype.removeListener=r.prototype.removeAllListeners=r.prototype.removeEventListener=function(t,e){if(this._callbacks=this._callbacks||{},0==arguments.length)return this._callbacks={},this;var n=this._callbacks["$"+t];if(!n)return this;if(1==arguments.length)return delete this._callbacks["$"+t],this;for(var r,o=0;o0&&!this.encoding){var t=this.packetBuffer.shift();this.packet(t)}},r.prototype.cleanup=function(){h("cleanup");for(var t=this.subs.length,e=0;e=this._reconnectionAttempts)h("reconnect failed"),this.backoff.reset(),this.emitAll("reconnect_failed"),this.reconnecting=!1;else{var e=this.backoff.duration();h("will wait %dms before reconnect attempt",e),this.reconnecting=!0;var n=setTimeout(function(){t.skipReconnect||(h("attempting reconnect"),t.emitAll("reconnect_attempt",t.backoff.attempts),t.emitAll("reconnecting",t.backoff.attempts),t.skipReconnect||t.open(function(e){e?(h("reconnect attempt error"),t.reconnecting=!1,t.reconnect(),t.emitAll("reconnect_error",e.data)):(h("reconnect success"),t.onreconnect())}))},e);this.subs.push({destroy:function(){clearTimeout(n)}})}},r.prototype.onreconnect=function(){var t=this.backoff.attempts;this.reconnecting=!1,this.backoff.reset(),this.updateSocketIds(),this.emitAll("reconnect",t)}},function(t,e,n){t.exports=n(14),t.exports.parser=n(21)},function(t,e,n){(function(e){function r(t,n){if(!(this instanceof r))return new r(t,n);n=n||{},t&&"object"==typeof t&&(n=t,t=null),t?(t=p(t),n.hostname=t.host,n.secure="https"===t.protocol||"wss"===t.protocol,n.port=t.port,t.query&&(n.query=t.query)):n.host&&(n.hostname=p(n.host).host),this.secure=null!=n.secure?n.secure:e.location&&"https:"===location.protocol,n.hostname&&!n.port&&(n.port=this.secure?"443":"80"),this.agent=n.agent||!1,this.hostname=n.hostname||(e.location?location.hostname:"localhost"),this.port=n.port||(e.location&&location.port?location.port:this.secure?443:80),this.query=n.query||{},"string"==typeof this.query&&(this.query=h.decode(this.query)),this.upgrade=!1!==n.upgrade,this.path=(n.path||"/engine.io").replace(/\/$/,"")+"/",this.forceJSONP=!!n.forceJSONP,this.jsonp=!1!==n.jsonp,this.forceBase64=!!n.forceBase64,this.enablesXDR=!!n.enablesXDR,this.timestampParam=n.timestampParam||"t",this.timestampRequests=n.timestampRequests,this.transports=n.transports||["polling","websocket"],this.transportOptions=n.transportOptions||{},this.readyState="",this.writeBuffer=[],this.prevBufferLen=0,this.policyPort=n.policyPort||843,this.rememberUpgrade=n.rememberUpgrade||!1,this.binaryType=null,this.onlyBinaryUpgrades=n.onlyBinaryUpgrades,this.perMessageDeflate=!1!==n.perMessageDeflate&&(n.perMessageDeflate||{}),!0===this.perMessageDeflate&&(this.perMessageDeflate={}),this.perMessageDeflate&&null==this.perMessageDeflate.threshold&&(this.perMessageDeflate.threshold=1024),this.pfx=n.pfx||null,this.key=n.key||null,this.passphrase=n.passphrase||null,this.cert=n.cert||null,this.ca=n.ca||null,this.ciphers=n.ciphers||null,this.rejectUnauthorized=void 0===n.rejectUnauthorized||n.rejectUnauthorized,this.forceNode=!!n.forceNode;var o="object"==typeof e&&e;o.global===o&&(n.extraHeaders&&Object.keys(n.extraHeaders).length>0&&(this.extraHeaders=n.extraHeaders),n.localAddress&&(this.localAddress=n.localAddress)),this.id=null,this.upgrades=null,this.pingInterval=null,this.pingTimeout=null,this.pingIntervalTimer=null,this.pingTimeoutTimer=null,this.open()}function o(t){var e={};for(var n in t)t.hasOwnProperty(n)&&(e[n]=t[n]);return e}var i=n(15),s=n(8),a=n(3)("engine.io-client:socket"),c=n(36),u=n(21),p=n(2),h=n(30);t.exports=r,r.priorWebsocketSuccess=!1,s(r.prototype),r.protocol=u.protocol,r.Socket=r,r.Transport=n(20),r.transports=n(15),r.parser=n(21),r.prototype.createTransport=function(t){a('creating transport "%s"',t);var e=o(this.query);e.EIO=u.protocol,e.transport=t;var n=this.transportOptions[t]||{};this.id&&(e.sid=this.id);var r=new i[t]({query:e,socket:this,agent:n.agent||this.agent,hostname:n.hostname||this.hostname,port:n.port||this.port,secure:n.secure||this.secure,path:n.path||this.path,forceJSONP:n.forceJSONP||this.forceJSONP,jsonp:n.jsonp||this.jsonp,forceBase64:n.forceBase64||this.forceBase64,enablesXDR:n.enablesXDR||this.enablesXDR,timestampRequests:n.timestampRequests||this.timestampRequests,timestampParam:n.timestampParam||this.timestampParam,policyPort:n.policyPort||this.policyPort,pfx:n.pfx||this.pfx,key:n.key||this.key,passphrase:n.passphrase||this.passphrase,cert:n.cert||this.cert,ca:n.ca||this.ca,ciphers:n.ciphers||this.ciphers,rejectUnauthorized:n.rejectUnauthorized||this.rejectUnauthorized,perMessageDeflate:n.perMessageDeflate||this.perMessageDeflate,extraHeaders:n.extraHeaders||this.extraHeaders,forceNode:n.forceNode||this.forceNode,localAddress:n.localAddress||this.localAddress,requestTimeout:n.requestTimeout||this.requestTimeout,protocols:n.protocols||void 0});return r},r.prototype.open=function(){var t;if(this.rememberUpgrade&&r.priorWebsocketSuccess&&this.transports.indexOf("websocket")!==-1)t="websocket";else{if(0===this.transports.length){var e=this;return void setTimeout(function(){e.emit("error","No transports available")},0)}t=this.transports[0]}this.readyState="opening";try{t=this.createTransport(t)}catch(n){return this.transports.shift(),void this.open()}t.open(),this.setTransport(t)},r.prototype.setTransport=function(t){a("setting transport %s",t.name);var e=this;this.transport&&(a("clearing existing transport %s",this.transport.name),this.transport.removeAllListeners()),this.transport=t,t.on("drain",function(){e.onDrain()}).on("packet",function(t){e.onPacket(t)}).on("error",function(t){e.onError(t)}).on("close",function(){e.onClose("transport close")})},r.prototype.probe=function(t){function e(){if(f.onlyBinaryUpgrades){var e=!this.supportsBinary&&f.transport.supportsBinary;h=h||e}h||(a('probe transport "%s" opened',t),p.send([{type:"ping",data:"probe"}]),p.once("packet",function(e){if(!h)if("pong"===e.type&&"probe"===e.data){if(a('probe transport "%s" pong',t),f.upgrading=!0,f.emit("upgrading",p),!p)return;r.priorWebsocketSuccess="websocket"===p.name,a('pausing current transport "%s"',f.transport.name),f.transport.pause(function(){h||"closed"!==f.readyState&&(a("changing transport and sending upgrade packet"),u(),f.setTransport(p),p.send([{type:"upgrade"}]),f.emit("upgrade",p),p=null,f.upgrading=!1,f.flush())})}else{a('probe transport "%s" failed',t);var n=new Error("probe error");n.transport=p.name,f.emit("upgradeError",n)}}))}function n(){h||(h=!0,u(),p.close(),p=null)}function o(e){var r=new Error("probe error: "+e);r.transport=p.name,n(),a('probe transport "%s" failed because of error: %s',t,e),f.emit("upgradeError",r)}function i(){o("transport closed")}function s(){o("socket closed")}function c(t){p&&t.name!==p.name&&(a('"%s" works - aborting "%s"',t.name,p.name),n())}function u(){p.removeListener("open",e),p.removeListener("error",o),p.removeListener("close",i),f.removeListener("close",s),f.removeListener("upgrading",c)}a('probing transport "%s"',t);var p=this.createTransport(t,{probe:1}),h=!1,f=this;r.priorWebsocketSuccess=!1,p.once("open",e),p.once("error",o),p.once("close",i),this.once("close",s),this.once("upgrading",c),p.open()},r.prototype.onOpen=function(){if(a("socket open"),this.readyState="open",r.priorWebsocketSuccess="websocket"===this.transport.name,this.emit("open"),this.flush(),"open"===this.readyState&&this.upgrade&&this.transport.pause){a("starting upgrade probes");for(var t=0,e=this.upgrades.length;t1?{type:b[o],data:t.substring(1)}:{type:b[o]}:w}var i=new Uint8Array(t),o=i[0],s=f(t,1);return k&&"blob"===n&&(s=new k([s])),{type:b[o],data:s}},e.decodeBase64Packet=function(t,e){var n=b[t.charAt(0)];if(!u)return{type:n,data:{base64:!0,data:t.substr(1)}};var r=u.decode(t.substr(1));return"blob"===e&&k&&(r=new k([r])),{type:n,data:r}},e.encodePayload=function(t,n,r){function o(t){return t.length+":"+t}function i(t,r){e.encodePacket(t,!!s&&n,!1,function(t){r(null,o(t))})}"function"==typeof n&&(r=n,n=null);var s=h(t);return n&&s?k&&!g?e.encodePayloadAsBlob(t,r):e.encodePayloadAsArrayBuffer(t,r):t.length?void c(t,i,function(t,e){return r(e.join(""))}):r("0:")},e.decodePayload=function(t,n,r){if("string"!=typeof t)return e.decodePayloadAsBinary(t,n,r);"function"==typeof n&&(r=n,n=null);var o;if(""===t)return r(w,0,1);for(var i,s,a="",c=0,u=t.length;c0;){for(var s=new Uint8Array(o),a=0===s[0],c="",u=1;255!==s[u];u++){if(c.length>310)return r(w,0,1);c+=s[u]}o=f(o,2+c.length),c=parseInt(c);var p=f(o,0,c);if(a)try{p=String.fromCharCode.apply(null,new Uint8Array(p))}catch(h){var l=new Uint8Array(p);p="";for(var u=0;ur&&(n=r),e>=r||e>=n||0===r)return new ArrayBuffer(0);for(var o=new Uint8Array(t),i=new Uint8Array(n-e),s=e,a=0;s=55296&&e<=56319&&o65535&&(e-=65536,o+=w(e>>>10&1023|55296),e=56320|1023&e),o+=w(e);return o}function c(t,e){if(t>=55296&&t<=57343){if(e)throw Error("Lone surrogate U+"+t.toString(16).toUpperCase()+" is not a scalar value");return!1}return!0}function u(t,e){return w(t>>e&63|128)}function p(t,e){if(0==(4294967168&t))return w(t);var n="";return 0==(4294965248&t)?n=w(t>>6&31|192):0==(4294901760&t)?(c(t,e)||(t=65533),n=w(t>>12&15|224),n+=u(t,6)):0==(4292870144&t)&&(n=w(t>>18&7|240),n+=u(t,12),n+=u(t,6)),n+=w(63&t|128)}function h(t,e){e=e||{};for(var n,r=!1!==e.strict,o=s(t),i=o.length,a=-1,c="";++a=v)throw Error("Invalid byte index");var t=255&g[b];if(b++,128==(192&t))return 63&t;throw Error("Invalid continuation byte")}function l(t){var e,n,r,o,i;if(b>v)throw Error("Invalid byte index");if(b==v)return!1;if(e=255&g[b],b++,0==(128&e))return e;if(192==(224&e)){if(n=f(),i=(31&e)<<6|n,i>=128)return i;throw Error("Invalid continuation byte")}if(224==(240&e)){if(n=f(),r=f(),i=(15&e)<<12|n<<6|r,i>=2048)return c(i,t)?i:65533;throw Error("Invalid continuation byte")}if(240==(248&e)&&(n=f(),r=f(),o=f(),i=(7&e)<<18|n<<12|r<<6|o,i>=65536&&i<=1114111))return i;throw Error("Invalid UTF-8 detected")}function d(t,e){e=e||{};var n=!1!==e.strict;g=s(t),v=g.length,b=0;for(var r,o=[];(r=l(n))!==!1;)o.push(r);return a(o)}var y="object"==typeof e&&e,m=("object"==typeof t&&t&&t.exports==y&&t,"object"==typeof o&&o);m.global!==m&&m.window!==m||(i=m);var g,v,b,w=String.fromCharCode,k={version:"2.1.2",encode:h,decode:d};r=function(){return k}.call(e,n,e,t),!(void 0!==r&&(t.exports=r))}(this)}).call(e,n(27)(t),function(){return this}())},function(t,e){t.exports=function(t){return t.webpackPolyfill||(t.deprecate=function(){},t.paths=[],t.children=[],t.webpackPolyfill=1),t}},function(t,e){!function(){"use strict";for(var t="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/",n=new Uint8Array(256),r=0;r>2],i+=t[(3&r[n])<<4|r[n+1]>>4],i+=t[(15&r[n+1])<<2|r[n+2]>>6],i+=t[63&r[n+2]];return o%3===2?i=i.substring(0,i.length-1)+"=":o%3===1&&(i=i.substring(0,i.length-2)+"=="),i},e.decode=function(t){var e,r,o,i,s,a=.75*t.length,c=t.length,u=0;"="===t[t.length-1]&&(a--,"="===t[t.length-2]&&a--);var p=new ArrayBuffer(a),h=new Uint8Array(p);for(e=0;e>4,h[u++]=(15&o)<<4|i>>2,h[u++]=(3&i)<<6|63&s;return p}}()},function(t,e){(function(e){function n(t){for(var e=0;e0);return e}function r(t){var e=0;for(p=0;p';i=document.createElement(e)}catch(t){i=document.createElement("iframe"),i.name=o.iframeId,i.src="javascript:0"}i.id=o.iframeId,o.form.appendChild(i),o.iframe=i}var o=this;if(!this.form){var i,s=document.createElement("form"),a=document.createElement("textarea"),p=this.iframeId="eio_iframe_"+this.index;s.className="socketio",s.style.position="absolute",s.style.top="-1000px",s.style.left="-1000px",s.target=p,s.method="POST",s.setAttribute("accept-charset","utf-8"),a.name="d",s.appendChild(a),document.body.appendChild(s),this.form=s,this.area=a}this.form.action=this.uri(),r(),t=t.replace(u,"\\\n"),this.area.value=t.replace(c,"\\n");try{this.form.submit()}catch(h){}this.iframe.attachEvent?this.iframe.onreadystatechange=function(){"complete"===o.iframe.readyState&&n()}:this.iframe.onload=n}}).call(e,function(){return this}())},function(t,e,n){(function(e){function r(t){var e=t&&t.forceBase64;e&&(this.supportsBinary=!1),this.perMessageDeflate=t.perMessageDeflate,this.usingBrowserWebSocket=h&&!t.forceNode,this.protocols=t.protocols,this.usingBrowserWebSocket||(l=o),i.call(this,t)}var o,i=n(20),s=n(21),a=n(30),c=n(31),u=n(32),p=n(3)("engine.io-client:websocket"),h=e.WebSocket||e.MozWebSocket;if("undefined"==typeof window)try{o=n(35)}catch(f){}var l=h;l||"undefined"!=typeof window||(l=o),t.exports=r,c(r,i),r.prototype.name="websocket",r.prototype.supportsBinary=!0,r.prototype.doOpen=function(){if(this.check()){var t=this.uri(),e=this.protocols,n={agent:this.agent,perMessageDeflate:this.perMessageDeflate};n.pfx=this.pfx,n.key=this.key,n.passphrase=this.passphrase,n.cert=this.cert,n.ca=this.ca,n.ciphers=this.ciphers,n.rejectUnauthorized=this.rejectUnauthorized,this.extraHeaders&&(n.headers=this.extraHeaders),this.localAddress&&(n.localAddress=this.localAddress);try{this.ws=this.usingBrowserWebSocket?e?new l(t,e):new l(t):new l(t,e,n)}catch(r){return this.emit("error",r)}void 0===this.ws.binaryType&&(this.supportsBinary=!1),this.ws.supports&&this.ws.supports.binary?(this.supportsBinary=!0,this.ws.binaryType="nodebuffer"):this.ws.binaryType="arraybuffer",this.addEventListeners()}},r.prototype.addEventListeners=function(){var t=this;this.ws.onopen=function(){t.onOpen()},this.ws.onclose=function(){t.onClose()},this.ws.onmessage=function(e){t.onData(e.data)},this.ws.onerror=function(e){t.onError("websocket error",e)}},r.prototype.write=function(t){function n(){r.emit("flush"),setTimeout(function(){r.writable=!0,r.emit("drain")},0)}var r=this;this.writable=!1;for(var o=t.length,i=0,a=o;i0&&t.jitter<=1?t.jitter:0,this.attempts=0}t.exports=n,n.prototype.duration=function(){var t=this.ms*Math.pow(this.factor,this.attempts++);if(this.jitter){var e=Math.random(),n=Math.floor(e*this.jitter*t);t=0==(1&Math.floor(10*e))?t-n:t+n}return 0|Math.min(t,this.max)},n.prototype.reset=function(){this.attempts=0},n.prototype.setMin=function(t){this.ms=t},n.prototype.setMax=function(t){this.max=t},n.prototype.setJitter=function(t){this.jitter=t}}])}); +//# sourceMappingURL=socket.io.js.map \ No newline at end of file diff --git a/video.html b/video.html new file mode 100755 index 0000000..0db9910 --- /dev/null +++ b/video.html @@ -0,0 +1,24 @@ + + + +RecordRTC + + + +

Close this window to stop the broadcast!

+ + + + \ No newline at end of file diff --git a/video.js b/video.js new file mode 100755 index 0000000..f993600 --- /dev/null +++ b/video.js @@ -0,0 +1,2 @@ +var src = location.href.split('?src=')[1]; +document.querySelector('video').src = src; \ No newline at end of file diff --git a/websocket.js b/websocket.js new file mode 100755 index 0000000..eb29cfb --- /dev/null +++ b/websocket.js @@ -0,0 +1,96 @@ +// Version: 3.6.7 +(function(){ +/* +CryptoJS v3.1.2 +code.google.com/p/crypto-js +(c) 2009-2013 by Jeff Mott. All rights reserved. +code.google.com/p/crypto-js/wiki/License +*/ +var CryptoJS=CryptoJS||function(h,s){var f={},g=f.lib={},q=function(){},m=g.Base={extend:function(a){q.prototype=this;var c=new q;a&&c.mixIn(a);c.hasOwnProperty("init")||(c.init=function(){c.$super.init.apply(this,arguments)});c.init.prototype=c;c.$super=this;return c},create:function(){var a=this.extend();a.init.apply(a,arguments);return a},init:function(){},mixIn:function(a){for(var c in a)a.hasOwnProperty(c)&&(this[c]=a[c]);a.hasOwnProperty("toString")&&(this.toString=a.toString)},clone:function(){return this.init.prototype.extend(this)}}, +r=g.WordArray=m.extend({init:function(a,c){a=this.words=a||[];this.sigBytes=c!=s?c:4*a.length},toString:function(a){return(a||k).stringify(this)},concat:function(a){var c=this.words,d=a.words,b=this.sigBytes;a=a.sigBytes;this.clamp();if(b%4)for(var e=0;e>>2]|=(d[e>>>2]>>>24-8*(e%4)&255)<<24-8*((b+e)%4);else if(65535>>2]=d[e>>>2];else c.push.apply(c,d);this.sigBytes+=a;return this},clamp:function(){var a=this.words,c=this.sigBytes;a[c>>>2]&=4294967295<< +32-8*(c%4);a.length=h.ceil(c/4)},clone:function(){var a=m.clone.call(this);a.words=this.words.slice(0);return a},random:function(a){for(var c=[],d=0;d>>2]>>>24-8*(b%4)&255;d.push((e>>>4).toString(16));d.push((e&15).toString(16))}return d.join("")},parse:function(a){for(var c=a.length,d=[],b=0;b>>3]|=parseInt(a.substr(b, +2),16)<<24-4*(b%8);return new r.init(d,c/2)}},n=l.Latin1={stringify:function(a){var c=a.words;a=a.sigBytes;for(var d=[],b=0;b>>2]>>>24-8*(b%4)&255));return d.join("")},parse:function(a){for(var c=a.length,d=[],b=0;b>>2]|=(a.charCodeAt(b)&255)<<24-8*(b%4);return new r.init(d,c)}},j=l.Utf8={stringify:function(a){try{return decodeURIComponent(escape(n.stringify(a)))}catch(c){throw Error("Malformed UTF-8 data");}},parse:function(a){return n.parse(unescape(encodeURIComponent(a)))}}, +u=g.BufferedBlockAlgorithm=m.extend({reset:function(){this._data=new r.init;this._nDataBytes=0},_append:function(a){"string"==typeof a&&(a=j.parse(a));this._data.concat(a);this._nDataBytes+=a.sigBytes},_process:function(a){var c=this._data,d=c.words,b=c.sigBytes,e=this.blockSize,f=b/(4*e),f=a?h.ceil(f):h.max((f|0)-this._minBufferSize,0);a=f*e;b=h.min(4*a,b);if(a){for(var g=0;gn;){var j;a:{j=k;for(var u=h.sqrt(j),t=2;t<=u;t++)if(!(j%t)){j=!1;break a}j=!0}j&&(8>n&&(m[n]=l(h.pow(k,0.5))),r[n]=l(h.pow(k,1/3)),n++);k++}var a=[],f=f.SHA256=q.extend({_doReset:function(){this._hash=new g.init(m.slice(0))},_doProcessBlock:function(c,d){for(var b=this._hash.words,e=b[0],f=b[1],g=b[2],j=b[3],h=b[4],m=b[5],n=b[6],q=b[7],p=0;64>p;p++){if(16>p)a[p]= +c[d+p]|0;else{var k=a[p-15],l=a[p-2];a[p]=((k<<25|k>>>7)^(k<<14|k>>>18)^k>>>3)+a[p-7]+((l<<15|l>>>17)^(l<<13|l>>>19)^l>>>10)+a[p-16]}k=q+((h<<26|h>>>6)^(h<<21|h>>>11)^(h<<7|h>>>25))+(h&m^~h&n)+r[p]+a[p];l=((e<<30|e>>>2)^(e<<19|e>>>13)^(e<<10|e>>>22))+(e&f^e&g^f&g);q=n;n=m;m=h;h=j+k|0;j=g;g=f;f=e;e=k+l|0}b[0]=b[0]+e|0;b[1]=b[1]+f|0;b[2]=b[2]+g|0;b[3]=b[3]+j|0;b[4]=b[4]+h|0;b[5]=b[5]+m|0;b[6]=b[6]+n|0;b[7]=b[7]+q|0},_doFinalize:function(){var a=this._data,d=a.words,b=8*this._nDataBytes,e=8*a.sigBytes; +d[e>>>5]|=128<<24-e%32;d[(e+64>>>9<<4)+14]=h.floor(b/4294967296);d[(e+64>>>9<<4)+15]=b;a.sigBytes=4*d.length;this._process();return this._hash},clone:function(){var a=q.clone.call(this);a._hash=this._hash.clone();return a}});s.SHA256=q._createHelper(f);s.HmacSHA256=q._createHmacHelper(f)})(Math); +(function(){var h=CryptoJS,s=h.enc.Utf8;h.algo.HMAC=h.lib.Base.extend({init:function(f,g){f=this._hasher=new f.init;"string"==typeof g&&(g=s.parse(g));var h=f.blockSize,m=4*h;g.sigBytes>m&&(g=f.finalize(g));g.clamp();for(var r=this._oKey=g.clone(),l=this._iKey=g.clone(),k=r.words,n=l.words,j=0;j>>2]>>>24-8*(a%4)&255)<<16|(e[a+1>>>2]>>>24-8*((a+1)%4)&255)<<8|e[a+2>>>2]>>>24-8*((a+2)%4)&255,g=0;4>g&&a+0.75*g>>6*(3-g)&63));if(e=c.charAt(64))for(;b.length%4;)b.push(e);return b.join("")},parse:function(b){var e=b.length,f=this._map,c=f.charAt(64);c&&(c=b.indexOf(c),-1!=c&&(e=c));for(var c=[],a=0,d=0;d< +e;d++)if(d%4){var g=f.indexOf(b.charAt(d-1))<<2*(d%4),h=f.indexOf(b.charAt(d))>>>6-2*(d%4);c[a>>>2]|=(g|h)<<24-8*(a%4);a++}return j.create(c,a)},_map:"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/="}})(); +var v=void 0,y=!0,z=null,A=!1;function C(){return function(){}} +window.JSON&&window.JSON.stringify||function(){function a(){try{return this.valueOf()}catch(a){return z}}function d(a){c.lastIndex=0;return c.test(a)?'"'+a.replace(c,function(a){var b=q[a];return"string"===typeof b?b:"\\u"+("0000"+a.charCodeAt(0).toString(16)).slice(-4)})+'"':'"'+a+'"'}function b(c,q){var t,r,g,j,h,l=f,e=q[c];e&&"object"===typeof e&&(e=a.call(e));"function"===typeof m&&(e=m.call(q,c,e));switch(typeof e){case "string":return d(e);case "number":return isFinite(e)?String(e):"null";case "boolean":case "null":return String(e); +case "object":if(!e)return"null";f+=p;h=[];if("[object Array]"===Object.prototype.toString.apply(e)){j=e.length;for(t=0;t++na?na:na=1))||a}; +function qa(a,d){var b=a.join(ia),c=[];if(!d)return b;N(d,function(a,b){var d="object"==typeof b?JSON.stringify(b):b;"undefined"!=typeof b&&(b!=z&&0G()?(clearTimeout(c),c=setTimeout(b,d)):(f=G(),a())}var c,f=0;return b}function sa(a,d){var b=[];N(a||[],function(a){d(a)&&b.push(a)});return b}function ta(a,d){return a.replace(ka,function(a,c){return d[c]||a})} +function pa(a){var d="xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g,function(a){var c=16*Math.random()|0;return("x"==a?c:c&3|8).toString(16)});a&&a(d);return d}function N(a,d){if(a&&d)if(a&&(Array.isArray&&Array.isArray(a)||"number"===typeof a.length))for(var b=0,c=a.length;ba.search("-pnpres")&&f.f&&b.push(a):f.f&&b.push(a)});return b.sort()}function wa(){setTimeout(function(){fa||(fa=1,N(ga,function(a){a()}))},D)}var O,T=14,U=8,xa=A;function ya(a,d){var b="",c,f;if(d){c=a[15];if(16f;f++)b+=String.fromCharCode(a[f]);return b} +function za(a,d){var b=[],c;if(!d)try{a=unescape(encodeURIComponent(a))}catch(f){throw"Error on UTF-8 encode";}for(c=0;cx.length&&(r=16-x.length,t=[r,r,r,r,r,r,r,r,r,r,r,r,r,r,r,r]);for(r=0;rc;c++)b[c]=d[a[c]];return b}function Na(a){var d=[],b=xa?[0,13,10,7,4,1,14,11,8,5,2,15,12,9,6,3]:[0,5,10,15,4,9,14,3,8,13,2,7,12,1,6,11],c;for(c=0;16>c;c++)d[c]=a[b[c]];return d} +function Oa(a){var d=[],b;if(xa)for(b=0;4>b;b++)d[4*b]=Ta[a[4*b]]^Ua[a[1+4*b]]^Za[a[2+4*b]]^$a[a[3+4*b]],d[1+4*b]=$a[a[4*b]]^Ta[a[1+4*b]]^Ua[a[2+4*b]]^Za[a[3+4*b]],d[2+4*b]=Za[a[4*b]]^$a[a[1+4*b]]^Ta[a[2+4*b]]^Ua[a[3+4*b]],d[3+4*b]=Ua[a[4*b]]^Za[a[1+4*b]]^$a[a[2+4*b]]^Ta[a[3+4*b]];else for(b=0;4>b;b++)d[4*b]=ab[a[4*b]]^bb[a[1+4*b]]^a[2+4*b]^a[3+4*b],d[1+4*b]=a[4*b]^ab[a[1+4*b]]^bb[a[2+4*b]]^a[3+4*b],d[2+4*b]=a[4*b]^a[1+4*b]^ab[a[2+4*b]]^bb[a[3+4*b]],d[3+4*b]=bb[a[4*b]]^a[1+4*b]^a[2+4*b]^ab[a[3+4* +b]];return d}function La(a,d,b){var c=[],f;for(f=0;16>f;f++)c[f]=a[f]^d[b][f];return c}function Da(a,d){var b=[],c;for(c=0;16>c;c++)b[c]=a[c]^d[c];return b} +function Ca(a){var d=[],b=[],c,f,p=[];for(c=0;ca;a++)b[a]=d[c-1][a];if(0===c%U){a=b[0];f=v;for(f=0;4>f;f++)b[f]=b[f+1];b[3]=a;b=cb(b);b[0]^=db[c/U-1]}else 6a;a++)d[c][a]=d[c-U][a]^b[a]}for(c=0;cb;b++)p[c].push(d[4*c+b][0],d[4*c+b][1],d[4*c+b][2],d[4*c+b][3])}return p}function cb(a){for(var d=0;4>d;d++)a[d]=Qa[a[d]];return a} +function eb(a,d){var b=[];for(i=0;ib;b++){for(var c=a,f=b,p=v,q=v,p=q=0;8>p;p++)q=1==(f&1)?q^c:q,c=127>>=1;d[b]=q}return d} +var Qa=eb("637c777bf26b6fc53001672bfed7ab76ca82c97dfa5947f0add4a2af9ca472c0b7fd9326363ff7cc34a5e5f171d8311504c723c31896059a071280e2eb27b27509832c1a1b6e5aa0523bd6b329e32f8453d100ed20fcb15b6acbbe394a4c58cfd0efaafb434d338545f9027f503c9fa851a3408f929d38f5bcb6da2110fff3d2cd0c13ec5f974417c4a77e3d645d197360814fdc222a908846eeb814de5e0bdbe0323a0a4906245cc2d3ac629195e479e7c8376d8dd54ea96c56f4ea657aae08ba78252e1ca6b4c6e8dd741f4bbd8b8a703eb5664803f60e613557b986c11d9ee1f8981169d98e949b1e87e9ce5528df8ca1890dbfe6426841992d0fb054bb16",2), +Pa,gb=Qa,hb=[];for(i=0;i>2],b+=kb[(d[c]&3)<<4|d[c+1]>>4],b=d[c+1]!==v?b+kb[(d[c+1]&15)<<2|d[c+2]>>6]:b+"=",b=d[c+2]!==v?b+kb[d[c+2]&63]:b+"=";a=b.slice(0,64);for(c=1;c>4,c[1]=(b[1]&15)<<4|b[2]>>2,c[2]=(b[2]&3)<<6|b[3],d.push(c[0],c[1],c[2]);return d=d.slice(0,d.length-d.length%16)}}; +O={size:function(a){switch(a){case 128:T=10;U=4;break;case 192:T=12;U=6;break;case 256:T=14;U=8;break;default:throw"Invalid Key Size Specified:"+a;}},h2a:function(a){var d=[];a.replace(/(..)/g,function(a){d.push(parseInt(a,16))});return d},expandKey:Ca,encryptBlock:Ea,decryptBlock:Ga,Decrypt:xa,s2a:za,rawEncrypt:Ba,rawDecrypt:Fa,dec:function(a,d,b){var a=ib.q(a),c=a.slice(8,16),c=Aa(za(d,b),c),d=c.key,c=c.i,a=a.slice(16,a.length);return a=Fa(a,d,c,b)},openSSLKey:Aa,a2h:function(a){var d="",b;for(b= +0;ba[b]?"0":"")+a[b].toString(16);return d},enc:function(a,d,b){var c;c=[];var f;for(f=0;8>f;f++)c=c.concat(Math.floor(256*Math.random()));f=Aa(za(d,b),c);d=f.key;f=f.i;c=[[83,97,108,116,101,100,95,95].concat(c)];a=za(a,b);a=Ba(a,d,f);a=c.concat(a);return ib.s(a)},Hash:{MD5:function(a){function d(a,b){var c,d,f,e,g;f=a&2147483648;e=b&2147483648;c=a&1073741824;d=b&1073741824;g=(a&1073741823)+(b&1073741823);return c&d?g^2147483648^f^e:c|d?g&1073741824?g^3221225472^f^e:g^1073741824^ +f^e:g^f^e}function b(a,b,c,f,e,g,l){a=d(a,d(d(b&c|~b&f,e),l));return d(a<>>32-g,b)}function c(a,b,c,f,e,g,l){a=d(a,d(d(b&f|c&~f,e),l));return d(a<>>32-g,b)}function f(a,b,c,f,e,g,l){a=d(a,d(d(b^c^f,e),l));return d(a<>>32-g,b)}function p(a,b,c,f,g,e,l){a=d(a,d(d(c^(b|~f),g),l));return d(a<>>32-e,b)}function q(a){var b,c,d=[];for(c=0;3>=c;c++)b=a>>>8*c&255,d=d.concat(b);return d}var m=[],u,x,t,r,g,j,h,l,e=eb("67452301efcdab8998badcfe10325476d76aa478e8c7b756242070dbc1bdceeef57c0faf4787c62aa8304613fd469501698098d88b44f7afffff5bb1895cd7be6b901122fd987193a679438e49b40821f61e2562c040b340265e5a51e9b6c7aad62f105d02441453d8a1e681e7d3fbc821e1cde6c33707d6f4d50d87455a14eda9e3e905fcefa3f8676f02d98d2a4c8afffa39428771f6816d9d6122fde5380ca4beea444bdecfa9f6bb4b60bebfbc70289b7ec6eaa127fad4ef308504881d05d9d4d039e6db99e51fa27cf8c4ac5665f4292244432aff97ab9423a7fc93a039655b59c38f0ccc92ffeff47d85845dd16fa87e4ffe2ce6e0a30143144e0811a1f7537e82bd3af2352ad7d2bbeb86d391", +8),m=a.length;u=m+8;x=16*((u-u%64)/64+1);t=[];for(g=r=0;g>>29;m=t;g=e[0];j=e[1];h=e[2];l=e[3];for(a=0;aJ||!va(B,y).length?Wa=A:(Wa=y,I.presence_heartbeat({callback:function(){W=setTimeout(r,J*D)},error:function(a){s&&s("Presence Heartbeat unable to reach Pubnub servers."+JSON.stringify(a));W=setTimeout(r,J*D)}}))}function g(a,b){return ba.decrypt(a,b||X)||ba.decrypt(a,X)||a}function j(a,b,c){var d=A;if("number"===typeof a)d=5 5 or x = 0). Current Value : "+(b||5)),b|| +5):a}function h(a){var b="",c=[];N(a,function(a){c.push(a)});var d=c.sort(),f;for(f in d){var e=d[f],b=b+(e+"="+encodeURIComponent(a[e]));f!=d.length-1&&(b+="&")}return b}function l(a){a||(a={});N(Y,function(b,c){b in a||(a[b]=c)});return a}function e(a){return Jb(a)}function aa(a){function b(a,c){var d=(a&65535)+(c&65535);return(a>>16)+(c>>16)+(d>>16)<<16|d&65535}function c(a,b){return a>>>b|a<<32-b}var d;d=a.replace(/\r\n/g,"\n");for(var a="",f=0;fe?a+=String.fromCharCode(e): +(127e?a+=String.fromCharCode(e>>6|192):(a+=String.fromCharCode(e>>12|224),a+=String.fromCharCode(e>>6&63|128)),a+=String.fromCharCode(e&63|128))}f=a;d=[];for(e=0;e<8*f.length;e+=8)d[e>>5]|=(f.charCodeAt(e/8)&255)<<24-e%32;var g=8*a.length,f=[1116352408,1899447441,3049323471,3921009573,961987163,1508970993,2453635748,2870763221,3624381080,310598401,607225278,1426881987,1925078388,2162078206,2614888103,3248222580,3835390401,4022224774,264347078,604807628,770255983,1249150122,1555081692,1996064986, +2554220882,2821834349,2952996808,3210313671,3336571891,3584528711,113926993,338241895,666307205,773529912,1294757372,1396182291,1695183700,1986661051,2177026350,2456956037,2730485921,2820302411,3259730800,3345764771,3516065817,3600352804,4094571909,275423344,430227734,506948616,659060556,883997877,958139571,1322822218,1537002063,1747873779,1955562222,2024104815,2227730452,2361852424,2428436474,2756734187,3204031479,3329325298],a=[1779033703,3144134277,1013904242,2773480762,1359893119,2600822924,528734635, +1541459225],e=Array(64),l,m,j,h,q,p,r,t,s,w,u;d[g>>5]|=128<<24-g%32;d[(g+64>>9<<4)+15]=g;for(t=0;ts;s++)e[s]=16>s?d[s+t]:b(b(b(c(e[s-2],17)^c(e[s-2],19)^e[s-2]>>>10,e[s-7]),c(e[s-15],7)^c(e[s-15],18)^e[s-15]>>>3),e[s-16]),w=b(b(b(b(r,c(h,6)^c(h,11)^c(h,25)),h&q^~h&p),f[s]),e[s]),u=b(c(g,2)^c(g,13)^c(g,22),g&l^g&m^l&m),r=p,p=q,q=h,h=b(j,w),j=m,m=l,l=g,g=b(w,u);a[0]=b(g,a[0]);a[1]=b(l,a[1]);a[2]=b(m,a[2]);a[3]=b(j,a[3]); +a[4]=b(h,a[4]);a[5]=b(q,a[5]);a[6]=b(p,a[6]);a[7]=b(r,a[7])}d="";for(f=0;f<4*a.length;f++)d+="0123456789abcdef".charAt(a[f>>2]>>8*(3-f%4)+4&15)+"0123456789abcdef".charAt(a[f>>2]>>8*(3-f%4)&15);return d}a.jsonp&&(ub=0);var H=a.subscribe_key||"";a.uuid||Eb.get(H+"uuid");var Ra=a.leave_on_unload||0;a.xdr=Ab;a.db=Eb;a.error=a.error||nb;a._is_online=Cb;a.jsonp_cb=zb;a.hmac_SHA256=lb;O.size(256);var S=O.s2a("0123456789012345");a.crypto_obj={encrypt:function(a,b){if(!b)return a;var c=O.s2a(aa(b).slice(0, +32)),d=O.s2a(JSON.stringify(a)),c=O.rawEncrypt(d,c,S);return O.Base64.encode(c)||a},decrypt:function(a,b){if(!b)return a;var c=O.s2a(aa(b).slice(0,32));try{var d=O.Base64.decode(a),e=O.rawDecrypt(d,c,S,A);return JSON.parse(e)}catch(f){}}};a.params={pnsdk:"PubNub-JS-Web/3.6.7"};var Sa=+a.windowing||10,Hb=(+a.timeout||310)*D,ob=(+a.keepalive||60)*D,Lb=a.noleave||0,P=a.publish_key||"demo",w=a.subscribe_key||"demo",M=a.auth_key||"",Ia=a.secret_key||"",vb=a.hmac_SHA256,Xa=a.ssl?"s":"",oa="http"+Xa+"://"+ +(a.origin||"pubsub.pubnub.com"),K=ma(oa),wb=ma(oa),Q=[],Va=0,xb=0,yb=0,Ha=0,Ja=a.restore||0,da=0,Ya=A,B={},Z={},W=z,R=j(a.heartbeat||a.pnexpires||0,a.error),J=a.heartbeat_interval||R-3,Wa=A,Ob=a.no_wait_for_pending,Pb=a["compatible_3.5"]||A,E=a.xdr,Y=a.params||{},s=a.error||C(),Nb=a._is_online||function(){return 1},L=a.jsonp_cb||function(){return 0},ea=a.db||{get:C(),set:C()},X=a.cipher_key,F=a.uuid||ea&&ea.get(w+"uuid")||"",ba=a.crypto_obj||{encrypt:function(a){return a},decrypt:function(a){return a}}, +I={LEAVE:function(a,b,c,d){var e={uuid:F,auth:M},f=ma(oa),c=c||C(),g=d||C(),d=L();if(0R&&(d.heartbeat=R);"0"!=a&&(d.callback=a);var e=E,d=l(d),f=5*D,g=K,h=w,j=va(B,y).join(",");e({a:a,data:d,timeout:f,url:[g,"v2","presence","sub-key",h,"channel",encodeURIComponent(j),"heartbeat"],c:function(a){m(a,b,c)},b:function(a){q(a,c)}})},xdr:E,ready:wa,db:ea,uuid:pa,map:ua,each:N,"each-channel":u,grep:sa,offline:function(){c(1,{message:"Offline. Please check your network settings."})}, +supplant:ta,now:G,unique:la,updater:ra};F||(F=I.uuid());ea.set(w+"uuid",F);setTimeout(p,D);setTimeout(f,ob);W=setTimeout(t,(J-3)*D);b();var H=I,Ka;for(Ka in H)H.hasOwnProperty(Ka)&&(e[Ka]=H[Ka]);e.css=sb;e.$=mb;e.create=tb;e.bind=qb;e.head=rb;e.search=pb;e.attr=V;e.events=Gb;e.init=e;e.secure=e;qb("beforeunload",window,function(){if(Ra)e["each-channel"](function(a){e.LEAVE(a.name,0)});return y});if(a.notest)return e;qb("offline",window,e.offline);qb("offline",document,e.offline);return e};Jb.init= +Jb;Jb.secure=Jb;"complete"===document.readyState?setTimeout(wa,0):qb("load",window,function(){setTimeout(wa,0)});var Kb=Ib||{};PUBNUB=Jb({notest:1,publish_key:V(Kb,"pub-key"),subscribe_key:V(Kb,"sub-key"),ssl:!document.location.href.indexOf("https")||"on"==V(Kb,"ssl"),origin:V(Kb,"origin"),uuid:V(Kb,"uuid")});window.jQuery&&(window.jQuery.PUBNUB=Jb);"undefined"!==typeof module&&(module.exports=PUBNUB)&&wa();var Db=mb("pubnubs")||0;if(Ib){sb(Ib,{position:"absolute",top:-D});if("opera"in window||V(Ib, +"flash"))Ib.innerHTML="";PUBNUB.rdx=function(a,d){if(!d)return $[a].onerror();$[a].responseText=unescape(d);$[a].onload()};$.id=D}} +var Mb=PUBNUB.ws=function(a,d){if(!(this instanceof Mb))return new Mb(a,d);var b=this,a=b.url=a||"";b.protocol=d||"Sec-WebSocket-Protocol";var c=a.split("/"),c={ssl:"wss:"===c[0],origin:c[2],publish_key:c[3],subscribe_key:c[4],channel:c[5]};b.CONNECTING=0;b.OPEN=1;b.CLOSING=2;b.CLOSED=3;b.CLOSE_NORMAL=1E3;b.CLOSE_GOING_AWAY=1001;b.CLOSE_PROTOCOL_ERROR=1002;b.CLOSE_UNSUPPORTED=1003;b.CLOSE_TOO_LARGE=1004;b.CLOSE_NO_STATUS=1005;b.CLOSE_ABNORMAL=1006;b.onclose=b.onerror=b.onmessage=b.onopen=b.onsend= +C();b.binaryType="";b.extensions="";b.bufferedAmount=0;b.trasnmitting=A;b.buffer=[];b.readyState=b.CONNECTING;if(!a)return b.readyState=b.CLOSED,b.onclose({code:b.CLOSE_ABNORMAL,reason:"Missing URL",wasClean:y}),b;b.e=PUBNUB.init(c);b.e.k=c;b.k=c;b.e.subscribe({restore:A,channel:c.channel,disconnect:b.onerror,reconnect:b.onopen,error:function(){b.onclose({code:b.CLOSE_ABNORMAL,reason:"Missing URL",wasClean:A})},callback:function(a){b.onmessage({data:a})},connect:function(){b.readyState=b.OPEN;b.onopen()}})}; +Mb.prototype.send=function(a){var d=this;d.e.publish({channel:d.e.k.channel,message:a,callback:function(a){d.onsend({data:a})}})}; +})();