diff --git a/ConcatenateBlobs/ConcatenateBlobs.js b/ConcatenateBlobs/ConcatenateBlobs.js index c33b6dc6..1ff749d7 100644 --- a/ConcatenateBlobs/ConcatenateBlobs.js +++ b/ConcatenateBlobs/ConcatenateBlobs.js @@ -1,4 +1,4 @@ -// Last time updated at Sep 15, 2014, 08:32:23 +// Last time updated at Nov 18, 2014, 08:32:23 // Latest file can be found here: https://cdn.webrtc-experiment.com/ConcatenateBlobs.js @@ -39,16 +39,17 @@ buffers.forEach(function(buffer) { byteLength += buffer.byteLength; }); + var tmp = new Uint16Array(byteLength); - var lastOffset = 0; buffers.forEach(function(buffer) { // BYTES_PER_ELEMENT == 2 for Uint16Array - if (buffer.byteLength % 2 != 0) { - delete buffer[byteLength - 1]; + var reusableByteLength = buffer.byteLength; + if (reusableByteLength % 2 != 0) { + buffer = buffer.slice(0, reusableByteLength - 1) } tmp.set(new Uint16Array(buffer), lastOffset); - lastOffset += buffer.byteLength; + lastOffset += reusableByteLength; }); var blob = new Blob([tmp.buffer], { diff --git a/ConcatenateBlobs/README.md b/ConcatenateBlobs/README.md index b8e5f6bc..68fd6511 100644 --- a/ConcatenateBlobs/README.md +++ b/ConcatenateBlobs/README.md @@ -21,6 +21,9 @@ To use it: ``` https://cdn.webrtc-experiment.com/ConcatenateBlobs.js + +// or +https://www.webrtc-experiment.com/ConcatenateBlobs.js ``` ## 2. Use it @@ -38,13 +41,7 @@ ConcatenateBlobs([arrayOfBlobs], 'audio/wav', function(resultingBlob) { ## Credits -[Muaz Khan](https://github.com/muaz-khan): - -1. Personal Webpage: http://www.muazkhan.com -2. Email: muazkh@gmail.com -3. Twitter: https://twitter.com/muazkh and https://twitter.com/WebRTCWeb -4. Google+: https://plus.google.com/+WebRTC-Experiment -5. Facebook: https://www.facebook.com/WebRTC +* [Muaz Khan](http://www.MuazKhan.com/) ## License diff --git a/ConcatenateBlobs/index.html b/ConcatenateBlobs/index.html index a6357bfb..f38b7d3f 100644 --- a/ConcatenateBlobs/index.html +++ b/ConcatenateBlobs/index.html @@ -7,7 +7,7 @@

ConcatenateBlobs | JavaScript - ConcatenateBlobs | JavaScript -

How to use?

diff --git a/ConcatenateBlobs/package.json b/ConcatenateBlobs/package.json index ba7265a3..286b52b2 100644 --- a/ConcatenateBlobs/package.json +++ b/ConcatenateBlobs/package.json @@ -1,7 +1,7 @@ { "name": "concatenateblobs", "preferGlobal": true, - "version": "1.0.0", + "version": "1.0.2", "author": { "name": "Muaz Khan", "email": "muazkh@gmail.com", @@ -42,6 +42,6 @@ "email": "muazkh@gmail.com" }, "homepage": "https://www.webrtc-experiment.com/ConcatenateBlobs/", - "_id": "concatenateblobs@1.0.0", + "_id": "concatenateblobs@", "_from": "concatenateblobs@" } diff --git a/DataChannel/DataChannel.js b/DataChannel/DataChannel.js index e8539527..aafb6a4e 100644 --- a/DataChannel/DataChannel.js +++ b/DataChannel/DataChannel.js @@ -1,4 +1,4 @@ -// Last time updated at May 21, 2014, 08:32:23 +// Last time updated at Nov 21, 2014, 08:32:23 // Muaz Khan - www.MuazKhan.com // MIT License - www.WebRTC-Experiment.com/licence @@ -172,7 +172,7 @@ prepareInit(function () { init(); - if (IsDataChannelSupported) dataConnector.createRoom(); + if (IsDataChannelSupported) dataConnector.createRoom(_channel); }); }; @@ -536,7 +536,6 @@ var defaultSocket = root.openSignalingChannel({ onmessage: function (response) { if (response.userToken == self.userToken) return; - if (isGetNewRoom && response.roomToken && response.broadcaster) config.ondatachannel(response); if (response.newParticipant) onNewParticipant(response.newParticipant); @@ -558,8 +557,8 @@ }); return { - createRoom: function () { - self.roomToken = uniqueToken(); + createRoom: function (roomToken) { + self.roomToken = roomToken || uniqueToken(); isbroadcaster = true; isGetNewRoom = false; diff --git a/DataChannel/README.md b/DataChannel/README.md index 3db5a6f3..1237afb4 100644 --- a/DataChannel/README.md +++ b/DataChannel/README.md @@ -1,12 +1,12 @@ -##### [DataChannel.js](https://github.com/muaz-khan/WebRTC-Experiment/tree/master/DataChannel) : A JavaScript wrapper library for RTCDataChannel APIs / [Demos](https://www.webrtc-experiment.com/#DataChannel) +# [DataChannel.js](https://github.com/muaz-khan/WebRTC-Experiment/tree/master/DataChannel) : A JavaScript wrapper library for RTCDataChannel APIs / [Demos](https://www.webrtc-experiment.com/#DataChannel) DataChannel.js is a JavaScript library useful to write many-to-many i.e. group file/data sharing or text chat applications. Its syntax is easier to use and understand. It highly simplifies complex tasks like any or all user rejection/ejection; direct messages delivery; and more. If you want all DataChannel.js functionalities along with media streaming and runtime additions/deletions then [RTCMultiConnection.js](http://www.rtcmulticonnection.org/) is a good chose with similar APIs/syntax. -= +* [DataChanel.js and Reliable Signaling](https://github.com/muaz-khan/Reliable-Signaler/tree/master/datachannel-client) -##### Features +## Features 1. Direct messages — to any user using his `user-id` 2. Eject/Reject any user — using his `user-id` @@ -20,17 +20,13 @@ If you want all DataChannel.js functionalities along with media streaming and ru 10. Multi-longest strings/files concurrent 11. File queue support added. Previously shared files will be auto transmitted to each new peer. -= - -##### [Demos using DataChannel.js](https://www.webrtc-experiment.com/#DataChannel) +## [Demos using DataChannel.js](https://www.webrtc-experiment.com/#DataChannel) 1. [DataChannel basic demo](https://www.webrtc-experiment.com/DataChannel/) 2. [Auto Session Establishment and Users presence detection](https://www.webrtc-experiment.com/DataChannel/auto-session-establishment.html) 3. [Text Chat using Pusher and DataChannel.js](http://webrtc-chat-demo.pusher.io/) -= - -##### Try a Quick Demo (Text Chat) +## Try a Quick Demo (Text Chat) ```html @@ -74,26 +70,20 @@ document.querySelector('button#setup-datachannel').onclick = function () { ``` -= - -##### First Step: Link the library +## First Step: Link the library ```html ``` -= - -##### Last Step: Start using it! +## Last Step: Start using it! ```javascript var channel = new DataChannel('[optional] channel-name'); channel.send(file || data || 'text-message'); ``` -= - -##### open/connect data channels +## open/connect data channels ```javascript // to create/open a new channel @@ -103,9 +93,7 @@ channel.open('channel-name'); channel.connect('channel-name'); ``` -= - -##### `onopen` and `onmessage` +## `onopen` and `onmessage` If you're familiar with WebSockets; these two methods works in the exact same way: @@ -116,9 +104,7 @@ channel.onmessage = function(message, userid, latency) { } `user-ids` can be used to send **direct messages** or to **eject/leave** any user: -= - -##### `ondatachannel` +## `ondatachannel` Allows you show list of all available data channels to the user; and let him choose which one to join: @@ -137,9 +123,7 @@ channel.ondatachannel = function(data_channel) { }; ``` -= - -##### Use custom user-ids +## Use custom user-ids ```javascript channel.userid = 'predefined-userid'; @@ -147,41 +131,31 @@ channel.userid = 'predefined-userid'; Remember; custom defined `user-id` must be unique username. -= - -##### Direct messages +## Direct messages ```javascript channel.channels[userid].send(file || data || 'text message'); ``` -= - -##### Eject/Reject users using their `user-ids` +## Eject/Reject users using their `user-ids` ```javascript channel.eject(userid); // throw a user out of your room! ``` -= - -##### Close/Leave the entire room +## Close/Leave the entire room ```javascript channel.leave(); // close your own entire data session ``` -= - -##### Detect users' presence +## Detect users' presence ```javascript channel.onleave = function(userid) { }; ``` -= - -##### Auto Close Entire Session +## Auto Close Entire Session When room initiator leaves; you can enforce auto-closing of the entire session. By default: it is `false`: @@ -198,9 +172,7 @@ channel.autoCloseEntireSession = true; channel.leave(); // closing entire session ``` -= - -##### `uuid` for files +## `uuid` for files You can get `uuid` for each file (being sent) like this: @@ -209,9 +181,7 @@ channel.send(file); var uuid = file.uuid; // "file"-Dot-uuid ``` -= - -##### To Share files +## To Share files ```javascript var progressHelper = {}; @@ -252,9 +222,8 @@ function updateLabel(progress, label) { label.innerHTML = position + '%'; } ``` -= -##### File Queue +## File Queue File Queue support added to make sure newly connected users gets all previously shared files. @@ -264,9 +233,7 @@ You can see list of previously shared files: console.log( channel.fileQueue ); ``` -= - -##### Auto-Save file to Disk +## Auto-Save file to Disk By default; `autoSaveToDisk` is set to `true`. When it is `true`; it will save file to disk as soon as it is received. To prevent auto-saving feature; just set it `false`: @@ -280,9 +247,7 @@ channel.onFileReceived = function (fileName, file) { }; ``` -= - -##### Latency Detection +## Latency Detection ```javascript channel.onmessage = function(message, userid, latency) { @@ -290,9 +255,7 @@ channel.onmessage = function(message, userid, latency) { }; ``` -= - -##### Concurrent Transmission +## Concurrent Transmission You can send multiple files concurrently; or multiple longer text messages: @@ -310,9 +273,7 @@ channel.send('longer string-2'); channel.send('longer string-3'); ``` -= - -##### Errors Handling +## Errors Handling ```javascript // error to open data ports @@ -322,9 +283,7 @@ channel.onerror = function(event) {} channel.onclose = function(event) {} ``` -= - -##### Data session direction +## Data session direction Default direction is `many-to-many`. @@ -342,16 +301,12 @@ https://github.com/muaz-khan/WebRTC-Experiment/blob/master/Signaling.md Remember, you can use any signaling implementation that exists out there without modifying any single line! Just skip below code and open [above link](https://github.com/muaz-khan/WebRTC-Experiment/blob/master/Signaling.md)! -= - ## Resources 1. Video Presentation for `openSignalingChannel`: https://vimeo.com/91780227 2. Documentation for `openSignalingChannel`: http://www.rtcmulticonnection.org/docs/openSignalingChannel/ -= - -##### Use [your own socket.io for signaling](https://github.com/muaz-khan/WebRTC-Experiment/blob/master/socketio-over-nodejs) +## Use [your own socket.io for signaling](https://github.com/muaz-khan/WebRTC-Experiment/blob/master/socketio-over-nodejs) ```javascript dataChannel.openSignalingChannel = function(config) { @@ -375,15 +330,13 @@ dataChannel.openSignalingChannel = function(config) { }; ``` -= - -##### Use Pusher for signaling +## Use Pusher for signaling A demo & tutorial available here: http://pusher.com/tutorials/webrtc_chat Another link: http://www.rtcmulticonnection.org/docs/openSignalingChannel/#pusher-signaling -##### Use [firebase for signaling](http://www.rtcmulticonnection.org/docs/openSignalingChannel/#firebase-signaling) +## Use [firebase for signaling](http://www.rtcmulticonnection.org/docs/openSignalingChannel/#firebase-signaling) ```javascript // firebase stores data on their servers @@ -410,10 +363,10 @@ channel.openSignalingChannel = function (config) { }; ``` -= - ## Other Signaling resources +* [DataChanel.js and Reliable Signaling](https://github.com/muaz-khan/Reliable-Signaler/tree/master/datachannel-client) + 1. [XHR for Signaling](http://www.rtcmulticonnection.org/docs/openSignalingChannel/#xhr-signaling) 2. [WebSync for Signaling](http://www.rtcmulticonnection.org/docs/openSignalingChannel/#websync-signaling) 3. [SignalR for Signaling](http://www.rtcmulticonnection.org/docs/openSignalingChannel/#signalr-signaling) @@ -421,9 +374,7 @@ channel.openSignalingChannel = function (config) { 5. [Firebase for Signaling](http://www.rtcmulticonnection.org/docs/openSignalingChannel/#firebase-signaling) 6. [PubNub for Signaling](http://www.rtcmulticonnection.org/docs/openSignalingChannel/#pubnub-signaling) -= - -##### `transmitRoomOnce` +## `transmitRoomOnce` `transmitRoomOnce` is preferred when using Firebase for signaling. It saves bandwidth and asks DataChannel.js library to not repeatedly transmit room details. @@ -431,9 +382,7 @@ channel.openSignalingChannel = function (config) { channel.transmitRoomOnce = true; ``` -= - -##### Browser Support +## Browser Support [DataChannel.js](https://github.com/muaz-khan/WebRTC-Experiment/tree/master/DataChannel) works fine on following browsers: @@ -443,8 +392,6 @@ channel.transmitRoomOnce = true; | Google Chrome | [Stable](https://www.google.com/intl/en_uk/chrome/browser/) / [Canary](https://www.google.com/intl/en/chrome/browser/canary.html) / [Beta](https://www.google.com/intl/en/chrome/browser/beta.html) / [Dev](https://www.google.com/intl/en/chrome/browser/index.html?extra=devchannel#eula) | | Android | [Chrome Beta](https://play.google.com/store/apps/details?id=com.chrome.beta&hl=en) | -= - -##### License +## License [DataChannel.js](https://github.com/muaz-khan/WebRTC-Experiment/tree/master/DataChannel) is released under [MIT licence](https://www.webrtc-experiment.com/licence/) . Copyright (c) [Muaz Khan](https://plus.google.com/+MuazKhan). diff --git a/README.md b/README.md index f72e1c5d..f44801b2 100644 --- a/README.md +++ b/README.md @@ -46,6 +46,7 @@ You don't need to modify any single line to use it. No single installation or mo 11. [Chrome-Extensions](https://github.com/muaz-khan/Chrome-Extensions) 12. [Firefox-Extensions](https://github.com/muaz-khan/Firefox-Extensions) 13. [WebRTC Scalable Broadcast](https://github.com/muaz-khan/WebRTC-Scalable-Broadcast) +14. [Reliable Signaling](https://github.com/muaz-khan/Reliable-Signaler) ## Important [Experiments](https://github.com/muaz-khan/WebRTC-Experiment) diff --git a/RTCMultiConnection/.gitignore b/RTCMultiConnection/.gitignore new file mode 100644 index 00000000..3fb9d160 --- /dev/null +++ b/RTCMultiConnection/.gitignore @@ -0,0 +1,5 @@ +# Node +node_modules + +# bower +bower_components diff --git a/RTCMultiConnection/.jshintrc b/RTCMultiConnection/.jshintrc new file mode 100644 index 00000000..d7bfa51d --- /dev/null +++ b/RTCMultiConnection/.jshintrc @@ -0,0 +1,20 @@ +{ + "browser": true, + "camelcase": true, + "curly": true, + "devel": true, + "eqeqeq": true, + "forin": false, + "globalstrict": true, + "quotmark": "single", + "undef": true, + "unused": "strict", + "globals": { + "RTCMultiConnection": true, + "RMCDefaultChannel": true, + "selfNPObject": true, + "MediaStream": true, + "AudioContext": true, + "onPluginRTCInitialized": true + } +} diff --git a/RTCMultiConnection/.npmignore b/RTCMultiConnection/.npmignore new file mode 100644 index 00000000..fe416ccd --- /dev/null +++ b/RTCMultiConnection/.npmignore @@ -0,0 +1,3 @@ +node_modules +lib-cov +npm-debug.log \ No newline at end of file diff --git a/RTCMultiConnection/.travis.yml b/RTCMultiConnection/.travis.yml new file mode 100644 index 00000000..c1cc7069 --- /dev/null +++ b/RTCMultiConnection/.travis.yml @@ -0,0 +1,10 @@ +language: node_js +node_js: + - "0.11" +install: npm install +before_script: + - npm install grunt-cli + - npm install grunt + - grunt +matrix: + fast_finish: true diff --git a/RTCMultiConnection/CHANGELOG.md b/RTCMultiConnection/CHANGELOG.md new file mode 100644 index 00000000..474f4f35 --- /dev/null +++ b/RTCMultiConnection/CHANGELOG.md @@ -0,0 +1,1893 @@ +
+### 2.0 (its latest work-in-progress release) + +``` +// install latest package +npm install rtcmulticonnection + +// use latest file (v2.*.*) + + +// or instead of installing NPM package, +// directly link the file: + + +// or + +``` + +#### Bug Fixes +* "connection.DetectRTC.screen.getChromeExtensionStatus" fixed. + + ```javascript + document.getElementById('add-screen').onclick = function() { + connection.DetectRTC.screen.extensionid = 'your-own-extension-id'; + connection.DetectRTC.screen.getChromeExtensionStatus(function(status) { + if(status == 'installed-enabled') { + connection.addStream({ screen: true, oneway: true }); + } + }); + }; + + ``` +* Bug-Fixed: Now, MediaStream is removed from "attachStreams" array when stopped. +* Bug-Fixed: Now, OfferToReceiveAudio/OfferToReceiveVideo are "NOT" forced to be false even for data-only connections. Because it affects renegotiation scenarios. +* Fixed: [issue#11](https://github.com/muaz-khan/RTCMultiConnection/issues/11) +* Fixed: "**[session](http://www.rtcmulticonnection.org/docs/session/)={data:true}**" must not having audio/video media lines. +* "[onstreamended](http://www.rtcmulticonnection.org/docs/onstreamended/)" fixed. [Ref](https://github.com/muaz-khan/RTCMultiConnection/issues/7) +* Renegotiation fixed. It was a bug in 2.*.* < 2.1.7 +* [stopRecording](http://www.rtcmulticonnection.org/docs/stopRecording/) "callback" fixed. + + ```javascript + connection.streams['streamid'].stopRecording(function(blobs) { + // blobs.video + }, { video: true }); + + ``` +* Screen negotiations fixed. Screen can be renegotiated many times. + + ```javascript + connection.addStream({ + screen: true, + oneway: true + }); + + ``` +* "connection.[candidates](http://www.rtcmulticonnection.org/docs/candidates/)" has been fixed. +* connection.[selectDevices](http://www.rtcmulticonnection.org/docs/selectDevices/) is fixed. + + ```javascript + connection.selectDevices('audioinput-deviceid', 'videooutput-deviceid'); + + ``` +* Fixed [#266](https://github.com/muaz-khan/WebRTC-Experiment/issues/266) i.e. IE11 support added through [PluginRTC](https://github.com/muaz-khan/PluginRTC) (webrtc-everywhere). +* Duplicate "enumerateDevices" listing fixed. +* **connection.sharePartOfScreen** fixed for sharing screen over multi-users: + + ```javascript + connection.sharePartOfScreen({ + element: 'body', // element to share + interval: 500 // how after take screenshots + }); + + ``` +* Fixed: TextSender is unable to send array. + + ```javascript + connection.send([1, 2, 3, 4, 5]); + + ``` +* Fixed: onStreamEvent.isAudio/onStreamEvent.isVideo seems NULL in mute/unmute cases with only {audio:true} or {video:true} + + ```javascript + connection.onmute = function(event) { + event.isAudio + event.isVideo + event.isScreen + }; + + ``` +#### Features +* Latest updates: https://github.com/muaz-khan/RTCMultiConnection/commits/master +* v2.2.1 breaking updates: + * connection.stats.sessions is removed; use connection.sessionDescriptions instead. + * connection.stats.numberOfSessions is removed; use connection.numberOfSessions instead. + * connection.stats.numberOfConnectedUsers is removed; use connection.numberOfConnectedUsers instead. + * connection.getStats and connection.stats are removed. +* onStreamEndedHandler updated. "connection.onstreamended" is fired only when both "mediaElement" and "mediaElement.parentNode" are not NULL. +* connection.onopen is now having "event.channel" object. +* 2nd invocation of "createDataChannel" is disabled. +* "connection.enableFileSharing" added. + + ```javascript + // to disable file sharing + connection.enableFileSharing = false; + + ``` +* Added: **connection.peers['target-userid'].[takeSnapshot](http://www.rtcmulticonnection.org/docs/takeSnapshot/)(callback)**; +* Added: **connection.streams['streamid'].takeSnapshot(callback)*; +* "[onleave](http://www.rtcmulticonnection.org/docs/onleave/)" is "merely" fired once for each user. +* "**sync:false**" added for "**connection.streams['streamid'].mute**" method. + + ```javascript + connection.streams.selectFirst({ local:true }).mute({ + video: true, + sync: false // mute video locally--only + }) + + // or + connection.streams['streamid'].mute({ + audio: true, + sync: false // mute audio locally--only + }) + + ``` +* "connection.[mediaConstraints](http://www.rtcmulticonnection.org/docs/mediaConstraints/)" updated. + + ```javascript + connection.mediaConstraints = { + video: { + mandatory: {}, + optional: [] + }, + audio: { + mandatory: {}, + optional: [] + } + }; + + ``` +* "**connection.rtcConfiguration**" added: + + ```javascript + connection.rtcConfiguration = { + iceTransports: 'relay', + iceServers: [iceServersArray] + } + + ``` +* Now "[onstreamended](http://www.rtcmulticonnection.org/docs/onstreamended/)" is fired merely "once" for each stream. +* **{audio:true,video:true}** are forced for Android. All media-constraints skipped. +* Firefox screen capturing is HTTPs-only. +* "**preferJSON**" is removed. Now data is "always" sent as ArrayBuffer. +* Now, [FileBufferReader](https://github.com/muaz-khan/FileBufferReader) is used for file sharing. +* FileSender/FileReceiver/FileConveter has been removed. +* onFileStart/onFileProgress/onFileEnd: now having "userid" and "extra" objects. +* When "muted" stream is negotiated; it fires "onmute" event as soon as as remote stream is received. + + ```javascript + var firstLocalStream = connection.streams.selectFirst({ + local: true + }); + + // you can mute a stream before joining a session + firstLocalStream.mute({ + video: true + }); + + connection.open(); + + ``` +* Now, "**autoReDialOnFailure**" is "true" by default. +* "**connection.enumerateDevices**" and "**connection.getMediaDevices**" added. + + ```javascript + // to iterate over all available media devices + connection.enumerateDevices(function(devices) { + devices.forEach(function(device) { + // device.deviceId + // device.kind == 'audioinput' || 'audiooutput' || 'audio' + + connection.selectDevices(device.deviceId); + }); + }); + + ``` +* "connection.changeBandwidth" added. + + ```javascript + connection.changeBandwidth({ + audio: 30, + video: 64 + }); + + ``` +* "connection.streams.remove" added. + + ```javascript + // fire "onstreamended" for all screen streams + connection.streams.remove({ + isScreen: true + }); + + // fire "onstreamended" for all local streams + connection.streams.remove({ + local: true + }); + + // fire "onstreamended" for all remote audio-only streams + connection.streams.remove({ + isAudio: true, + remote: true + }); + + ``` +* "connection.streams.selectFirst" and "connection.streams.selectAll" added. + + ```javascript + // first local stream + var firstLocalStream = connection.streams.selectFirst({ + local: true + }); + + // all audio-only streams + var allAudioOnlyStreams = connection.streams.selectAll({ + isAudio: true + }); + + // a user's all streams + var firstLocalScreenStream = connection.streams.selectAll({ + userid: 'remote-userid' + }); + + ``` +* "connection.streams.stop" improved. + + ```javascript + // stop a user's all screen streams + connection.streams.stop({ + userid: 'remote-userid', + screen: true + }); + + ``` +* "**connection.privileges.canStopRemoteStream**" and "**connection.privileges.canMuteRemoteStream**" added: + + ```javascript + // set it "true" if you want to allow user to stop/mute remote stream + connection.privileges = { + canStopRemoteStream: true, // user can stop remote streams + canMuteRemoteStream: true // user can mute remote streams + }; + + // otherwise, for "false" values, if user will try to programmatically invoke "stop" or "mute" method, + // you'll be informed in the "onstatechange" event. + connection.onstatechange = function(state) { + if(state.name == 'stop-request-denied') { + alert(state.reason); + } + + if(state.name == 'mute-request-denied') { + alert(state.reason); + } + }; + + ``` +* Breaking changes for "connection.onstatechange". Now an "object" is passed over "onstatechange" + + ```javascript + connection.onstatechange = function(state) { + // state.userid == 'target-userid' || 'browser' + // state.extra == 'target-user-extra-data' || {} + // state.name == 'short name' + // state.reason == 'longer description' + if(state.name == 'stop-request-denied') { + alert(state.reason); + } + }; + + ``` +* "connection.[streams](http://www.rtcmulticonnection.org/docs/streams/).mute" updated: + + ```javascript + // mute all remote audio-only streams. + connection.streams.mute({ + isAudio: true, + remote: true + }); + + // unmute all local screen streams. + connection.streams.unmute({ + isScreen: true, + local: true + }); + + ``` +* ""connection.UA"" updated for "**is\*****": + + ```javascript + connection.UA.isChrome + connection.UA.isFirefox + connection.UA.isIE + connection.UA.isOpera + connection.UA.isSafari + connection.UA.isMobileDevice + connection.UA.version + + ``` +* **connection.preferJSON=true** added. You can set "**false**" to send non-Blob types i.e. **ArrayBuffer/ArrayBufferView/DataView** etc. + + ```javascript + // Remember: it doesn't applies to file sharing. + // it applies to all other kinds of data. + connection.preferJSON = false; + + // send array buffer + connection.send( new ArrayBuffer(10) ); + + // get array buffer + connection.onmessage = function(event) { + var buffer = event.data; + }; + + // convert string to array-buffer + connection.send( str2ab('a string test') ); + + // parsing array-buffer back into string + connection.onmessage = function(event) { + var string = ab2str(event.data); + }; + + // this method converts array-buffer into string + function ab2str(buf) { + return String.fromCharCode.apply(null, new Uint16Array(buf)); + } + + // this method converts string into array-buffer + function str2ab(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; + } + + ``` +* **connection.getExternalIceServers** is now "**false**" by default. If you want to use XirSys based ICE-Servers in your application, you MUST set it "**true**": + + ```javascript + // if you want to use XirSys-based STUN/TURN servers + connection.getExternalIceServers = true; + + ``` +* Now, "**[sendCustomMessage](http://www.rtcmulticonnection.org/docs/sendCustomMessage/)**", "**[addStream](http://www.rtcmulticonnection.org/docs/addStream/)**", "**switchStream**", "**[renegotiate](http://www.rtcmulticonnection.org/docs/renegotiate/)**" and "**[removeStream](http://www.rtcmulticonnection.org/docs/removeStream/)**" can be called even if there is no user connected. + + ```javascript + var connection = new RTCMultiConnection(); + + // quickly after initializing constructor + // call addStream to add screen + // it will wait until a user is connected + // and it will auto share/renegotiate your screen with first user + connection.addStream({ + screen: true, + oneway: true + }); + + // send a custom message + // it will be sent as soon as first user connects with you + connection.sendCustomMessage('hi, there'); + + ``` +* Added support for initial "inactive" sessions. You can setup audio/video connection however streams will be "inactive": + + ```javascript + // streams are on-hold: + connection.session = { + inactive: true, + audio: true, + video: true + }; + + // to unhold streams later + connection.unhold('both'); // both audio and video + + ``` +* Added **connection.waitUntilRemoteStreamStartsFlowing**. It allows you override default Wait-Until-Remote-Stream-starts-flowing behaviour. + + ```javascript + connection.waitUntilRemoteStreamStartsFlowing = false; + + ``` +* Added: **connection.switchStream**. It will remove all old local streams and add new stream. + + ```javascript + // remove all old streams and add screen in oneway. + connection.switchStream({ + screen: true, + oneway: true + }); + + ``` +* Added **connection.disconnect**: + + ```javascript + // it means that: + // 1) close all sockets + // 2) close all peers + // 3) clear all data + // 4) refresh everything + // Note: local streams will be kept in "localStreams" object if "keepStreamsOpened" boolean is used. + connection.disconnect(); + + // it fires: + connection.ondisconnected = function(event) { + if(event.isSocketsDisconnected == true) { } + }; + + ``` +* Updated: connection.**[removeStream](http://www.rtcmulticonnection.org/docs/removeStream/)**: + + ```javascript + // remove all screen streams. + // you can use "remove all video streams" by passing "video" + // or "remove all audio streams" by passing "audio" + connection.removeStream('screen'); + + // remove-all but multiple streams + // i.e. remove all audio and video streams + // or remove all audio and screen streams + connection.removeStream({ + screen: true, + audio: true + }); + + ``` +* Updated: connection.**[streams](http://www.rtcmulticonnection.org/docs/streams/)**.stop: + + ```javascript + // stop any single stream: audio or video or screen + connection.streams.stop('screen'); + + // stop multiple streams + connection.streams.stop({ + remote: true, + video: true, + screen: true + }); + + ``` +* Data-channels "send" method improved. + + ```javascript + connection.send('longest-test' || big_array || blob || file); + + ``` +* **googTemporalLayeredScreencast** and **googLeakyBucket** added for screen capturing. +* Updated **connection.candidates**: + + ```javascript + connection.candidates = { + stun: true, // NEW property since v2.0 + turn: true, // NEW property since v2.0 + host: true + }; + + ``` +* Added **connection.localStreams**. All local streams are always kept in this object even if session is closed. Look at above section i.e. **keepStreamsOpened**. + + ```javascript + var stream = connection.localStreams['streamid']; + + // or + connection.onSessionClosed = function() { + var stream = connection.localStreams['streamid']; + }; + + // or + for(var streamid in connection.localStreams) { + var stream = connection.localStreams[streamid]; + } + + ``` +* Added **connection.log** and **connection.onlog**. It allows you display logs in UI instead of in the console. + + ```javascript + // if you want to disable logs + connection.log = false; + + connection.onlog = function(log) { + var div = document.createElement('div'); + div.innerHTML = JSON.stringify(log, null, '
'); + document.documentElement.appendChild(div); + }; + + ``` +* Added **connection.keepStreamsOpened**. It allows you keep MediaStream active even if entire session is closed. It is useful in session-reinitiation scenarios. + + ```javascript + connection.keepStreamsOpened = true; + + ``` +* Removed: **connection.caniuse.checkIfScreenSharingFlagEnabled**. It was redundant. +* webrtc-everywhere/temasys support added for Safari & IE11. [PluginRTC](https://github.com/muaz-khan/PluginRTC) + + +### 1.9 (2014-08-10) + +``` + + +// or + + +// or + +``` + +#### Bug Fixes +* **connection.[eject](http://www.rtcmulticonnection.org/docs/eject/)** is fixed. + + ```javascript + connection.eject('target-userid'); + + // check if user is ejected + // clear rooms-list if user is ejected + connection.onSessionClosed = function (session) { + if (session.isEjected) { + warn(session.userid, 'ejected you.'); + } else warn('Session has been closed.', session); + + if (session.isEjected) { + roomsList.innerHTML = ''; + roomsList.style.display = 'block'; + } + }; + + ``` +* Fixed: **remoteEvent.streamid** and **remoteEvent.isScreen**: + + ```javascript + connection.onstream = function(event) { + if(event.isScreen) { + // it is screen + } + }; + + ``` +* **[connection.session](http://www.rtcmulticonnection.org/docs/session/)={}** fixed. It allows moderator/initiator to become a listener/viewer i.e. it supports many-to-one scenarios: + + ```javascript + // for initiator + connection.session = {}; + + // for participants + connection.onNewSession = function(session) { + session.join({ + audio: true, + video: true + }); + }; + + ``` +#### Features +* Workaround-added: [Firefox don't yet support onended for any stream](https://bugzilla.mozilla.org/show_bug.cgi?id=1045810) (remote/local) +* RTCMultiConnection is updated for **audio+screen** from single getUserMedia request for Firefox Nightly. Below snippet is sharing single video stream containing both audio/video tracks; and target browser is joining with only audio. Screen can be viewed on both chrome and Firefox. If you'll share from chrome, then it will be making multiple getUserMedia requests. + + ```javascript + // audio+video+screen will become audio+screen for Firefox + // because Firefox isn't supporting multi-streams feature + + // initiator from Firefox + initiator.session = { + screen: true, + audio: true + }; + + // participant in chrome or Firefox + participant.onNewSession = function(session) { + session.join({ audio: true }); + }; + + ``` +* Screen capturing support for Firefox nightly added. You simply need to open "**about:config**" on Firefox nightly and set "**media.getusermedia.screensharing.enabled**" to "**true**". + + ```javascript + // same for Firefox + connection.session = { + screen: true, + oneway: true + }; + + ``` +* **connection.[dontCaptureUserMedia](http://www.rtcmulticonnection.org/docs/dontCaptureUserMedia/)** added: + + ```javascript + connection.dontCaptureUserMedia = true; + + ``` +* connection.**[dontAttachStream](http://www.rtcmulticonnection.org/docs/dontAttachStream/)** updated: + + ```javascript + connection.dontAttachStream = true; + + ``` +* **connection.onstreamid** added: + + ```javascript + // on getting remote stream's clue + connection.onstreamid = function (e) { + var mediaElement = document.createElement(e.isAudio ? 'audio' : 'video'); + mediaElement.controls = true; + mediaElement.poster = connection.resources.muted; + mediaElement.id = e.streamid; + connection.body.appendChild(mediaElement); + }; + + ``` +* **connection.peers['target-userid'].getStats** added. + + ```javascript + connection.peers['target-userid'].peer.getStats(function (result) { + // many useful statistics here + }); + + ``` +* **connection.onconnected** added. + + ```javascript + connection.onconnected = function (event) { + log('Peer connection has been established between you and', event.userid); + + // event.peer.addStream || event.peer.removeStream || event.peer.changeBandwidth + // event.peer == connection.peers[event.userid] + + event.peer.getStats(function (result) { + // many useful statistics here + }); + }; + + ``` +* **connection.onfailed** added. + + ```javascript + connection.onfailed = function (event) { + event.peer.renegotiate(); + // or event.peer.redial(); + // event.targetuser.browser == 'firefox' || 'chrome' + }; + + ``` +* Screen capturing is improved, and [single google chrome extension](https://chrome.google.com/webstore/detail/screen-capturing/ajhifddimkapgcifgcodmmfdlknahffk) is used to support capturing from all domains! +* **connection.[processSdp](http://www.rtcmulticonnection.org/docs/processSdp/)** added. + + ```javascript + connection.processSdp = function(sdp) { + sdp = remove_vp8_codecs(sdp); + sdp = prefer_opus (sdp); + sdp = use_maxaveragebitrate(sdp); + return sdp; + }; + + ``` +* **[connection.mediaConstraints](http://www.rtcmulticonnection.org/docs/mediaConstraints/)** and **[connection.media](http://www.rtcmulticonnection.org/docs/media/)** are updated: + + ```javascript + connection.mediaConstraints = { + mandatory: { + maxWidth: 1920, + maxHeight: 1080, + minAspectRatio: 1.77, + + minFrameRate: 3, + maxFrameRate: 64 + }, + optional: [ + bandwidth: 256 + ] + }; + + ``` +* **[connection.onstream](http://www.rtcmulticonnection.org/docs/onstream/)** is updated for **event.isScreen**: + + ```javascript + connection.onstream = function (event) { + if(event.isScreen) { + // it is screen stream + } + + if(event.isAudio) { + // it is audio-only stream + } + + if(event.isVideo) { + // it is audio+video stream + } + }; + + ``` +* **[connection.refresh](http://www.rtcmulticonnection.org/docs/refresh/)** is updated and session re-initiation is improved. + + ```javascript + // you simply need to invoke "connection.leave" to + // leave a session so that you can rejoin same session + connection.onstatechange = function (state) { + if(state == 'connected-with-initiator') { + document.getElementById('leave-session').disabled = false; + } + }; + + document.getElementById('leave-session').onclick = function() { + connection.leave(); + }; + + ``` +* **connection.iceProtocols** added. + + ```javascript + connection.iceProtocols = { + tcp: true, // prefer using TCP-candidates + udp: true // prefer using UDP-candidates + }; + + ``` +* Use custom chrome extension for screen capturing: + + ```javascript + connection.DetectRTC.screen.extensionid = 'your-app-store-extensionid'; + + ``` +* STUN/TURN servers are updated; as well as ICE-servers from XirSys are used: + + ```javascript + // to disable XirSys ICE-Servers + connection.getExternalIceServers = false; + + ``` +* **connection.preventSSLAutoAllowed** is disabled. + + ```javascript + // to enable it + connection.preventSSLAutoAllowed = true; + + ``` + + +### 1.8 (2014-06-28) + +``` + +``` + +#### Bug Fixes +* Fixed: If Chrome starts video-only session; and Firefox joins with only audio. Then both fails to connect; though sendrecv/recvonly/sendonly everything is correctly implemented. +* Fixed: "the videos are not square and they look grainy not has sharp as before". Now video is captured & streamed with better quality. +* [startRecording](http://www.rtcmulticonnection.org/docs/startRecording/)/[stopRecording](http://www.rtcmulticonnection.org/docs/stopRecording/) updated & fixed. + + ```javascript + // record both audio and video + connection.streams['stream-id'].startRecording({ + audio: true, + video: true + }); + + // stop both audio and video + connection.streams['stream-id'].stopRecording(function (blob) { + // blob.audio --- audio blob + // blob.video --- video blob + }, {audio:true, video:true} ); + + ``` +* Fixed **connection.streams.stop()** via [issue #255](https://github.com/muaz-khan/WebRTC-Experiment/issues/225#issuecomment-46283072). +#### Features +* (to fix canary ipv6 candidates issues): disabled "googIPv6", "googDscp" and "googImprovedWifiBwe" +* "**connection.leaveOnPageUnload**" added. + + ```javascript + // if you want to prevent/override/bypass default behaviour + connection.leaveOnPageUnload = false; + + // display a notification box + window.addEventListener('beforeunload', function () { + return 'Are you want to leave?'; + }, false); + + // leave here + window.addEventListener('unload', function () { + connection.leave(); + }, false); + + ``` + + * renegotiation scenarios that fails: + * if chrome starts video-only session and firefox joins with only audio + * if chrome starts with audio-only session and firefox joins with only video + * if chrome starts only audio and firefox joins with audio+video + * renegotiation scenarios that works: + * if chrome starts audio+video and firefox joins with only audio or audio+video + * if both browsers has similar streams + +* "connection.[onstatechange](http://www.rtcmulticonnection.org/docs/onstatechange/)" added: + + ```javascript + connection.onstatechange = function (state, reason) { + // fetching-usermedia + // usermedia-fetched + + // detecting-room-presence + // room-not-available + // room-available + + // connecting-with-initiator + // connected-with-initiator + + // failed---has reason + + // request-accepted + // request-rejected + + if(state == 'room-not-available') { + // room no longer exist + } + }; + + ``` +Remember, older "[onstats](http://www.rtcmulticonnection.org/docs/onstats/)" event has been removed in v1.8. +* Now if you'll invoke "**[connection.sharePartOfScreen(...)](http://www.rtcmulticonnection.org/docs/sharePartOfScreen/)*" and a new user will join you; existing part of screen will be auto shared with him. + + It means that "**[sharePartOfScreen](http://www.rtcmulticonnection.org/docs/sharePartOfScreen/)**" will work with all new/old users. +* "**connection.donotJoin**" added: + + ```javascript + connection.onstatechange = function (state) { + if(state == 'room-not-available') { + connection.donotJoin(connection.sessionid); + } + }; + + ``` +* You can set **connection.DetectRTC.screen.extensionid="your-chrome-extensionid**" to make sure inline (newly) installed chrome extension is quickly used for screen capturing instead of prompting user to reload page once to use it. + + It means that install the chrome extension and RTCMultiConnection will auto use it. Don't ask your users to reload the page: + + ```javascript + connection.DetectRTC.screen.extensionid = 'ajhifddimkapgcifgcodmmfdlknahffk'; + + ``` +* "**connection.[DetectRTC](http://www.rtcmulticonnection.org/docs/DetectRTC/).hasSpeakers**" added. +* "connection.[resumePartOfScreenSharing()](http://www.rtcmulticonnection.org/docs/resumePartOfScreenSharing/)" added. +* "**event.blobURL**" in the [onstream](http://www.rtcmulticonnection.org/docs/onstream/) event is fixed for Firefox. + + ```javascript + connection.onstream = function(e) { + // e.blobURL -- now it is always blob:URI + }; + + ``` +* "PreRecordedMediaStreamer" is moved to a separate javascript file. +>https://www.rtcmulticonnection.org/PreRecordedMediaStreamer.js +* function "**stopTracks**" updated. +* Now, you can easily manage external resources/URLs using "**connection.resources**": + + ```javascript + connection.resources = { + RecordRTC: 'https://www.webrtc-experiment.com/RecordRTC.js', + PreRecordedMediaStreamer: 'https://www.rtcmulticonnection.org/PreRecordedMediaStreamer.js', + customGetUserMediaBar: 'https://www.webrtc-experiment.com/navigator.customGetUserMediaBar.js', + html2canvas: 'https://www.webrtc-experiment.com/screenshot.js', + hark: 'https://www.rtcmulticonnection.org/hark.js', + firebase: 'https://www.rtcmulticonnection.org/firebase.js', + firebaseio: 'https://chat.firebaseIO.com/', + muted: 'https://www.webrtc-experiment.com/images/muted.png' + }; + + ``` +* **[connection.DetectRTC.MediaDevices](http://www.rtcmulticonnection.org/docs/DetectRTC/)** added: + + ```javascript + // to iterate over all available media devices + connection.getDevices(function() { + connection.DetectRTC.MediaDevices.forEach(function(device) { + // device.deviceId + // device.kind == 'audioinput' || 'audiooutput' || 'audio' + + connection.selectDevices(device.deviceId); + }); + }); + + ``` +* Now, **hark.js** is used instead of **SoundMeter.js**: + + ```javascript + connection.onspeaking = function() {}; + connection.onsilence = function() {}; + + ``` +* **captureUserMediaOnDemand** added for **connection.open** method: + + ```javascript + // it is "disabled" by default + // captureUserMediaOnDemand means that "getUserMedia" API for initiator will + // be invoked only when required. + // i.e. when first participant is detected. + + // you can enable it by setting it to "true" + connection.open({ + captureUserMediaOnDemand: true + }); + + ``` +* **connection.DetectRTC.screen.getChromeExtensionStatus** added. + + ```javascript + var extensionid = 'ajhifddimkapgcifgcodmmfdlknahffk'; + + connection.DetectRTC.screen.getChromeExtensionStatus(extensionid, function(status) { + if(status == 'installed-enabled') { + // chrome extension is installed & enabled. + } + + if(status == 'installed-disabled') { + // chrome extension is installed but disabled. + } + + if(status == 'not-installed') { + // chrome extension is not installed + } + + if(status == 'not-chrome') { + // using non-chrome browser + } + }); + + ``` +* **onMediaCaptured** added for **connection.open** method: + + ```javascript + connection.open({ + onMediaCaptured: function() { + // initiator enable camera/microphone + // you can share "sessionDescription" with other users + // and they can quickly join initiator! + } + }); + + ``` +* **openSignalingChannel** is moved to "**setDefaults**" private function. +* **connection.preventSSLAutoAllowed added**. Now RTCMultiConnection focuses more on end-users privacy! You can ask RTCMultiConnection to "always" display "getUserMedia-permission-bar" even if chrome is running on HTTPs i.e. SSL domain: + + ```javascript + // by default "preventSSLAutoAllowed" is true only for "HTTPs" domains + // you can force it for HTTP domains as well by setting this Boolean in your HTML page. + connection.preventSSLAutoAllowed = true; + + ``` +* **onScreenCapturingExtensionAvailable** is fired when RTCMultiConnection detects that chrome extension for screen capturing is installed and available: + + ```javascript + connection.onScreenCapturingExtensionAvailable = function() { + btnInlineInstallButton.disabled = true; + }; + + ``` +* Now, **[connection.join](http://www.rtcmulticonnection.org/docs/join/)** method allows you force how to join (i.e. with or without streams etc.): + + ```javascript + // it doesn't matter if incoming stream is audio+video + // you can join it with only audio or with only video + // or anonymously i.e. { oneway: true } + var joinWith = { + audio: true + }; + + connection.join('sessionid', joinWith); // 2nd parameter + + ``` +* Now, [onNewSession](http://www.rtcmulticonnection.org/docs/onNewSession/) is fired once for each room. It will NEVER fire multiple times. +* **[chrome.desktopCapture.chooseDesktopMedia](https://github.com/muaz-khan/WebRTC-Experiment/tree/master/desktop-sharing)** is now preferred for screen capturing; and if extension is not installed or disabled, then RTCMultiConnection will auto fallback to [command-line flag oriented screen-capturing API](https://www.webrtc-experiment.com/Pluginfree-Screen-Sharing/); and if both are not available then it will throw a clear "human readable" exception. + + Chrome extension is available [here](https://www.webrtc-experiment.com/store/capture-screen/). +* You can use **connection.DetectRTC** like this: + + ```javascript + connection.DetectRTC.load(function() { + if(connection.DetectRTC.hasMicrophone) { } + if(connection.DetectRTC.hasWebcam) { } + }); + + connection.DetectRTC.screen.isChromeExtensionAvailable(function(available) { + if(available) alert('Chrome screen capturing extension is installed and available.'); + }): + + ``` +* **navigator.getUserMedia** errors handling in **onMediaError** event: + + ```javascript + connection.onMediaError = function(error) { + if(error.name == 'PermissionDeniedError') { + alert(error.message); + } + }; + + ``` + + +### 1.7 (2014-05-14) + +``` +v1.7 focused on reliable API invocation, reliable concurrent users connectivity, and more. A few other features were added in this build like part of screen streaming, pre-recorded media streaming, ice-trickling booleans, and obviously DetectRTC! + +``` + +#### Bug Fixes +* Session re-initiation has been fixed. You can leave/rejoin many times. +* Mute/UnMute and "onstreamended" among multiple users, issues fixed. +#### Features +* **connection.trickleIce** added. Useful if you're using XHR/SIP/XMPP for signaling. [XHR Demo](https://github.com/muaz-khan/XHR-Signaling) + + ```javascript + // to ask RTCMultiConnection to wait until all ICE candidates + // are gathered; and all ice are merged in the SDP + // you JUST need to share that SDP "only"! + connection.trickleIce = false; + + ``` +* You can use "**connection.DetectRTC**" to [detect](https://github.com/muaz-khan/WebRTC-Experiment/blob/master/DetectRTC) WebRTC features! + + ```javascript + if(connection.DetectRTC.isWebRTCSupported) {} + if(connection.DetectRTC.hasMicrophone) {} + if(connection.DetectRTC.hasWebcam) {} + + ``` +* Following scenarios implemented: + * if system doesn't have any microphone attached; RTCMultiConnection will skip "**{audio:true}**" and prompt only "**{video: true}**" + * if system doesn't have any webcam attached; RTCMultiConnection will skip "**{video:true}**" and prompt only "**{audio: true}**" +* Previously "connection.renegotiatedSessions" was an array; now it is object. + + ```javascript + // sometimes you try to manually fetch media streams + // sometimes you allow a user to enable webcam ... but don't renegotiate quickly.. + // you may want to ask other user to invoke "renegotiate" function + // you may want to override default behaviours + + // below code snippet is used in MultiRTC demo in "ui-peer-connection.js" file + + var session = { + audio: true // you're manually capturing audio + }; + + connection.captureUserMedia(function (stream) { + // you can see that "renegotiatedSessions" is an object + // because we wanted to prevent duplicate entries + connection.renegotiatedSessions[ JSON.stringify(session) ] = { + session: session, + stream: stream + } + + connection.peers[message.userid].peer.connection.addStream(stream); + }, session); + + ``` +* Now, using default camera resolutions instead of using minWidth/minHeight and maxWidth/maxHeight. You can [easily override](http://www.rtcmulticonnection.org/docs/mediaConstraints/) those values: + + ```javascript + connection.mediaConstraints.mandatory = { + minWidth: 1280, + maxWidth: 1280, + minHeight: 720, + maxHeight: 720, + minAspectRatio: 1.77 + }; + + ``` +* Now, auto redials while you're renegotiating and any single browser is firefox! +* Previously-renegotiated streams and new users renegotiation has been improved. +* Now, auto injecting **VoiceActivityDetection:true** for chrome to make sure **c=IN** is always present; otherwise **b=AS** will fail. +* **connection.dontOverrideSession** added: + + ```javascript + // dont-override-session allows you force RTCMultiConnection + // to not override default session of participants; + // by default, session is always overridden and set to the session coming from initiator! + connection.dontOverrideSession = true; + + ``` +* connection.askToShareParticipants and connection.shareParticipants added. Useful in [multi-broadcasters and many viewers scenarios](https://www.webrtc-experiment.com/RTCMultiConnection/Multi-Broadcasters-and-Many-Viewers.html)! + + ```javascript + connection.onstream = function (e) { + if (e.type == 'remote' && role == 'Anonymous Viewer') { + // because "viewer" joined room as "oneway:true" + // initiator will NEVER share participants + // to manually ask for participants; + // call "askToShareParticipants" method. + connection.askToShareParticipants(); + } + + // if you're moderator + // if stream-type is 'remote' + // if target user is broadcaster! + if (connection.isInitiator && e.type == 'remote' && !e.session.oneway) { + // call "shareParticipants" to manually share participants with all connected users! + connection.shareParticipants({ + dontShareWith: e.userid + }); + } + }; + + ``` +* **connection.join('sessionid')** has been improved. +* Now you can pass "sessionid" over "join" method! + + ```javascript + // make sure that room is created! + // otherwise this "join" method will fail to join the room! + // "join" method NEVER waits for onNewSession!! + // it directly "joins" the room! + connection.join('sessionid'); + + ``` +* [Admin/Guest features](http://www.rtcmulticonnection.org/docs/admin-guest/) has been removed. You should use v1.6 or earlier to use admin/guest features. +* **screen:300kbps** added in **connection.[bandwidth](http://www.rtcmulticonnection.org/docs/bandwidth/)**: + + ```javascript + // by default + connection.bandwidth = { + screen: 300 + }; + + // if you're using node-webkit; then you MUST set it to NULL + // make sure that it is set both for browser-client and node-webkit! + connection.bandwidth.screen = null; + + ``` +* Now, you can check **connection.[stats](http://www.rtcmulticonnection.org/docs/stats/).numberOfSessions** in a specific channel: + + ```javascript + alert( connection.stats.numberOfSessions ); + + // You can even access all sessions using this object-like array: + alert( connection.stats.sessions['sessionid'] ); + + // or + for(var session in connection.stats.sessions) { + console.log(connection.stats.sessions[session]); + } + + // You can get above values usnig "getStats" method as well: + connection.getStats(function(stat) { + console.log(stat.numberOfSessions); + + for(var session in stat.sessions) { + console.log(stat.sessions[session]); + } + }); + + ``` +* Now, multiple users can join a room at the same time and all will be interconnected! +* Now, "streamid" is synced among all users! You can mute/unmute or stop single stream-id and it will affect among all connected users! +* **autoReDialOnFailure** added. You can force RTCMultiConnection to auto redial if peer connection is dropped unexpectedly or failed out of any exception! + + ```javascript + // by default, it is "false" + // it is false because workaround that is used to capture connections' failures + // affects renegotiation scenarios! + connection.autoReDialOnFailure = true; + + ``` +* You can override [setDefaultEventsForMediaElement](http://www.rtcmulticonnection.org/docs/setDefaultEventsForMediaElement/) to prevent default mute/unmute handlers on media elements. + + ```javascript + // by default it captures "onplay", "onpause" and "onvolumechange" events for all media elements + connection.setDefaultEventsForMediaElement = false; + + ``` +* **onpartofscreenstopped** and **onpartofscreenpaused** added: + + ```javascript + // invoked when you "manually" stopped part-of-screen sharing! + connection.onpartofscreenstopped = function() {}; + + // invoked when you "manually" paused part-of-screen sharing! + connection.onpartofscreenpaused = function() {}; + + ``` +* Now, you can call **sharePartOfScreen**, **pausePartOfScreenSharing** and **stopPartOfScreenSharing** for all users: + + ```javascript + connection.sharePartOfScreen({ + element: '#div-id', // querySelector or HTMLElement + interval: 1000 // milliseconds + }); + + connection.pausePartOfScreenSharing(); + connection.stopPartOfScreenSharing(); + + ``` +* **hold/unhold** of individual media lines implemented: + + ```javascript + // hold only your audio + connection.peers['target-userid'].hold('audio'); + connection.onhold = function(track) { + if(track.kind == 'audio') {} + }; + + // hold only your video + connection.peers['target-userid'].hold('video'); + connection.onunhold = function(track) { + if(track.kind == 'video') {} + }; + + // unhold all your 'video' m-lines + connection.unhold('video'); + + // hold all your 'audio' m-lines + connection.hold('audio'); + + ``` +* When you mute/unmute; video-controls are synced among all users! Volume is also synced! +* **window.skipRTCMultiConnectionLogs** is replaced with **connection.skipLogs** method: + + ```javascript + connection.skipLogs(); + + ``` +* Part of screen sharing has been implemented: + + ```javascript + // to share a DIV or region of screen with a specific user + connection.peers['target-userid'].sharePartOfScreen({ + element: 'body', // querySelector or HTMLElement + interval: 1000 // milliseconds + }); + + // to capture shared parts of screen + // var image = document.querySelector('img'); + connection.onpartofscreen = function (e) { + // image.src = e.screenshot; + }; + + // to pause part-of-screen sharing + connection.peers['target-userid'].pausePartOfScreenSharing = true; + + // to stop part-of-screen sharing + connection.peers['target-userid'].stopPartOfScreenSharing = true; + + ``` +* Now, [open](http://www.rtcmulticonnection.org/docs/open/) method returns "sessionDescription" object. "open" method also accepts an object as well! + * [join](http://www.rtcmulticonnection.org/docs/join/) method can be used to join that "sessionDescription" anytime without connecting to signaling channel! + * For initiator: + + ```javascript + var initiator = new RTCMultiConnection(); + + // you can pass object instead of string! + var sessionDescription = initiator.open({ + dontTransmit: true + }); + + websocket.send(sessionDescription); + + ``` + * For participant: + + ```javascript + var participant = new RTCMultiConnection(); + + websocket.onmessage = function(event) { + var sessionDescription = event.data.sessionDescription; + participant.join( sessionDescription ); + }; + + ``` +* Now, [removeStream](http://www.rtcmulticonnection.org/docs/removeStream/) method quickly removes streams and auto renegotiates. You can also call [removeStream](http://www.rtcmulticonnection.org/docs/removeStream/) over [peers](http://www.rtcmulticonnection.org/docs/peers/) object: + + ```javascript + connection.peers['target-userid'].removeStream( 'stream-id' ); + + ``` +* Now, [onRequest](http://www.rtcmulticonnection.org/docs/onRequest/) is fired only for [session-initiator](http://www.rtcmulticonnection.org/docs/session-initiator/). +* "[shareMediaFile](http://www.rtcmulticonnection.org/docs/shareMediaFile/)" and "[onMediaFile](http://www.rtcmulticonnection.org/docs/onMediaFile/)" added. (i.e. pre-recorded media support added): + + ```javascript + // select WebM file to share as pre-recorded media! + document.querySelector('input[type=file]').onchange = function() { + connection.shareMediaFile( this.files[0] ); + }; + + // receive WebM files + connection.onMediaFile = function(e) { + // e.mediaElement (it is video-element) + // e.userid + // e.extra + + yourExistingVideoElement.src = e.mediaElement.src; + + // or + document.body.appendChild(e.mediaElement); + }; + + ``` +* Now, "[getDevices](http://www.rtcmulticonnection.org/docs/getDevices/)" method skips duplicate devices and returns array of unique audio/video devices. +* Now, "[onFileStart](http://www.rtcmulticonnection.org/docs/onFileStart/)", "[onFileProgress](http://www.rtcmulticonnection.org/docs/onFileProgress/)" and "[onFileEnd](http://www.rtcmulticonnection.org/docs/onFileEnd/)" has remote user's ID, "sending" object and "[extra](http://www.rtcmulticonnection.org/docs/extra/)" object: + + ```javascript + // file sending or receiving instance is started. + connection.onFileStart = function(file) { + // file.userid ---- userid of the file sender + // file.extra ---- extra data from file sender + + // file.sending ---- true or false + }; + + // file sending or receiving instance is ended. + connection.onFileEnd = function(file) { + // file.userid ---- userid of the file sender + // file.extra ---- extra data from file sender + + // file.sending ---- true or false + }; + + // file sending or receiving instance is working. + connection.onFileProgress = function(chunk) { + // chunk.userid ---- userid of the file sender + // chunk.extra ---- extra data from file sender + + // chunk.sending ---- true or false + }; + + ``` +* "[onstream](http://www.rtcmulticonnection.org/docs/onstream/)" has two new objects: + + ```javascript + connection.onstream = function(e) { + // e.isVideo ---- if it is a Video stream + // e.isAudio ---- if it is an Audio stream + }; + + ``` +* File [chunk-size](http://www.rtcmulticonnection.org/docs/chunkSize/) and [chunk-interval](http://www.rtcmulticonnection.org/docs/chunkInterval/) are fixed for Firefox. +* Now, when renegotiating media streams, v1.7 checks to verify if remote stream is delivered to end-user; if delivery fails, v1.7 auto tries renegotiation again. +* Now, "isAcceptNewSession" is shifted to root-level. Using this feature, you can allow single user to join multiple rooms from the same [channel](http://www.rtcmulticonnection.org/docs/channel-id/): + + ```javascript + connection.onstream = function(event) { + if(event.type == 'remote') { + // set "isAcceptNewSession=true" so this user can get and join new session i.e. room + // it means that "onNewSession" will be fired for this user as soon as other room is available! + connection.isAcceptNewSession = true; + } + }; + + ``` +* Now, "[session](http://www.rtcmulticonnection.org/docs/session/)" object is always passed over "[onstreamended](http://www.rtcmulticonnection.org/docs/onstreamended/)" event; so, you can check which stream is stopped: screen or audio/video: + + ```javascript + connection.onstreamended = function(event) { + if(event.session.screen) { } // if screen stream is stopped. + if(event.session.audio && !event.session.video) { } // if audio stream is stopped. + if(event.session.audio && event.session.video) { } // if audio+video stream is stopped. + + // event.type == 'local' || 'remote' + }; + + ``` +* v1.6 and earlier releases has a bug for renegotiated streams. When you try to stop renegotiated stream; it is removed only from 1st participant; not from all participants. This issue has been fixed in v1.7. +* "**forceToStopRemoteStream**" added. + + ```javascript + var forceToStopRemoteStream = true; + connection.streams['remote-stream-id'].stop( forceToStopRemoteStream ); + + ``` +This feature was disabled since v1.4 however now enabled by passing a single boolean over "**stop**" method. +* Default session is always kept there; it was a bug in v1.6 and earlier releases. Each new renegotiated session was overriding old sessions. +* Renegotiated sessions are always stored; and always renegotiated to newcomers. +* Renegotiation of external streams along with external session added. +* Remote stream stop & removal issues fixed. Now, "**onstreamended**" is always fired for all users according to the stream stopped. +* "**[setDefaultEventsForMediaElement](http://www.rtcmulticonnection.org/docs/setDefaultEventsForMediaElement/)**" added. i.e. mute/unmute are implemented by default! When you'll click mute button from native video control; v1.7 will auto invoke "mute" among all relevant peers. + + + +### 1.6 (2014-01-29) + +``` + +``` + +#### Bug Fixes +* [Bug #152](https://github.com/muaz-khan/WebRTC-Experiment/issues/152) fixed. +#### Features +* Now v1.6 is capable to auto-redial if connection is dropped by any mean. (This feature is disabled because it was affecting renegotiation process). +* Now you can join as audio-only, video-only or screen-only stream both on chrome and firefox: + + ```javascript + // original session is "audio-only" stream + connection.session = { audio: true }; + connection.onNewSession = function(session) { + // join with both audio and video! + session.join({audio: true, video: true}); + }; + + ``` + + ```javascript + // original session is "audio+video" stream + connection.session = { audio: true, video: true }; + connection.onNewSession = function(session) { + // join with audio-only stream! + session.join({audio: true}); + }; + + ``` +* "[fakeDataChannels](http://www.rtcmulticonnection.org/docs/fakeDataChannels/)" added. Using "[fakeDataChannels](http://www.rtcmulticonnection.org/docs/fakeDataChannels/)" object you can setup fake data connection while you're sharing audio/video/screen. You can do text-chat; share files; etc. without using WebRTC data channels. + + ```javascript + // audio+video and fake data connection + connection.fakeDataChannels = true; + connection.session = { audio: true, video: true }; + + // only fake data connection; no audio; no video; no WebRTC data channel! + connection.fakeDataChannels = true; + connection.session = { }; + + ``` +* "[UA](http://www.rtcmulticonnection.org/docs/UA/)" object added. It returns whether browser is chrome; firefox or mobile device: + + ```javascript + var isFirefox = connection.UA.Firefox; + var isChrome = connection.UA.Chrome; + var isMobileDevice = connection.UA.Mobile; + + ``` +* Now you can [renegotiate](http://www.rtcmulticonnection.org/docs/renegotiation/) data-connection in your existing audio/video/screen session! + + ```javascript + connection.addStream({ + data: true + }); + + ``` +* [Pull #152](https://github.com/muaz-khan/WebRTC-Experiment/issues/152) merged for "[token](http://www.rtcmulticonnection.org/docs/token/)" method: + + ```javascript + var randomString = connection.token(); + connection.userid = connection.token(); + + ``` +* "[autoTranslateText](http://www.rtcmulticonnection.org/docs/autoTranslateText/)" method added: + + ```javascript + // all incoming text messages will be converted in this language + // by default, it is "en-US" + connection.language = 'ja'; // prefer Japanese + + // it is "false" by default. + connection.autoTranslateText = true; + + // you can use "e.data" and "e.original" + connection.onmessage = function(e) { + // e.data ---- translated text + // e.original ---- original text + }; + + connection.Translator.TranslateText(textToConvert, function(convertedText) { + console.log(convertedText); + }); + + ``` +* "session.join" method added. Remember, it is "session.join"; it isn't "connection.join"! + + ```javascript + connection.onNewSession = function(session) { + session.join(); // join session as it is! + session.join({audio: true}); // join session while allowing only audio + session.join({video: true}); // join session while allowing only video + session.join({screen: true}); // join session while allowing only screen + session.join({audio: true, video: true}); // join session while allowing both audio and video + }; + + ``` +* Now, [MRecordRTC](https://github.com/muaz-khan/WebRTC-Experiment/tree/master/RecordRTC/MRecordRTC) is used for [audio/video recording](http://www.rtcmulticonnection.org/docs/startRecording/)! +* "[saveToDisk](http://www.rtcmulticonnection.org/docs/saveToDisk/)" method added. You can invoke save-as dialog like this: + + ```javascript + // "fileName" is optional + connection.saveToDisk(fileURL, fileName); + + // you can save recorded blob to disk like this: + connection.streams['stream-id'].stopRecording(function (audioBlob, videoBlob) { + connection.saveToDisk(audioBlob); + connection.saveToDisk(videoBlob); + }, {audio:true, video:true} ); + + ``` +* If you're recording both audio and video; then "[stopRecording](http://www.rtcmulticonnection.org/docs/stopRecording/)" will return both blobs in the single callback; where 1st parameter value will be audio-blob; and 2nd parameter value will be video-blob: + + ```javascript + // stop both audio/video streams + connection.streams['stream-id'].stopRecording(function (audioBlob, videoBlob) { + // POST both audio/video "Blobs" to PHP/other server using single FormData/XHR2 + }, {audio:true, video:true} ); + + ``` +* "[onhold](http://www.rtcmulticonnection.org/docs/onhold/)" and "[onunhold](http://www.rtcmulticonnection.org/docs/onunhold/)" events added: + + ```javascript + // to hold call; same like skype! + connection.peers['user-id'].hold(); + connection.onhold = function(e) { + // e.mediaElement || e.stream || e.userid || e.streamid + }; + + // to unhold call; same like skype! + connection.peers['user-id'].unhold(); + connection.onunhold = function(e) { + // e.mediaElement || e.stream || e.userid || e.streamid + }; + + ``` +* Many method has been added in "[peers](http://www.rtcmulticonnection.org/docs/peers/)" object. + + ```javascript + // renegotiate while adding external media streams + connection.peers['user-id'].renegotiate(Custom_MediaStream); + + // renegotiate while no media stream is added + connection.peers['user-id'].renegotiate(); + + // change bandwidth at runtime + connection.peers['user-id'].changeBandwidth({ + audio: 20, + video: 30, + data: 50 + }); + + // send private message to target user; privacy is guaranteed! + connection.peers['user-id'].sendCustomMessage('private-message'); + connection.peers['user-id'].onCustomMessage = function(privateMessage) { + console.log('private custom message', privateMessage); + }; + + // to drop call; same like skype! + connection.peers['user-id'].drop(); + + // to hold call; same like skype! + connection.peers['user-id'].hold(); + + // to unhold call; same like skype! + connection.peers['user-id'].unhold(); + + ``` + * "changeBandwidth" method is useful to modify bandwidth usage at runtime! + * "drop" method is useful to drop call between two users same like skype! + * "sendCustomMessage" and "onCustomMessage" are useful to exchange private data between two users where your preferred signaling solution is used to send data i.e. it works all the time! Remember, there is a public version of "[sendCustomMessage](http://www.rtcmulticonnection.org/docs/sendCustomMessage/)" as well! + * "hold" and "unhold" works same like skype! +* "[renegotiate](http://www.rtcmulticonnection.org/docs/renegotiate/)" method added. + + ```javascript + // renegotiate with/without media streams! + connection.renegotiate(Custom_MediaStream); + + ``` + "[renegotiate](http://www.rtcmulticonnection.org/docs/renegotiate/)" method works same like "[addStream](http://www.rtcmulticonnection.org/docs/addStream/)" however it gives you a little bit more control over renegotiation process! + + If connection is suddently dropped; or connection has not been established for 5 seconds; then you can use "[renegotiate](http://www.rtcmulticonnection.org/docs/renegotiate/)" method to retry/redial. + + In multi-user connectivity scenarios; it is suggested to try "[peers](http://www.rtcmulticonnection.org/docs/peers/)" object to renegotiate/retry/redial connections. See next section for more details. +* "[refresh](http://www.rtcmulticonnection.org/docs/refresh/)" method added. + + ```javascript + connection.refresh(); + + ``` +Using "[refresh](http://www.rtcmulticonnection.org/docs/refresh/)" method; you can refresh/reload the RTCMultiConnection object! +* "[remove](http://www.rtcmulticonnection.org/docs/remove/)" method added. + + ```javascript + connection.remove('user-id'); + + ``` +Using "[remove](http://www.rtcmulticonnection.org/docs/remove/)" method; you can clear all sockets, peers and streams coming from that user; so that he can reconnect to the same [session](http://www.rtcmulticonnection.org/docs/sessionid/)! +* File sharing is improved! Chrome/Firefox interoperability support added. +* "[preferSCTP](http://www.rtcmulticonnection.org/docs/preferSCTP/)" is now enabled by default. +* "[chunkSize](http://www.rtcmulticonnection.org/docs/chunkSize/)" added. You can set 64k chunk-size for chrome-to-chrome SCTP-data streaming! + + ```javascript + // for chrome-to-chrome data streaming only! + connection.chunkSize = 64 * 1000; + + // default value is 15k because Firefox's receiving limit is 16k! + connection.chunkSize = 15 * 1000; + + ``` +* All possible "complex" [renegotiation](http://www.rtcmulticonnection.org/docs/renegotiation/) scenarios are supported! + * You can select any participant to broadcast his screen or media stream in oneway direction over all connected users! + * There can be 5 broadcasters; and many viewers/listeners! + * Each and every skype-like feature is possible using v1.6! You can add/remove streams many times using existing peer connections! + + + +### 1.5 (2013-12-31) + +``` + +``` + +#### Bug Fixes +* [mute](http://www.rtcmulticonnection.org/docs/mute/)/[unmute](http://www.rtcmulticonnection.org/docs/unmute/) and "[stop](http://www.rtcmulticonnection.org/docs/stop/)" fixed both for chrome and firefox. +* Echo issue; i.e. self-sound playback fixed. +#### Features +* You can get list of devices using "[getDevices](http://www.rtcmulticonnection.org/docs/getDevices/)" method and prefer any single or two audio/video devices using "[selectDevices](http://www.rtcmulticonnection.org/docs/selectDevices/)" method. + + ```javascript + // get list of devices + connection.getDevices(function(devices){ + for (var device in devices) { + device = devices[device]; + + // device.kind == 'audio' || 'video' + console.log(device.id, device.label); + } + }); + + // select any audio and/or video device + connection.selectDevices(firstDeviceID, secondDeviceID); + + ``` +* "[onspeaking](http://www.rtcmulticonnection.org/docs/onspeaking/)" and "[onsilence](http://www.rtcmulticonnection.org/docs/onsilence/)" added. + + ```javascript + connection.onspeaking = function (e) { + // e.streamid, e.userid, e.stream, etc. + e.mediaElement.style.border = '1px solid red'; + }; + + connection.onsilence = function (e) { + // e.streamid, e.userid, e.stream, etc. + e.mediaElement.style.border = ''; + }; + + ``` +* "[connection.streams.stop](http://www.rtcmulticonnection.org/docs/stop/)" added to stop all local/remote streams. + + ```javascript + // stop all local media streams + connection.streams.stop('local'); + + // stop all remote media streams + connection.streams.stop('remote'); + + // stop all media streams + connection.streams.stop(); + + ``` +* [onmute](http://www.rtcmulticonnection.org/docs/onmute/)/[onunmute](http://www.rtcmulticonnection.org/docs/onunmute/) auto displays "poster" if not overridden! (both on chrome and firefox) + + ```javascript + connection.onmute = function(e) { + e.mediaElement.setAttribute('poster', + 'www.webrtc-experiment.com/images/muted.png'); + }; + + connection.onunmute = function (e) { + e.mediaElement.removeAttribute('poster'); + }; + + ``` +* If screen-sharing is stopped using blue button, "onstreamended" will be auto fired for both users! +* "[onstream](http://www.rtcmulticonnection.org/docs/onstream/)" auto appends video to "[document.body](http://www.rtcmulticonnection.org/docs/body/)" element; if not overridden! +* Default file progress-bar implemented. +* You can manage videos and "progress-bar" container element by setting "[connection.body](http://www.rtcmulticonnection.org/docs/body/)". + + ```javascript +
+ connection.body = document.querySelector('.container'); + + ``` +* Throws a clear error if users try to interop RTP-datachannels with [SCTP](http://www.rtcmulticonnection.org/docs/preferSCTP/). +* [onmute](http://www.rtcmulticonnection.org/docs/onunmute/)/[onunmute](http://www.rtcmulticonnection.org/docs/onunmute/) are fixed for both local and remote media streams. Both streams will be [auto-synced](https://github.com/muaz-khan/WebRTC-Experiment/issues/135#issuecomment-31289682)! +* Now, only [session-initiator](http://www.rtcmulticonnection.org/docs/session-initiator/) can [eject](http://www.rtcmulticonnection.org/docs/eject/) a user. +* [takeSnapshot](http://www.rtcmulticonnection.org/docs/takeSnapshot/)/[snapshots](http://www.rtcmulticonnection.org/docs/snapshots/) added: + + ```javascript + // iterate over all snapshots/ + for(var snapshot in connection.snapshots) { + snapshot = connection.snapshots[snapshot]; + } + + // or, to access individual snapshot later + image.src = connection.snapshots['userid']; + + // to take a snapshot + connection.takeSnapshot('userid', function(snapshot) { + image.src = snapshot; + }); + + ```javascript +* [Session initiator](http://www.rtcmulticonnection.org/docs/session-initiator/) can now [stop](http://www.rtcmulticonnection.org/docs/stop/) remote media streams. It works same like [eject](http://www.rtcmulticonnection.org/docs/eject/) method: + + ```javascript + connection.streams['remote-stream-id'].stop(); + + ``` +* [reject](http://www.rtcmulticonnection.org/docs/reject/) method added. Now, you can reject any request in [onRequest](http://www.rtcmulticonnection.org/docs/onRequest/) event.: + + ```javascript + connection.onRequest = function (request) { + connection.reject(request); + }; + + // "onstats" event can be used to know whether request is rejected or accepted + connection.onstats = function (stats, callee) { + // callee rejected the request + if (stats == 'rejected') {} + + // callee accepted caller's request + if (stats == 'accepted') {} + + // callee.userid || callee.extra + }; + + ``` +* [getStats](http://www.rtcmulticonnection.org/docs/getStats/) and [stats](http://www.rtcmulticonnection.org/docs/stats/) added. + + ```javascript + connection.getStats(function(stat) { + // stat.numberOfConnectedUsers + }); + + for(var stat in connection.stats) { + console.log(stat, connection.stats[stat]); + } + + // you can directly access "numberOfConnectedUsers" later like this: + console.log(connection.stats.numberOfConnectedUsers); + + ```javascript +* [caniuse](http://www.rtcmulticonnection.org/docs/caniuse/) added. + + + ```javascript + console.log( connection.caniuse.RTCPeerConnection ); + console.log( connection.caniuse.getUserMedia ); + console.log( connection.caniuse.AudioContext ); + console.log( connection.caniuse.ScreenSharing ); + console.log( connection.caniuse.RtpDataChannels ); + console.log( connection.caniuse.SctpDataChannels ); + + connection.caniuse.checkIfScreenSharingFlagEnabled(function (isFlagEnabled, warning) { + if (isFlagEnabled) { + console.error('Multi-capturing of screen is not allowed. Capturing process is denied. Try chrome >= M31.'); + } + + if (warning) console.error(warning); + + else if (!isFlagEnabled) { + console.error('It seems that "Enable screen capture support in getUserMedia" flag is not enabled.'); + } + }); + + ``` +* "[drop](http://www.rtcmulticonnection.org/docs/drop/)" added. Using [drop](http://www.rtcmulticonnection.org/docs/drop/) method; you can drop the call, same like skype! This method will detach all "local" media streams from both sides. + + ```javascript + connection.drop(); + + ``` +* "[ondrop](http://www.rtcmulticonnection.org/docs/ondrop/)" event added. It is fired if other user drops the call. + + ```javascript + // "ondrop" is fired; if media-connection is droppped by other user + connection.ondrop = function() { }; + + ``` +* "[sendCustomMessage](http://www.rtcmulticonnection.org/docs/sendCustomMessage/)" added. Using [sendCustomMessage](http://www.rtcmulticonnection.org/docs/sendCustomMessage/) method; you can share public messages over the default socket. E.g. + * State of the [session-participant](http://www.rtcmulticonnection.org/docs/session-participant/) or [session-initiator](http://www.rtcmulticonnection.org/docs/session-initiator/); whether he is going to leave; or going to share a media stream. + * Ask a [session-participant](http://www.rtcmulticonnection.org/docs/session-participant/) to broadcast screen or video in one-way [direction](http://www.rtcmulticonnection.org/docs/direction/). + * Ask [session-participants](http://www.rtcmulticonnection.org/docs/session-participant/) to attach audio stream; even if it is oneway stream coming from [session-initiator](http://www.rtcmulticonnection.org/docs/session-initiator/). + * There are unlimited scenarios; that can be accomplished using [sendCustomMessage](http://www.rtcmulticonnection.org/docs/sendCustomMessage/) method; just imagine and go ahead and use it! + + ```javascript + connection.sendCustomMessage(any_kind_of_data |or| string_message); + + ``` +* "[onCustomMessage](http://www.rtcmulticonnection.org/docs/onCustomMessage/)" event added. Custom messages can be received using [onCustomMessage](http://www.rtcmulticonnection.org/docs/onCustomMessage/) event. + + ```javascript + connection.onCustomMessage = function(message) { }; + + ``` +* "[stopRecording](http://www.rtcmulticonnection.org/docs/stopRecording/)" now returns both audio/video blobs in the single "callback"! + + ```javascript + // stop single audio stream + connection.streams['stream-id'].stopRecording(function (blob) { + // POST "Blob" to PHP/other server using FormData/XHR2 + }, {audio:true}); + + // stop single video stream + connection.streams['stream-id'].stopRecording(function (blob) { + // POST "Blob" to PHP/other server using FormData/XHR2 + }, {video:true}); + + // stop both audio/video streams + connection.streams['stream-id'].stopRecording(function (audioBlob, videoBlob) { + // POST both audio/video "Blobs" to PHP/other server using single FormData/XHR2 + }, {audio:true, video:true} ); + + ``` + + +### 1.4 (2013-06-06) + +``` + +``` + +#### Features +* Multiple concurrent files transmission / [(a0f9b72)](https://github.com/muaz-khan/WebRTC-Experiment/commit/a0f9b72654b3ba7c5232968d9850e35fb770bbbb#RTCMultiConnection) +* Advance renegotiation +* Admin/Guest features; useful in realtime chatting rooms where direct invitation is mandatory / [(572ac33)](https://github.com/muaz-khan/WebRTC-Experiment/commit/572ac336357b8530d779529e109197ea7b8f6f8e#RTCMultiConnection) +* Multi-streams attachment i.e. audio+video+data+screen / [(075eaa9)](https://github.com/muaz-khan/WebRTC-Experiment/commit/075eaa978399a2309b664164e875187ec7b6444a#RTCMultiConnection) +* Mute/UnMute/Stop of individual, all at once; all remote or all local streams +* onstreamended added; a better method comparing "onleave" +* maxParticipantsAllowed added +* media/sdp constraints / [(8d76c0c)](https://github.com/muaz-khan/WebRTC-Experiment/commit/8d76c0cb5be4d8df17c6603220c091b8ea2ff0f6#RTCMultiConnection) +* Session re-initiation / [(a0f9b72)](https://github.com/muaz-khan/WebRTC-Experiment/commit/a0f9b72654b3ba7c5232968d9850e35fb770bbbb#RTCMultiConnection) +* removeStream added to allow removal of existing media streams +* disableDtlsSrtp added to fix renegotiation process which fails on chrome when DTLS/SRTP enabled +* autoSaveToDisk added to allow customization of file-sharing +* file-sharing extended and fixed; no crash for large files anymore! +* renegotiation for chrome M29 and upper +* sctp/reliable data channels support for chrome (unreliable is still default) +* enable/disable ice candidates (host/relfexive/relay) +* enable/disable bandwidth sdp parameters (by default, enabled) +* noise/echo stepped down; a simple/lazy workaround +* audio/video recording added / using [RecordRTC](https://github.com/muaz-khan/WebRTC-Experiment/tree/master/RecordRTC) +* directions simplified +* SCTP data channels are preferred / preferSCTP +* onmute/onunmute added. [Demo](https://www.webrtc-experiment.com/RTCMultiConnection-v1.4-Demos/mute-unmute.html) +* File queue support added. Previously shared files will be auto transmitted to each new peer. + + +### 1.3 (2013-05-19) + +``` + +``` + +#### Features +* Syntax changed; a few breaking changes comparing v1.2 / [(ac36855)](https://github.com/muaz-khan/WebRTC-Experiment/commit/ac368557ce857dad1fbcf70aa58813d50cec6047#RTCMultiConnection) +* Simple renegotiation +* Mute/UnMute of individual streams +* Auto-session establishment feature removed +* Application specific bandwidth (b=AS) / [(6df6a55)](https://github.com/muaz-khan/WebRTC-Experiment/commit/6df6a5507268c84b91fe8445f0b9ef1f5781b687#RTCMultiConnection) and [(b38a228)](https://github.com/muaz-khan/WebRTC-Experiment/commit/b38a22834593cfc02893d320500dfb609f519580#RTCMultiConnection) +* Direct Messages +* New TURN format added / [(c0688f9)](https://github.com/muaz-khan/WebRTC-Experiment/commit/c0688f9eabfee4113150f3d362f2b3a2aa5c2895#RTCMultiConnection) / [IETF Draft](http://tools.ietf.org/html/draft-uberti-rtcweb-turn-rest-00) +* Compatible to [socket.io over node.js](https://github.com/muaz-khan/WebRTC-Experiment/tree/master/socketio-over-nodejs) / [(b2e7789)](https://github.com/muaz-khan/WebRTC-Experiment/commit/b2e7789bcb79a4248090081750e26c984a76d0b0#RTCMultiConnection) + + +### 1.2 (2013-04-20) + +``` + +``` + +#### Features +* Multi-session establishment +* Auto-session establishment +* Manual-session establishment +* A little bit clear session/direction values e.g. + > connection.session='audio + video and data' +* Users ejection, rejection and presence detection / [(305dd27)](https://github.com/muaz-khan/WebRTC-Experiment/commit/305dd27af73c9219183f78120e8ebbb8443efb1e#RTCMultiConnection) +* Keep session active all the time; event if initiator leaves / [(bd8ae0f)](https://github.com/muaz-khan/WebRTC-Experiment/commit/bd8ae0f5529e7a3900ef5ccac61f1364390be6b3#RTCMultiConnection) +* Custom data i.e. extra data transmission +* Audio-only streaming fixed / [(a4a6c35)](https://github.com/muaz-khan/WebRTC-Experiment/commit/a4a6c3589e341617767213703683f1dba6c7548e#RTCMultiConnection) +* Custom Handlers for server i.e. transmitRoomOnce + + +### 1.1 (2013-03-25) + +``` + +``` + +#### Features +* Multiple sessions & directions / [(0174312)](https://github.com/muaz-khan/WebRTC-Experiment/commit/017431280099e892744a6300ea866e7324f5e4c2#RTCMultiConnection) +* File, data and text sharing (of any size & length) +* Chrome/Firefox interoperability +* Firefox's [new DataChannel](https://github.com/muaz-khan/WebRTC-Experiment/wiki/WebRTC-DataChannel-and-Firefox#points) syntax implemented / [(7bad719)](https://github.com/muaz-khan/WebRTC-Experiment/commit/7bad719345814c7f832fad59abf31642e096b276#RTCMultiConnection) diff --git a/RTCMultiConnection/CONTRIBUTING.md b/RTCMultiConnection/CONTRIBUTING.md new file mode 100644 index 00000000..34059334 --- /dev/null +++ b/RTCMultiConnection/CONTRIBUTING.md @@ -0,0 +1,33 @@ + / Wiki Pages / Documentation / FAQ / Demos / Changes Log + +[![npm](https://img.shields.io/npm/v/rtcmulticonnection.svg)](https://npmjs.org/package/rtcmulticonnection) [![downloads](https://img.shields.io/npm/dm/rtcmulticonnection.svg)](https://npmjs.org/package/rtcmulticonnection) [![Build Status: Linux](https://travis-ci.org/muaz-khan/RTCMultiConnection.png?branch=master)](https://travis-ci.org/muaz-khan/RTCMultiConnection) + +# Contributors + +1. [Muaz Khan](https://github.com/muaz-khan) +2. [Alexey Kucherenko](https://github.com/killmenot) +3. [Yuri](https://github.com/Yuripetusko) +4. [Dmitry](https://github.com/Reptoh) +5. +Your Name + +# Rules to Contribute + +``` +git clone --depth=50 --branch=master git://github.com/muaz-khan/RTCMultiConnection.git muaz-khan/RTCMultiConnection + +# install all dependencies +npm install + +# install grunt for code style verifications +npm install grunt-cli +npm install grunt + +# verify your changes +npm test # or "grunt" + +# Success? Make a pull request! +``` + +## License + +[RTCMultiConnection](https://github.com/muaz-khan/RTCMultiConnection) is released under [MIT licence](https://github.com/muaz-khan/RTCMultiConnection/blob/master/LICENSE.md) . Copyright (c) [Muaz Khan](http://www.MuazKhan.com/). diff --git a/RTCMultiConnection/Gruntfile.js b/RTCMultiConnection/Gruntfile.js new file mode 100644 index 00000000..0b3c5e04 --- /dev/null +++ b/RTCMultiConnection/Gruntfile.js @@ -0,0 +1,125 @@ +'use strict'; + +module.exports = function(grunt) { + require('load-grunt-tasks')(grunt, { + pattern: 'grunt-*', + config: 'package.json', + scope: 'devDependencies' + }); + + // configure project + grunt.initConfig({ + // make node configurations available + pkg: grunt.file.readJSON('package.json'), + concat: { + options: { + stripBanners: true, + separator: '' + }, + dist: { + src: [ + 'dev/head.js', + 'dev/RTCMultiConnection.js', + 'dev/RTCMultiSession.js', + 'dev/getUserMedia.js', + 'dev/PeerConnection.js', + 'dev/FileSaver.js', + 'dev/TextReceiver.js', + 'dev/TextSender.js', + 'dev/globals.js', + 'dev/DetectRTC.js', + 'dev/setDefaults.js', + 'dev/tail.js' + ], + dest: 'RTCMultiConnection.js', + }, + }, + htmlhint: { + html1: { + src: [ + 'demos/*.html' + ], + options: { + 'tag-pair': true + } + } + }, + jshint: { + options: { + ignores: [], + // use default .jshintrc files + jshintrc: true + }, + files: ['RTCMultiConnection.js'] + }, + uglify: { + options: { + mangle: false + }, + my_target: { + files: { + 'RTCMultiConnection.min.js': ['RTCMultiConnection.js'] + } + } + }, + jsbeautifier: { + files: ['RTCMultiConnection.js', 'demos/*.html', 'dev/*.js', 'Gruntfile.js'], + options: { + js: { + braceStyle: "collapse", + breakChainedMethods: false, + e4x: false, + evalCode: false, + indentChar: " ", + indentLevel: 0, + indentSize: 4, + indentWithTabs: false, + jslintHappy: false, + keepArrayIndentation: false, + keepFunctionIndentation: false, + maxPreserveNewlines: 10, + preserveNewlines: true, + spaceBeforeConditional: true, + spaceInParen: false, + unescapeStrings: false, + wrapLineLength: 0 + }, + html: { + braceStyle: "collapse", + indentChar: " ", + indentScripts: "keep", + indentSize: 4, + maxPreserveNewlines: 10, + preserveNewlines: true, + unformatted: ["a", "sub", "sup", "b", "i", "u"], + wrapLineLength: 0 + }, + css: { + indentChar: " ", + indentSize: 4 + } + } + }, + bump: { + options: { + files: ['package.json', 'bower.json'], + updateConfigs: [], + commit: true, + commitMessage: 'v%VERSION%', + commitFiles: ['package.json', 'bower.json'], + createTag: true, + tagName: '%VERSION%', + tagMessage: '%VERSION%', + push: false, + pushTo: 'upstream', + gitDescribeOptions: '--tags --always --abbrev=1 --dirty=-d' + } + } + }); + + // enable plugins + + // set default tasks to run when grunt is called without parameters + // http://gruntjs.com/api/grunt.task + grunt.registerTask('default', ['concat', 'jsbeautifier', 'htmlhint', 'jshint', 'uglify']); +}; diff --git a/RTCMultiConnection/LICENSE.md b/RTCMultiConnection/LICENSE.md new file mode 100644 index 00000000..8ecd04e1 --- /dev/null +++ b/RTCMultiConnection/LICENSE.md @@ -0,0 +1,23 @@ + +The MIT License +================ + +Copyright (c) 2014-2018 Muaz Khan \ + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/RTCMultiConnection/Library/RTCMultiConnection-v1.0.js b/RTCMultiConnection/Library/RTCMultiConnection-v1.0.js deleted file mode 100644 index 01238967..00000000 --- a/RTCMultiConnection/Library/RTCMultiConnection-v1.0.js +++ /dev/null @@ -1,1016 +0,0 @@ -/* MIT License: https://webrtc-experiment.appspot.com/licence/ - 2013, Muaz Khan--[github.com/muaz-khan] - - For documentation and examples: http://bit.ly/RTCMultiConnection */ -function RTCMultiConnection(configuration) { - configuration = configuration || {}; - var config = { - openSignalingChannel: configuration.openSignalingChannel, - onNewSession: function(session) { - config.session = session.session; - config.direction = session.direction; - - if (configuration.onNewSession) - configuration.onNewSession(session); - else { - if (configuration.joinedASession) return; - configuration.joinedASession = true; - - if (session.direction === Direction.OneWay || session.session === Session.Data) - joinSession(session); - else - captureUserMedia(function() { - joinSession(session); - }); - } - }, - onChannelOpened: function(channel) { - if (configuration.onopen) - configuration.onopen(channel); - }, - onChannelMessage: function(data) { - if (!data.size) data = JSON.parse(data); - - if (data.type === 'text') - textReceiver.receive(data, configuration.onmessage); - else if (data.size || data.type === 'file') - fileReceiver.receive(data, config); - else if (configuration.onmessage) - configuration.onmessage(data); - }, - direction: configuration.direction || Direction.ManyToMany, - session: configuration.session || Session.AudioVideo, - onChannelClosed: function(e) { - if (configuration.onclose) - configuration.onclose(e); - }, - onChannelError: function(e) { - if (configuration.onerror) - configuration.onerror(e); - }, - onFileReceived: function(fileName) { - if (configuration.onFileReceived) - configuration.onFileReceived(fileName); - }, - onFileProgress: function(packets) { - if (configuration.onFileProgress) - configuration.onFileProgress(packets); - }, - iceServers: configuration.iceServers, - attachStream: configuration.attachStream - }; - if (!configuration.openSignalingChannel) - throw 'openSignalingChannel is mandatory.'; - - if (configuration.onRemoteStream) - config.onRemoteStream = configuration.onRemoteStream; - - var rtcSession = new RTCMultiSession(config), - fileReceiver = new FileReceiver(), - textReceiver = new TextReceiver(); - - function joinSession(session) { - if (!session || !session.userid || !session.sessionid) - throw 'invalid data passed.'; - - rtcSession.joinSession(session); - } - - function captureUserMedia(callback) { - var constraints, session = config.session; - - if (session === Session.Data || config.attachStream) - return callback(); - - if (session.isAudio()) { - constraints = { - audio: true, - video: false - }; - } - if (session.isScreen()) { - video_constraints = { - mandatory: { - chromeMediaSource: 'screen' - }, - optional: [] - }; - constraints = { - audio: false, - video: video_constraints - }; - } - - if (session === Session.Video || session === Session.VideoData) { - var video_constraints = { - mandatory: {}, - optional: [] - }; - constraints = { - audio: false, - video: video_constraints - }; - } - var mediaElement = document.createElement(session.isAudio() ? 'audio' : 'video'); - var mediaConfig = { - video: mediaElement, - onsuccess: function(stream) { - config.attachStream = stream; - callback && callback(); - - if (configuration.onLocalStream) - configuration.onLocalStream({ - stream: stream, - mediaElement: mediaElement, - blobURL: mediaElement.mozSrcObject || mediaElement.src - }); - }, - onerror: function() { - if (session.isAudio()) - throw 'unable to get access to your microphone'; - else if (session.isScreen()) { - if (location.protocol === 'http:') { - throw 'Please test this WebRTC experiment on HTTPS.'; - } else { - throw 'Screen capturing is either denied or not supported.'; - } - } else { - throw 'Unable to get access to your webcam'; - } - } - }; - if (constraints) - mediaConfig.constraints = constraints; - getUserMedia(mediaConfig); - return true; - } - - return { - initSession: function(options) { - options = options || {}; - captureUserMedia(function() { - rtcSession.initSession({ - direction: configuration.direction, - session: configuration.session, - sessionid: options.sessionid, - userid: options.userid - }); - if (options.callback) options.callback(rtcSession.getSession()); - }); - }, - connectSession: function(session) { - if (configuration.joinedASession) - return; - configuration.joinedASession = true; - - if (session.direction === Direction.OneWay) - joinSession(session); - else - captureUserMedia(function() { - joinSession(session); - }); - }, - send: function(data) { - if (!data) throw 'No file, data or text message to share.'; - if (data.size) - FileSender.send({ - file: data, - channel: rtcSession, - onFileSent: configuration.onFileSent, - onFileProgress: configuration.onFileProgress - }); - else - TextSender.send({ - text: data, - channel: rtcSession - }); - } - }; -} - -var Session = { - AudioVideoData: 'AudioVideoData', - AudioVideo: 'AudioVideo', - AudioData: 'AudioData', - VideoData: 'VideoData', - Audio: 'Audio', - Video: 'Video', - Data: 'Data', - ScreenData: 'ScreenData', - Screen: 'Screen' -}; - -var Direction = { - OneWay: 'OneWay', - OneToOne: 'OneToOne', - OneToMany: 'OneToMany', - ManyToMany: 'ManyToMany' -}; - -String.prototype.isAudio = function() { - var session = this + ''; - return session === Session.Audio || session === Session.AudioData; -}; - -String.prototype.isScreen = function() { - var session = this + ''; - return session === Session.Screen || session === Session.ScreenData; -}; - -/* RTCPeerConnection object is a wrapper for RTCWeb APIs */ -window.moz = !!navigator.mozGetUserMedia; -var RTCPeerConnection = function(options) { - var w = window, - PeerConnection = w.mozRTCPeerConnection || w.webkitRTCPeerConnection, - SessionDescription = w.mozRTCSessionDescription || w.RTCSessionDescription, - IceCandidate = w.mozRTCIceCandidate || w.RTCIceCandidate; - - var STUN = { - url: !moz ? 'stun:stun.l.google.com:19302' : 'stun:23.21.150.121' - }; - var TURN = { - url: 'turn:webrtc%40live.com@numb.viagenie.ca', - credential: 'muazkh' - }; - var iceServers = { - iceServers: options.iceServers || [STUN] - }; - if (!moz && !options.iceServers) iceServers.iceServers[1] = TURN; - - var optional = { - optional: [] - }; - - if (!moz) { - optional.optional = [{ - DtlsSrtpKeyAgreement: true - } - ]; - if (options.onChannelMessage) - optional.optional = [{ - RtpDataChannels: true - } - ]; - } - - var peerConnection = new PeerConnection(iceServers, optional); - - var dataPorts = getPorts(); - openOffererChannel(); - - peerConnection.onicecandidate = onicecandidate; - if (options.attachStream) - peerConnection.addStream(options.attachStream); - peerConnection.onaddstream = onaddstream; - - function onicecandidate(event) { - if (!event.candidate || !peerConnection) - return; - if (options.onICE) - options.onICE(event.candidate); - } - - var remoteStreamEventFired = false; - - function onaddstream(event) { - if (remoteStreamEventFired || !event || !options.onRemoteStream) - return; - remoteStreamEventFired = true; - options.onRemoteStream(event.stream); - } - - var constraints = options.constraints || { - optional: [], - mandatory: { - OfferToReceiveAudio: true, - OfferToReceiveVideo: true - } - }; - - var chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'.split(''), - extractedChars = ''; - - function getChars() { - extractedChars += chars[parseInt(Math.random() * 40)] || ''; - if (extractedChars.length < 40) - getChars(); - - return extractedChars; - } - - function getInteropSDP(sdp) { - var inline = getChars() + '\r\n' + (extractedChars = ''); - sdp = sdp.indexOf('a=crypto') == -1 ? sdp.replace(/c=IN/g, - 'a=crypto:1 AES_CM_128_HMAC_SHA1_80 inline:' + inline + - 'c=IN') : sdp; - - return sdp; - } - - if (moz && !options.onChannelMessage) - constraints.mandatory.MozDontOfferDataChannel = true; - - function createOffer() { - if (!options.onOfferSDP) - return; - - peerConnection.createOffer(function(sessionDescription) { - sessionDescription.sdp = getInteropSDP(sessionDescription.sdp); - peerConnection.setLocalDescription(sessionDescription); - options.onOfferSDP(sessionDescription); - }, null, constraints); - } - - function createAnswer() { - if (!options.onAnswerSDP) - return; - - options.offerSDP = new SessionDescription(options.offerSDP); - peerConnection.setRemoteDescription(options.offerSDP); - - peerConnection.createAnswer(function(sessionDescription) { - sessionDescription.sdp = getInteropSDP(sessionDescription.sdp); - peerConnection.setLocalDescription(sessionDescription); - options.onAnswerSDP(sessionDescription); - - /* signaling method MUST be faster; otherwise increase "300" */ - moz && options.onChannelMessage && setTimeout(function() { - peerConnection.connectDataConnection(dataPorts[0], dataPorts[1]); - }, 300); - }, null, constraints); - } - - if ((options.onChannelMessage && !moz) || !options.onChannelMessage) { - createOffer(); - createAnswer(); - } - - var channel; - - function openOffererChannel() { - if (!options.onChannelMessage || (moz && !options.onOfferSDP)) - return; - - if (!moz) - _openOffererChannel(); - else - peerConnection.onconnection = _openOffererChannel; - - if (moz && !options.attachStream) { - navigator.mozGetUserMedia({ - audio: true, - fake: true - }, function(stream) { - peerConnection.addStream(stream); - createOffer(); - }, useless); - } - } - - function _openOffererChannel() { - channel = peerConnection.createDataChannel( - options.channel || 'RTCDataChannel', - moz ? {} : { - reliable: false - }); - setChannelEvents(); - } - - function setChannelEvents() { - channel.onmessage = function(event) { - if (options.onChannelMessage) - options.onChannelMessage(event); - }; - - channel.onopen = function() { - if (options.onChannelOpened) - options.onChannelOpened(channel); - }; - channel.onclose = function(event) { - if (options.onChannelClosed) - options.onChannelClosed(event); - console.warn('WebRTC Data Channel closed.', event); - }; - channel.onerror = function(event) { - if (options.onChannelError) - options.onChannelError(event); - console.error('WebRTC Data Channel error:', event); - }; - } - - if (options.onAnswerSDP && moz) - openAnswererChannel(); - - function openAnswererChannel() { - peerConnection.ondatachannel = function(_channel) { - channel = _channel; - channel.binaryType = 'blob'; - setChannelEvents(); - }; - - if (moz && !options.attachStream) { - navigator.mozGetUserMedia({ - audio: true, - fake: true - }, function(stream) { - peerConnection.addStream(stream); - createAnswer(); - }, useless); - } - } - - function useless() { - } - - function getPorts(ports) { - if (!moz || !options.onChannelMessage) - return false; - ports = ports || options.dataPorts || [5000, 5001]; - return ports; - } - - return { - addAnswerSDP: function(sdp, _dataPorts) { - sdp = new SessionDescription(sdp); - peerConnection.setRemoteDescription(sdp, function() { - if (moz && options.onChannelMessage) { - var ports = getPorts(_dataPorts); - peerConnection.connectDataConnection(ports[1], ports[0]); - } - }); - }, - addICE: function(candidate) { - peerConnection.addIceCandidate(new IceCandidate({ - sdpMLineIndex: candidate.sdpMLineIndex, - candidate: candidate.candidate - })); - }, - - peer: peerConnection, - channel: channel, - sendData: function(message) { - channel && channel.send(message); - } - }; -}; - -var video_constraints = { - mandatory: {}, - optional: [] -}; - -function getUserMedia(options) { - var n = navigator, - media; - n.getMedia = n.webkitGetUserMedia || n.mozGetUserMedia; - n.getMedia(options.constraints || { - audio: true, - video: video_constraints - }, streaming, options.onerror || function(e) { - console.error(e); - }); - - function streaming(stream) { - var video = options.video; - if (video) { - video[moz ? 'mozSrcObject' : 'src'] = moz ? stream : window.webkitURL.createObjectURL(stream); - video.play(); - } - options.onsuccess && options.onsuccess(stream); - media = stream; - } - - return media; -} - -function RTCMultiSession(config) { - var session = config.session || '', - direction = config.direction || '', - self = { - id: uniqueToken() - }, - channels = '--', - isbroadcaster, - isAcceptNewSession = true, - defaultSocket = {}, RTCDataChannels = []; - - function openDefaultSocket() { - defaultSocket = config.openSignalingChannel({ - onmessage: onDefaultSocketResponse - }); - } - - function onDefaultSocketResponse(response) { - if (response.userid == self.id) - return; - - if (isAcceptNewSession && response.sessionid && response.userid) - if (config.onNewSession) - config.onNewSession(response); - - if (response.newParticipant) - onNewParticipant(response.newParticipant); - - if (response.userid && response.targetUser == self.id && response.participant && channels.indexOf(response.userid) == -1) { - channels += response.userid + '--'; - openSubSocket({ - isofferer: true, - channel: response.channel || response.userid, - closeSocket: true - }); - } - } - - function getPort() { - return Math.random() * 1000 << 1000; - } - - function openSubSocket(_config) { - if (!_config.channel) - return; - var socketConfig = { - channel: _config.channel, - onmessage: socketResponse, - onopen: function() { - if (isofferer && !peer) - initPeer(); - } - }; - - var socket = config.openSignalingChannel(socketConfig), - isofferer = _config.isofferer, - isGotRemoteStream, - inner = {}, - dataPorts = [getPort(), getPort()], - mediaElement = document.createElement(session.isAudio() ? 'audio' : 'video'), - peer; - - var peerConfig = { - onICE: function(candidate) { - socket.send({ - id: self.id, - candidate: { - sdpMLineIndex: candidate.sdpMLineIndex, - candidate: JSON.stringify(candidate.candidate) - } - }); - }, - onChannelOpened: onChannelOpened, - onChannelMessage: function(event) { - if (config.onChannelMessage) - config.onChannelMessage(event.data); - }, - attachStream: config.attachStream, - onRemoteStream: function(stream) { - mediaElement[moz ? 'mozSrcObject' : 'src'] = moz ? stream : window.webkitURL.createObjectURL(stream); - mediaElement.play(); - - _config.stream = stream; - if (session.isAudio()) { - mediaElement.addEventListener('play', function() { - this.muted = false; - this.volume = 1; - afterRemoteStreamStartedFlowing(); - }, false); - } else - onRemoteStreamStartsFlowing(); - }, - iceServers: config.iceServers, - session: session - }; - - function initPeer(offerSDP) { - if (direction === Direction.OneToOne && window.isFirstConnectionOpened) - return; - if (!offerSDP) { - peerConfig.onOfferSDP = sendsdp; - } else { - peerConfig.offerSDP = offerSDP; - peerConfig.onAnswerSDP = sendsdp; - peerConfig.dataPorts = dataPorts; - } - - if (session.indexOf('Data') === -1) - peerConfig.onChannelMessage = null; - - peer = RTCPeerConnection(peerConfig); - } - - function onRemoteStreamStartsFlowing() { - if (!(mediaElement.readyState <= HTMLMediaElement.HAVE_CURRENT_DATA || mediaElement.paused || mediaElement.currentTime <= 0)) { - afterRemoteStreamStartedFlowing(); - } else - setTimeout(onRemoteStreamStartsFlowing, 300); - } - - function afterRemoteStreamStartedFlowing() { - if (config.onRemoteStream) - config.onRemoteStream({ - mediaElement: mediaElement, - stream: _config.stream, - session: session, - direction: direction, - blobURL: mediaElement.mozSrcObject || mediaElement.src - }); - - onSessionOpened(); - } - - function onChannelOpened(channel) { - RTCDataChannels[RTCDataChannels.length] = channel; - if (config.onChannelOpened) config.onChannelOpened(channel); - onSessionOpened(); - } - - function onSessionOpened() { - if (isGotRemoteStream) return; - window.isFirstConnectionOpened = isGotRemoteStream = true; - - if (direction === Direction.ManyToMany && isbroadcaster && channels.split('--').length > 3) { - defaultSocket.send({ - newParticipant: socket.channel, - userid: self.id - }); - } - - // if (_config.closeSocket && socket) socket = null; - } - - function sendsdp(sdp) { - sdp = JSON.stringify(sdp); - var part = parseInt(sdp.length / 3); - - var firstPart = sdp.slice(0, part), - secondPart = sdp.slice(part, sdp.length - 1), - thirdPart = ''; - - if (sdp.length > part + part) { - secondPart = sdp.slice(part, part + part); - thirdPart = sdp.slice(part + part, sdp.length); - } - - socket.send({ - id: self.id, - firstPart: firstPart, - - /* sending RTCDataChannel ports alongwith sdp */ - dataPorts: dataPorts - }); - - socket.send({ - id: self.id, - secondPart: secondPart - }); - - socket.send({ - id: self.id, - thirdPart: thirdPart - }); - } - - function socketResponse(response) { - if (response.id == self.id) - return; - - if (response.dataPorts) - inner.dataPorts = response.dataPorts; - - if (response.firstPart || response.secondPart || response.thirdPart) { - if (response.firstPart) { - inner.firstPart = response.firstPart; - if (inner.secondPart && inner.thirdPart) - selfInvoker(); - } - if (response.secondPart) { - inner.secondPart = response.secondPart; - if (inner.firstPart && inner.thirdPart) - selfInvoker(); - } - - if (response.thirdPart) { - inner.thirdPart = response.thirdPart; - if (inner.firstPart && inner.secondPart) - selfInvoker(); - } - } - - if (response.candidate && !isGotRemoteStream) { - peer && peer.addICE({ - sdpMLineIndex: response.candidate.sdpMLineIndex, - candidate: JSON.parse(response.candidate.candidate) - }); - } - } - - var invokedOnce = false; - - function selfInvoker() { - if (invokedOnce) - return; - - invokedOnce = true; - inner.sdp = JSON.parse(inner.firstPart + inner.secondPart + inner.thirdPart); - if (isofferer) - peer.addAnswerSDP(inner.sdp, inner.dataPorts); - else - initPeer(inner.sdp); - } - } - - function startBroadcasting() { - defaultSocket.send({ - sessionid: self.sessionid, - userid: self.id, - session: config.session, - direction: config.direction - }); - - if (config.direction === Direction.OneToOne) { - if (!window.isFirstConnectionOpened) - setTimeout(startBroadcasting, 3000); - } else - setTimeout(startBroadcasting, 3000); - } - - function onNewParticipant(channel) { - if (!channel || channels.indexOf(channel) != -1 || channel == self.id) - return; - channels += channel + '--'; - - var new_channel = uniqueToken(); - openSubSocket({ - channel: new_channel, - closeSocket: true - }); - - defaultSocket.send({ - participant: true, - userid: self.id, - targetUser: channel, - channel: new_channel - }); - } - - function uniqueToken() { - var s4 = function() { - return Math.floor(Math.random() * 0x10000).toString(16); - }; - return s4() + s4() + "-" + s4() + "-" + s4() + "-" + s4() + "-" + s4() + s4() + s4(); - } - - openDefaultSocket(); - return { - initSession: function(_config) { - self.sessionid = _config.sessionid || uniqueToken(); - - if (_config.session) - session = _config.session; - - if (_config.direction) - direction = _config.direction; - - if (_config.userid) - self.id = _config.userid; - - isbroadcaster = true; - isAcceptNewSession = false; - startBroadcasting(); - }, - joinSession: function(_config) { - self.sessionid = _config.sessionid; - - if (_config.session) - session = _config.session; - if (_config.direction) - direction = _config.direction; - - isAcceptNewSession = false; - - openSubSocket({ - channel: self.id - }); - - defaultSocket.send({ - participant: true, - userid: self.id, - targetUser: _config.userid - }); - }, - send: function(message) { - var _channels = RTCDataChannels, - data, length = _channels.length; - if (!length) return; - - if (moz && message.file) data = message.file; - else data = JSON.stringify(message); - - for (var i = 0; i < length; i++) - _channels[i].send(data); - }, - getSession: function() { - return { - userid: self.id, - sessionid: self.sessionid, - session: session, - direction: direction - }; - } - }; -} - -var FileSender = { - send: function(config) { - var channel = config.channel, - file = config.file; - - /* if firefox nightly: share file blob directly */ - if (moz) { - /* used on the receiver side to set received file name */ - channel.send({ - fileName: file.name, - type: 'file' - }); - - /* sending the entire file at once */ - channel.send({ - file: file - }); - - if (config.onFileSent) - config.onFileSent(file); - } - - /* if chrome */ - if (!moz) { - var reader = new window.FileReader(); - reader.readAsDataURL(file); - reader.onload = onReadAsDataURL; - } - - var packetSize = 1000 /* chars */, - textToTransfer = '', - numberOfPackets = 0, - packets = 0; - - function onReadAsDataURL(event, text) { - var data = { - type: 'file' - }; - - if (event) { - text = event.target.result; - numberOfPackets = packets = data.packets = parseInt(text.length / packetSize); - } - - if (config.onFileProgress) - config.onFileProgress({ - remaining: packets--, - length: numberOfPackets, - sent: numberOfPackets - packets - }); - - if (text.length > packetSize) - data.message = text.slice(0, packetSize); - else { - data.message = text; - data.last = true; - data.name = file.name; - - if (config.onFileSent) - config.onFileSent(file); - } - - channel.send(data); - - textToTransfer = text.slice(data.message.length); - - if (textToTransfer.length) - setTimeout(function() { - onReadAsDataURL(null, textToTransfer); - }, 500); - } - } -}; - -function FileReceiver() { - var content = [], - fileName = '', - packets = 0, - numberOfPackets = 0; - - function receive(data, config) { - /* if firefox nightly & file blob shared */ - if (moz) { - if (data.fileName) - fileName = data.fileName; - if (data.size) { - var reader = new window.FileReader(); - reader.readAsDataURL(data); - reader.onload = function(event) { - FileSaver.SaveToDisk(event.target.result, fileName); - if (config.onFileReceived) - config.onFileReceived(fileName); - }; - } - } - - if (!moz) { - if (data.packets) - numberOfPackets = packets = parseInt(data.packets); - - if (config.onFileProgress) - config.onFileProgress({ - remaining: packets--, - length: numberOfPackets, - received: numberOfPackets - packets - }); - - content.push(data.message); - - if (data.last) { - FileSaver.SaveToDisk(content.join(''), data.name); - if (config.onFileReceived) - config.onFileReceived(data.name); - content = []; - } - } - } - - return { - receive: receive - }; -} - -var TextSender = { - send: function(config) { - var channel = config.channel, - initialText = config.text, - packetSize = 1000 /* chars */, - textToTransfer = ''; - - if (typeof initialText !== 'string') initialText = JSON.stringify(initialText); - - if (moz || initialText.length <= packetSize) channel.send(config.text); - else sendText(initialText); - - function sendText(textMessage, text) { - var data = { - type: 'text' - }; - - 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; - } - - channel.send(data); - - textToTransfer = text.slice(data.message.length); - - if (textToTransfer.length) - setTimeout(function() { - sendText(null, textToTransfer); - }, 500); - } - } -}; - -function TextReceiver() { - var content = []; - - function receive(data, onmessage) { - content.push(data.message); - if (data.last) { - if (onmessage) onmessage(content.join('')); - content = []; - } - } - - return { - receive: receive - }; -} - - -var FileSaver = { - SaveToDisk: function(fileUrl, fileName) { - var save = document.createElement('a'); - save.href = fileUrl; - save.target = '_blank'; - save.download = fileName || fileUrl; - - var evt = document.createEvent('MouseEvents'); - evt.initMouseEvent('click', true, true, window, 1, 0, 0, 0, 0, false, false, false, false, 0, null); - - save.dispatchEvent(evt); - - (window.URL || window.webkitURL).revokeObjectURL(save.href); - } -}; \ No newline at end of file diff --git a/RTCMultiConnection/Library/RTCMultiConnection-v1.1.js b/RTCMultiConnection/Library/RTCMultiConnection-v1.1.js deleted file mode 100644 index 9db8ba23..00000000 --- a/RTCMultiConnection/Library/RTCMultiConnection-v1.1.js +++ /dev/null @@ -1,1048 +0,0 @@ -// Muaz Khan - www.MuazKhan.com -// MIT License - www.WebRTC-Experiment.com/licence -// Documentation - www.RTCMultiConnection.org/docs/ - -// FAQ - www.RTCMultiConnection.org/FAQ/ -// Development News - trello.com/b/8bhi1G6n/RTCMultiConnection - -// v1.1 changes log - www.RTCMultiConnection.org/changes-log/#v1.1 -// _______________________ -// RTCMultiConnection-v1.1 - -function RTCMultiConnection(channel) { - this.channel = channel; - var self = this, - rtcSession, fileReceiver, textReceiver; - - self.onmessage = function(message) { - console.debug('DataChannel message:', message); - }; - - self.onopen = function(_channel) { - _channel.send('First text message!'); - }; - - self.onFileReceived = function(fileName) { - console.debug('File <', fileName, '> received successfully.'); - }; - - self.onFileSent = function(file) { - console.debug('File <', file.name, '> sent successfully.'); - }; - self.onFileProgress = function(packets) { - console.debug('<', packets.remaining, '> items remaining.'); - }; - self.session = Session.AudioVideo; - self.direction = Direction.ManyToMany; - - function prepareInit(callback) { - if (!self.openSignalingChannel) { - self.openSignalingChannel = function(config) { - config = config || { }; - channel = config.channel || self.channel || 'default-channel'; - var socket = new window.Firebase('https://chat.firebaseIO.com/' + channel); - socket.channel = channel; - socket.on('child_added', function(data) { - config.onmessage && config.onmessage(data.val()); - }); - socket.send = function(data) { - this.push(data); - }; - config.onopen && setTimeout(config.onopen, 1); - - if (!self.socket) self.socket = socket; - if (channel != self.channel || (self.isInitiator && channel == self.channel)) - socket.onDisconnect().remove(); - - return socket; - }; - - if (!window.Firebase) { - var script = document.createElement('script'); - script.src = 'https://cdn.firebase.com/v0/firebase.js'; - script.onload = callback; - document.documentElement.appendChild(script); - } else callback(); - } else callback(); - } - - function init() { - self.direction = self.direction.lowercase(); - self.session = self.session.lowercase(); - - self.config = { - openSignalingChannel: function(config) { - return self.openSignalingChannel(config); - }, - onNewSession: function(session) { - if (self.channel !== session.sessionid) return; - - if (self.joinedARoom) return; - self.joinedARoom = true; - - self.session = session.session; - self.direction = session.direction; - - if (session.direction === Direction.OneWay || session.session === Session.Data) - joinSession(session); - else - captureUserMedia(function() { - joinSession(session); - }); - }, - onChannelOpened: function() { - self.onopen(self); - }, - onChannelMessage: function(data) { - if (!data.size) data = JSON.parse(data); - - if (data.type === 'text') - textReceiver.receive(data, self.onmessage); - else if (data.size || data.type === 'file') - fileReceiver.receive(data, self.config); - else self.onmessage(data); - }, - onChannelClosed: self.onclose, - onChannelError: self.onerror, - onFileReceived: self.onFileReceived, - onFileProgress: self.onFileProgress, - iceServers: self.iceServers, - - attachStream: self.attachStream, - onRemoteStream: self.onstream, - - direction: self.direction, - session: self.session, - channel: self.channel - }; - rtcSession = new RTCMultiSession(self.config); - fileReceiver = new FileReceiver(); - textReceiver = new TextReceiver(); - } - - function joinSession(session) { - if (!session || !session.userid || !session.sessionid) - throw 'invalid data passed.'; - - rtcSession.joinSession(session); - } - - self.open = function(_channel) { - if (self.socket) self.socket.onDisconnect().remove(); - else self.isInitiator = true; - - if (_channel) self.channel = _channel; - prepareInit(function() { - init(); - captureUserMedia(rtcSession.initSession); - }); - }; - self.connect = function(_channel) { - if (_channel) self.channel = _channel; - prepareInit(init); - }; - self.onstream = function(stream) { - console.debug('stream:', stream); - }; - self.send = function(data) { - if (!data) throw 'No file, data or text message to share.'; - if (data.size) - FileSender.send({ - file: data, - channel: rtcSession, - onFileSent: self.onFileSent, - onFileProgress: self.onFileProgress - }); - else - TextSender.send({ - text: data, - channel: rtcSession - }); - }; - - function captureUserMedia(callback) { - var constraints, session = self.session; - - if (session === Session.Data || self.dontAttachStream) - return callback(); - - if (self.attachStream) { - self.config.attachStream = self.attachStream; - return callback(); - } - - if (session.isAudio()) { - constraints = { - audio: true, - video: false - }; - } - if (session.isScreen()) { - video_constraints = { - mandatory: { - chromeMediaSource: 'screen' - }, - optional: [] - }; - constraints = { - audio: false, - video: video_constraints - }; - } - - if (session === Session.Video || session === Session.VideoData) { - var video_constraints = { - mandatory: { }, - optional: [] - }; - constraints = { - audio: false, - video: video_constraints - }; - } - var mediaElement = document.createElement(session.isAudio() ? 'audio' : 'video'); - var mediaConfig = { - video: mediaElement, - onsuccess: function(stream) { - self.config.attachStream = stream; - callback && callback(); - - // issue #37: Sometimes Firefox crashes while echoing and you have to hard reboot your PC ( Ubuntu 13.04, FF 21 ) - mediaElement.muted = true; - self.onstream({ - stream: stream, - mediaElement: mediaElement, - blobURL: mediaElement.mozSrcObject || mediaElement.src, - type: 'local' - }); - }, - onerror: function() { - if (session.isAudio()) - throw 'unable to get access to your microphone'; - else if (session.isScreen()) { - if (location.protocol === 'http:') { - throw 'Please test this WebRTC experiment on HTTPS.'; - } else { - throw 'Screen capturing is either denied or not supported. Are you enabled flag: "Enable screen capture support in getUserMedia"?'; - } - } else { - throw 'Unable to get access to your webcam'; - } - } - }; - if (constraints) - mediaConfig.constraints = constraints; - getUserMedia(mediaConfig); - return true; - } -} - -var Session = { - AudioVideoData: 'audiovideodata', - AudioVideo: 'audiovideo', - AudioData: 'audiodata', - VideoData: 'videodata', - Audio: 'audio', - Video: 'video', - Data: 'data', - ScreenData: 'screendata', - Screen: 'screen' -}; - -var Direction = { - OneWay: 'oneway', - OneToOne: 'onetoone', - OneToMany: 'onetomany', - ManyToMany: 'manytomany' -}; - -String.prototype.isAudio = function() { - var session = this + ''; - return session === Session.Audio || session === Session.AudioData; -}; - -String.prototype.isScreen = function() { - var session = this + ''; - return session === Session.Screen || session === Session.ScreenData; -}; - -String.prototype.lowercase = function() { - var str = this + ''; - return str.toLowerCase().replace( /-|( )|\+|only|and/g , ''); -}; - -window.MediaStream = window.MediaStream || window.webkitMediaStream; - -window.moz = !!navigator.mozGetUserMedia; -var RTCPeerConnection = function(options) { - var w = window, - PeerConnection = w.mozRTCPeerConnection || w.webkitRTCPeerConnection, - SessionDescription = w.mozRTCSessionDescription || w.RTCSessionDescription, - IceCandidate = w.mozRTCIceCandidate || w.RTCIceCandidate; - - var STUN = { - url: !moz ? 'stun:stun.l.google.com:19302' : 'stun:23.21.150.121' - }; - var TURN = { - url: 'turn:homeo@turn.bistri.com:80', - credential: 'homeo' - }; - - var iceServers = { - iceServers: [STUN] - }; - - if (!moz) { - // in chrome M29 and higher - if (parseInt(navigator.userAgent.match( /Chrom(e|ium)\/([0-9]+)\./ )[2]) >= 28) - TURN = { - url: 'turn:turn.bistri.com:80', - credential: 'homeo', - username: 'homeo' - }; - - // No STUN to make sure it works all the time! - iceServers.iceServers = [STUN, TURN]; - } - - var optional = { - optional: [] - }; - - if (!moz) { - optional.optional = [{ - DtlsSrtpKeyAgreement: true - }]; - if (options.onChannelMessage) - optional.optional = [{ - RtpDataChannels: true - }]; - } - - var peerConnection = new PeerConnection(iceServers, optional); - - openOffererChannel(); - - peerConnection.onicecandidate = onicecandidate; - if (options.attachStream) - peerConnection.addStream(options.attachStream); - peerConnection.onaddstream = onaddstream; - - function onicecandidate(event) { - if (!event.candidate || !peerConnection) - return; - if (options.onICE) - options.onICE(event.candidate); - } - - var remoteStreamEventFired = false; - - function onaddstream(event) { - if (remoteStreamEventFired || !event || !options.onRemoteStream) - return; - remoteStreamEventFired = true; - options.onRemoteStream(event.stream); - } - - var constraints = options.constraints || { - optional: [], - mandatory: { - OfferToReceiveAudio: true, - OfferToReceiveVideo: true - } - }; - - var chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'.split(''), - extractedChars = ''; - - function getChars() { - extractedChars += chars[parseInt(Math.random() * 40)] || ''; - if (extractedChars.length < 40) - getChars(); - - return extractedChars; - } - - function createOffer() { - if (!options.onOfferSDP) - return; - - peerConnection.createOffer(function(sessionDescription) { - peerConnection.setLocalDescription(sessionDescription); - options.onOfferSDP(sessionDescription); - }, onSdpError, constraints); - } - - function createAnswer() { - if (!options.onAnswerSDP) - return; - - options.offerSDP = new SessionDescription(options.offerSDP); - peerConnection.setRemoteDescription(options.offerSDP, onSdpSuccess, onSdpError); - - peerConnection.createAnswer(function(sessionDescription) { - peerConnection.setLocalDescription(sessionDescription); - options.onAnswerSDP(sessionDescription); - }, onSdpError, constraints); - } - - if ((options.onChannelMessage && !moz) || !options.onChannelMessage) { - createOffer(); - createAnswer(); - } - - var channel; - - function openOffererChannel() { - if (!options.onChannelMessage || (moz && !options.onOfferSDP)) - return; - - _openOffererChannel(); - - if (moz && !options.attachStream) { - navigator.mozGetUserMedia({ - audio: true, - fake: true - }, function(stream) { - peerConnection.addStream(stream); - createOffer(); - }, useless); - } - } - - function _openOffererChannel() { - channel = peerConnection.createDataChannel( - options.channel || 'RTCDataChannel', - moz ? { } : { - reliable: false - }); - - if (moz) channel.binaryType = 'blob'; - setChannelEvents(); - } - - function setChannelEvents() { - channel.onmessage = function(event) { - if (options.onChannelMessage) - options.onChannelMessage(event); - }; - - channel.onopen = function() { - if (options.onChannelOpened) - options.onChannelOpened(channel); - }; - channel.onclose = function(event) { - if (options.onChannelClosed) - options.onChannelClosed(event); - console.warn('WebRTC Data Channel closed.', event); - }; - channel.onerror = function(event) { - if (options.onChannelError) - options.onChannelError(event); - console.error('WebRTC Data Channel error:', event); - }; - } - - if (options.onAnswerSDP && moz) - openAnswererChannel(); - - function openAnswererChannel() { - peerConnection.ondatachannel = function(event) { - channel = event.channel; - channel.binaryType = 'blob'; - setChannelEvents(); - }; - - if (moz && !options.attachStream) { - navigator.mozGetUserMedia({ - audio: true, - fake: true - }, function(stream) { - peerConnection.addStream(stream); - createAnswer(); - }, useless); - } - } - - function useless() { - } - - function onSdpSuccess() { - } - - function onSdpError(e) { - console.error('sdp error:', e.name, e.message); - } - - return { - addAnswerSDP: function(sdp) { - sdp = new SessionDescription(sdp); - peerConnection.setRemoteDescription(sdp, onSdpSuccess, onSdpError); - }, - addICE: function(candidate) { - peerConnection.addIceCandidate(new IceCandidate({ - sdpMLineIndex: candidate.sdpMLineIndex, - candidate: candidate.candidate - })); - }, - - peer: peerConnection, - channel: channel, - sendData: function(message) { - channel && channel.send(message); - } - }; -}; - -var video_constraints = { - mandatory: { }, - optional: [] -}; - -function getUserMedia(options) { - var n = navigator, - media; - n.getMedia = n.webkitGetUserMedia || n.mozGetUserMedia; - n.getMedia(options.constraints || { - audio: true, - video: video_constraints - }, streaming, options.onerror || function(e) { - console.error(e); - }); - - function streaming(stream) { - var video = options.video; - if (video) { - video[moz ? 'mozSrcObject' : 'src'] = moz ? stream : window.webkitURL.createObjectURL(stream); - video.play(); - } - options.onsuccess && options.onsuccess(stream); - media = stream; - } - - return media; -} - -function RTCMultiSession(config) { - var session = config.session || Session.AudioVideo, - direction = config.direction || Direction.ManyToMany, - self = { - id: uniqueToken(), - sessionid: config.channel - }, - channels = '--', - isbroadcaster, - isAcceptNewSession = true, - defaultSocket = { }, RTCDataChannels = []; - - function openDefaultSocket() { - defaultSocket = config.openSignalingChannel({ - onmessage: onDefaultSocketResponse - }); - } - - function onDefaultSocketResponse(response) { - if (response.userid == self.id) - return; - - if (isAcceptNewSession && response.sessionid && response.userid) - if (config.onNewSession) - config.onNewSession(response); - - if (response.newParticipant) - onNewParticipant(response.newParticipant); - - if (response.userid && response.targetUser == self.id && response.participant && channels.indexOf(response.userid) == -1) { - channels += response.userid + '--'; - openSubSocket({ - isofferer: true, - channel: response.channel || response.userid, - closeSocket: true - }); - } - } - - function openSubSocket(_config) { - if (!_config.channel) - return; - var socketConfig = { - channel: _config.channel, - onmessage: socketResponse, - onopen: function() { - if (isofferer && !peer) - initPeer(); - } - }; - - var socket = config.openSignalingChannel(socketConfig), - isofferer = _config.isofferer, - isGotRemoteStream, - inner = { }, - mediaElement = document.createElement(session.isAudio() ? 'audio' : 'video'), - peer; - - var peerConfig = { - onICE: function(candidate) { - socket.send({ - id: self.id, - candidate: { - sdpMLineIndex: candidate.sdpMLineIndex, - candidate: JSON.stringify(candidate.candidate) - } - }); - }, - onChannelOpened: onChannelOpened, - onChannelMessage: function(event) { - if (config.onChannelMessage) - config.onChannelMessage(event.data); - }, - attachStream: config.attachStream, - onRemoteStream: function(stream) { - mediaElement[moz ? 'mozSrcObject' : 'src'] = moz ? stream : window.webkitURL.createObjectURL(stream); - mediaElement.autoplay = true; - mediaElement.controls = true; - mediaElement.play(); - - _config.stream = stream; - if (session.isAudio()) { - mediaElement.addEventListener('play', function() { - setTimeout(function() { - mediaElement.muted = false; - mediaElement.volume = 1; - window.audio = mediaElement; - afterRemoteStreamStartedFlowing(); - }, 3000); - }, false); - } else - onRemoteStreamStartsFlowing(); - }, - iceServers: config.iceServers, - session: session - }; - - function initPeer(offerSDP) { - if (direction === Direction.OneToOne && window.isFirstConnectionOpened) - return; - if (!offerSDP) { - peerConfig.onOfferSDP = sendsdp; - } else { - peerConfig.offerSDP = offerSDP; - peerConfig.onAnswerSDP = sendsdp; - } - - if (session.indexOf('data') === -1) - peerConfig.onChannelMessage = null; - - if (session.isAudio()) { - /* OfferToReceiveVideo MUST be false for audio-only streaming */ - peerConfig.constraints = { - optional: [], - mandatory: { - OfferToReceiveAudio: true, - OfferToReceiveVideo: false - } - }; - } - - peer = RTCPeerConnection(peerConfig); - } - - function onRemoteStreamStartsFlowing() { - if (!(mediaElement.readyState <= HTMLMediaElement.HAVE_CURRENT_DATA || mediaElement.paused || mediaElement.currentTime <= 0)) { - afterRemoteStreamStartedFlowing(); - } else - setTimeout(onRemoteStreamStartsFlowing, 300); - } - - function afterRemoteStreamStartedFlowing() { - if (config.onRemoteStream) - config.onRemoteStream({ - mediaElement: mediaElement, - stream: _config.stream, - session: session, - direction: direction, - blobURL: mediaElement.mozSrcObject || mediaElement.src, - type: 'remote' - }); - - onSessionOpened(); - } - - function onChannelOpened(channel) { - RTCDataChannels[RTCDataChannels.length] = channel; - if (config.onChannelOpened) config.onChannelOpened(channel); - onSessionOpened(); - } - - function onSessionOpened() { - if (isGotRemoteStream) return; - window.isFirstConnectionOpened = isGotRemoteStream = true; - - if (direction === Direction.ManyToMany && isbroadcaster && channels.split('--').length > 3) { - defaultSocket.send({ - newParticipant: socket.channel, - userid: self.id - }); - } - } - - function sendsdp(sdp) { - sdp = JSON.stringify(sdp); - var part = parseInt(sdp.length / 3); - - var firstPart = sdp.slice(0, part), - secondPart = sdp.slice(part, sdp.length - 1), - thirdPart = ''; - - if (sdp.length > part + part) { - secondPart = sdp.slice(part, part + part); - thirdPart = sdp.slice(part + part, sdp.length); - } - - socket.send({ - id: self.id, - firstPart: firstPart - }); - - socket.send({ - id: self.id, - secondPart: secondPart - }); - - socket.send({ - id: self.id, - thirdPart: thirdPart - }); - } - - function socketResponse(response) { - if (response.id == self.id) - return; - - if (response.firstPart || response.secondPart || response.thirdPart) { - if (response.firstPart) { - inner.firstPart = response.firstPart; - if (inner.secondPart && inner.thirdPart) - selfInvoker(); - } - if (response.secondPart) { - inner.secondPart = response.secondPart; - if (inner.firstPart && inner.thirdPart) - selfInvoker(); - } - - if (response.thirdPart) { - inner.thirdPart = response.thirdPart; - if (inner.firstPart && inner.secondPart) - selfInvoker(); - } - } - - if (response.candidate && !isGotRemoteStream) { - peer && peer.addICE({ - sdpMLineIndex: response.candidate.sdpMLineIndex, - candidate: JSON.parse(response.candidate.candidate) - }); - } - } - - var invokedOnce = false; - - function selfInvoker() { - if (invokedOnce) - return; - - invokedOnce = true; - inner.sdp = JSON.parse(inner.firstPart + inner.secondPart + inner.thirdPart); - if (isofferer) - peer.addAnswerSDP(inner.sdp); - else - initPeer(inner.sdp); - } - } - - function startBroadcasting() { - defaultSocket.send({ - sessionid: self.sessionid, - userid: self.id, - session: config.session, - direction: config.direction - }); - - if (config.direction === Direction.OneToOne) { - if (!window.isFirstConnectionOpened) - setTimeout(startBroadcasting, 3000); - } else - setTimeout(startBroadcasting, 3000); - } - - function onNewParticipant(channel) { - if (!channel || channels.indexOf(channel) != -1 || channel == self.id) - return; - channels += channel + '--'; - - var new_channel = uniqueToken(); - openSubSocket({ - channel: new_channel, - closeSocket: true - }); - - defaultSocket.send({ - participant: true, - userid: self.id, - targetUser: channel, - channel: new_channel - }); - } - - function uniqueToken() { - var s4 = function() { - return Math.floor(Math.random() * 0x10000).toString(16); - }; - return s4() + s4() + "-" + s4() + "-" + s4() + "-" + s4() + "-" + s4() + s4() + s4(); - } - - openDefaultSocket(); - return { - initSession: function() { - isbroadcaster = true; - isAcceptNewSession = false; - startBroadcasting(); - }, - joinSession: function(_config) { - _config = _config || { }; - - session = _config.session; - direction = _config.direction; - - if (_config.sessionid) self.sessionid = _config.sessionid; - isAcceptNewSession = false; - - openSubSocket({ - channel: self.id - }); - - defaultSocket.send({ - participant: true, - userid: self.id, - targetUser: _config.userid - }); - }, - send: function(message) { - var _channels = RTCDataChannels, - data, length = _channels.length; - if (!length) return; - - if (moz && message.file) data = message.file; - else data = JSON.stringify(message); - - for (var i = 0; i < length; i++) - _channels[i].send(data); - }, - getSession: function() { - return { - userid: self.id, - sessionid: self.sessionid, - session: session, - direction: direction - }; - } - }; -} - -var FileSender = { - send: function(config) { - var channel = config.channel, - file = config.file; - - /* if firefox nightly: share file blob directly */ - if (moz) { - /* used on the receiver side to set received file name */ - channel.send({ - fileName: file.name, - type: 'file' - }); - - /* sending the entire file at once */ - channel.send({ - file: file - }); - - if (config.onFileSent) - config.onFileSent(file); - } - - /* if chrome */ - if (!moz) { - var reader = new window.FileReader(); - reader.readAsDataURL(file); - reader.onload = onReadAsDataURL; - } - - var packetSize = 1000 /* chars */, - textToTransfer = '', - numberOfPackets = 0, - packets = 0; - - function onReadAsDataURL(event, text) { - var data = { - type: 'file' - }; - - if (event) { - text = event.target.result; - numberOfPackets = packets = data.packets = parseInt(text.length / packetSize); - } - - if (config.onFileProgress) - config.onFileProgress({ - remaining: packets--, - length: numberOfPackets, - sent: numberOfPackets - packets - }); - - if (text.length > packetSize) - data.message = text.slice(0, packetSize); - else { - data.message = text; - data.last = true; - data.name = file.name; - - if (config.onFileSent) - config.onFileSent(file); - } - - channel.send(data); - - textToTransfer = text.slice(data.message.length); - - if (textToTransfer.length) - setTimeout(function() { - onReadAsDataURL(null, textToTransfer); - }, 500); - } - } -}; - -function FileReceiver() { - var content = [], - fileName = '', - packets = 0, - numberOfPackets = 0; - - function receive(data, config) { - /* if firefox nightly & file blob shared */ - if (moz) { - if (data.fileName) - fileName = data.fileName; - if (data.size) { - var reader = new window.FileReader(); - reader.readAsDataURL(data); - reader.onload = function(event) { - FileSaver.SaveToDisk(event.target.result, fileName); - if (config.onFileReceived) - config.onFileReceived(fileName); - }; - } - } - - if (!moz) { - if (data.packets) - numberOfPackets = packets = parseInt(data.packets); - - if (config.onFileProgress) - config.onFileProgress({ - remaining: packets--, - length: numberOfPackets, - received: numberOfPackets - packets - }); - - content.push(data.message); - - if (data.last) { - FileSaver.SaveToDisk(content.join(''), data.name); - if (config.onFileReceived) - config.onFileReceived(data.name); - content = []; - } - } - } - - return { - receive: receive - }; -} - -var TextSender = { - send: function(config) { - var channel = config.channel, - initialText = config.text, - packetSize = 1000 /* chars */, - textToTransfer = ''; - - if (typeof initialText !== 'string') initialText = JSON.stringify(initialText); - - if (moz || initialText.length <= packetSize) channel.send(config.text); - else sendText(initialText); - - function sendText(textMessage, text) { - var data = { - type: 'text' - }; - - 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; - } - - channel.send(data); - - textToTransfer = text.slice(data.message.length); - - if (textToTransfer.length) - setTimeout(function() { - sendText(null, textToTransfer); - }, 500); - } - } -}; - -function TextReceiver() { - var content = []; - - function receive(data, onmessage) { - content.push(data.message); - if (data.last) { - if (onmessage) onmessage(content.join('')); - content = []; - } - } - - return { - receive: receive - }; -} - - -var FileSaver = { - SaveToDisk: function(fileUrl, fileName) { - var save = document.createElement('a'); - save.href = fileUrl; - save.target = '_blank'; - save.download = fileName || fileUrl; - - var evt = document.createEvent('MouseEvents'); - evt.initMouseEvent('click', true, true, window, 1, 0, 0, 0, 0, false, false, false, false, 0, null); - - save.dispatchEvent(evt); - - (window.URL || window.webkitURL).revokeObjectURL(save.href); - } -}; diff --git a/RTCMultiConnection/Library/RTCMultiConnection-v1.2.js b/RTCMultiConnection/Library/RTCMultiConnection-v1.2.js deleted file mode 100644 index 1d24393e..00000000 --- a/RTCMultiConnection/Library/RTCMultiConnection-v1.2.js +++ /dev/null @@ -1,1300 +0,0 @@ -// Muaz Khan - www.MuazKhan.com -// MIT License - www.WebRTC-Experiment.com/licence -// Documentation - www.RTCMultiConnection.org/docs/ - -// FAQ - www.RTCMultiConnection.org/FAQ/ -// Development News - trello.com/b/8bhi1G6n/RTCMultiConnection - -// v1.2 changes log - www.RTCMultiConnection.org/changes-log/#v1.2 -// _______________________ -// RTCMultiConnection-v1.2 - -(function() { - window.RTCMultiConnection = function(channel, extras) { - extras = extras || { }; - this.channel = channel; - var self = this, - rtcSession, fileReceiver, textReceiver; - - self.onmessage = function(message) { - console.debug('DataChannel message:', message); - }; - - self.onopen = function() { - console.debug('Data connection opened.'); - }; - - self.onFileReceived = function(fileName) { - console.debug('File <', fileName, '> received successfully.'); - }; - - self.onFileSent = function(file) { - console.debug('File <', file.name, '> sent successfully.'); - }; - - self.onFileProgress = function(packets) { - console.debug('<', packets.remaining, '> items remaining.'); - }; - - self.session = extras.session || Session.AudioVideo; - self.direction = extras.direction || Direction.ManyToMany; - - function prepareInit(callback) { - if (!self.openSignalingChannel) { - if (typeof self.transmitRoomOnce == 'undefined') self.transmitRoomOnce = true; - - // socket.io over node.js: https://github.com/muaz-khan/WebRTC-Experiment/blob/master/socketio-over-nodejs - self.openSignalingChannel = function(config) { - config = config || { }; - - channel = config.channel || self.channel || 'default-channel'; - var socket = new window.Firebase('https://' + (self.firebase || 'chat') + '.firebaseIO.com/' + channel); - socket.channel = channel; - socket.on('child_added', function(data) { - var value = data.val(); - config.onmessage(value); - }); - - socket.send = function(data) { - this.push(data); - }; - - if (!self.socket) self.socket = socket; - if (channel != self.channel || (self.isInitiator && channel == self.channel)) - socket.onDisconnect().remove(); - - if (config.onopen) setTimeout(config.onopen, 1); - return socket; - }; - - if (!window.Firebase) { - var script = document.createElement('script'); - script.src = 'https://cdn.firebase.com/v0/firebase.js'; - script.onload = callback; - document.documentElement.appendChild(script); - } else callback(); - } else callback(); - } - - function init() { - if (self.config) return; - - self.direction = self.direction.lowercase(); - self.session = self.session.lowercase(); - - self.config = { - openSignalingChannel: function(config) { - return self.openSignalingChannel(config); - }, - onNewSession: function(session) { - if (self.channel !== session.sessionid) return false; - - if (!rtcSession) { - self._session = session; - return; - } - - if (self.onNewSession) return self.onNewSession(session); - - if (self.joinedARoom) return false; - self.joinedARoom = true; - - return joinSession(session, session.extra); - }, - onChannelOpened: function(userid) { - self.onopen(userid); - }, - onChannelMessage: function(data) { - if (!data.size) data = JSON.parse(data); - - if (data.type === 'text') - textReceiver.receive(data, self.onmessage); - else if (data.size || data.type === 'file') - fileReceiver.receive(data, self.config); - else self.onmessage(data); - }, - onChannelClosed: function(event) { - self.onclose(event); - }, - onChannelError: function(event) { - self.onerror(event); - }, - onFileReceived: function(fileName) { - self.onFileReceived(fileName); - }, - onFileProgress: function(packets) { - self.onFileProgress(packets); - }, - iceServers: self.iceServers, - attachStream: self.attachStream, - onRemoteStream: function(stream) { - self.onstream(stream); - }, - onleave: function(userid, extra) { - self.onleave(userid, extra); - }, - direction: self.direction.lowercase(), - session: self.session.lowercase(), - channel: self.channel, - transmitRoomOnce: self.transmitRoomOnce, - autoCloseEntireSession: typeof self.autoCloseEntireSession == 'undefined' ? false : self.autoCloseEntireSession, - connection: self - }; - rtcSession = new RTCMultiSession(self.config); - - // bug: these two must be fixed. Must be able to receive many files concurrently. - fileReceiver = new FileReceiver(); - textReceiver = new TextReceiver(); - - if (self._session) self.config.onNewSession(self._session); - } - - function joinSession(session, extra) { - if (!session || !session.userid || !session.sessionid) - throw 'invalid data passed.'; - - self.session = session.session; - self.direction = session.direction; - - if (session.direction === Direction.OneWay || session.session === Session.Data) - rtcSession.joinSession(session, extra || { }); - else - captureUserMedia(function() { - rtcSession.joinSession(session, extra || { }); - }); - } - - self.join = joinSession; - - self.open = function(_channel, extra) { - if (self.socket) self.socket.onDisconnect().remove(); - - self.isInitiator = true; - - if (typeof _channel === 'string') { - if (_channel) self.channel = _channel; - extra = extra || { }; - } else extra = _channel || { }; - - prepareInit(function() { - init(); - captureUserMedia(function() { - rtcSession.initSession(extra); - }); - }); - }; - - self.connect = function(_channel) { - if (_channel) self.channel = _channel; - - prepareInit(init); - }; - - self.onstream = function(stream) { - console.debug('stream:', stream); - }; - - self.send = function(data) { - if (!data) throw 'No file, data or text message to share.'; - if (data.size) - FileSender.send({ - file: data, - channel: rtcSession, - onFileSent: function(file) { - self.onFileSent(file); - }, - onFileProgress: function(packets) { - self.onFileProgress(packets); - } - }); - else - TextSender.send({ - text: data, - channel: rtcSession - }); - }; - - function captureUserMedia(callback) { - var constraints, session = self.session; - - if (session === Session.Data || self.dontAttachStream || self.config.attachStream) - return callback(); - - if (self.attachStream) { - self.config.attachStream = self.attachStream; - return callback(); - } - - if (session.isAudio()) { - console.debug('audio-only session'); - constraints = { - audio: true, - video: false - }; - } - - if (session.isScreen()) { - console.debug('screen-only session'); - video_constraints = { - mandatory: { - chromeMediaSource: 'screen' - }, - optional: [] - }; - constraints = { - audio: false, - video: video_constraints - }; - } - - if (session === Session.Video || session === Session.VideoData) { - console.debug('audio-less video-only session.'); - video_constraints = { - mandatory: { }, - optional: [] - }; - constraints = { - audio: false, - video: video_constraints - }; - } - var mediaElement = document.createElement(session.isAudio() ? 'audio' : 'video'); - var mediaConfig = { - video: mediaElement, - onsuccess: function(stream) { - self.config.attachStream = stream; - callback && callback(); - - // issue #37: Sometimes Firefox crashes while echoing and you have to hard reboot your PC ( Ubuntu 13.04, FF 21 ) - mediaElement.muted = true; - self.onstream({ - stream: stream, - mediaElement: mediaElement, - blobURL: mediaElement.mozSrcObject || mediaElement.src, - type: 'local' - }); - }, - onerror: function() { - if (session.isAudio()) - throw 'Unable to get access to your microphone'; - else if (session.isScreen()) { - if (location.protocol === 'http:') { - throw 'Please test this WebRTC experiment on HTTPS.'; - } else { - throw 'Screen capturing is either denied or not supported. Are you enabled flag: "Enable screen capture support in getUserMedia"?'; - } - } else { - throw 'Unable to get access to your webcam'; - } - } - }; - if (constraints) - mediaConfig.constraints = constraints; - - getUserMedia(mediaConfig); - return true; - } - - this.onUserLeft = function(userid /*, extra */) { - console.debug(userid, 'left!'); - }; - - this.onleave = function(userid, extra) { - self.onUserLeft(userid, extra); - }; - - this.leave = function(userid) { - rtcSession.leave(userid, self.autoCloseEntireSession); - }; - - this.eject = function(userid) { - if (!userid) throw '"user-id" is mandatory.'; - rtcSession.leave(userid, self.autoCloseEntireSession); - }; - - for (var extra in extras) { - this[extra] = extras[extra]; - } - - if (self.channel) self.connect(); - }; - - var Session = { - AudioVideoData: 'audiovideodata', - AudioVideo: 'audiovideo', - AudioData: 'audiodata', - VideoData: 'videodata', - Audio: 'audio', - Video: 'video', - Data: 'data', - ScreenData: 'screendata', - Screen: 'screen' - }; - window.RTCSession = Session; - - var Direction = { - OneWay: 'oneway', - OneToOne: 'onetoone', - OneToMany: 'onetomany', - ManyToMany: 'manytomany' - }; - window.RTCDirection = Direction; - - String.prototype.isAudio = function() { - var session = this + ''; - return session === Session.Audio || session === Session.AudioData; - }; - - String.prototype.isScreen = function() { - var session = this + ''; - return session === Session.Screen || session === Session.ScreenData; - }; - - String.prototype.lowercase = function() { - var str = this + ''; - return str.toLowerCase().replace( /-|( )|\+|only|and/g , ''); - }; - - Array.prototype.swap = function() { - var swapped = [], - arr = this, - length = arr.length; - for (var i = 0; i < length; i++) { - if (arr[i]) swapped[swapped.length] = arr[i]; - } - return swapped; - }; - - window.MediaStream = window.MediaStream || window.webkitMediaStream; - - window.moz = !!navigator.mozGetUserMedia; - var RTCPeerConnection = function(options) { - var w = window, - PeerConnection = w.mozRTCPeerConnection || w.webkitRTCPeerConnection, - SessionDescription = w.mozRTCSessionDescription || w.RTCSessionDescription, - IceCandidate = w.mozRTCIceCandidate || w.RTCIceCandidate; - - var STUN = { - url: !moz ? 'stun:stun.l.google.com:19302' : 'stun:23.21.150.121' - }; - - var TURN = { - url: 'turn:homeo@turn.bistri.com:80', - credential: 'homeo' - }; - - var iceServers = { - iceServers: options.iceServers || [STUN] - }; - - if (!moz && !options.iceServers) { - if (parseInt(navigator.userAgent.match( /Chrom(e|ium)\/([0-9]+)\./ )[2]) >= 28) - TURN = { - url: 'turn:turn.bistri.com:80', - credential: 'homeo', - username: 'homeo' - }; - - // No STUN to make sure it works all the time! - iceServers.iceServers = [STUN, TURN]; - } - - var optional = { - optional: [] - }; - - if (!moz) { - optional.optional = [{ - DtlsSrtpKeyAgreement: true - }]; - if (options.onChannelMessage) - optional.optional = [{ - RtpDataChannels: true - }]; - } - - var peerConnection = new PeerConnection(iceServers, optional); - - openOffererChannel(); - - peerConnection.onicecandidate = onicecandidate; - if (options.attachStream) - peerConnection.addStream(options.attachStream); - peerConnection.onaddstream = onaddstream; - - function onicecandidate(event) { - if (!event.candidate || !peerConnection) - return; - if (options.onICE) - options.onICE(event.candidate); - } - - var remoteStreamEventFired = false; - - function onaddstream(event) { - if (remoteStreamEventFired || !event || !options.onRemoteStream) - return; - remoteStreamEventFired = true; - options.onRemoteStream(event.stream); - } - - var constraints = options.constraints || { - optional: [], - mandatory: { - OfferToReceiveAudio: true, - OfferToReceiveVideo: true - } - }; - - var chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'.split(''), - extractedChars = ''; - - function getChars() { - extractedChars += chars[parseInt(Math.random() * 40)] || ''; - if (extractedChars.length < 40) - getChars(); - - return extractedChars; - } - - function createOffer() { - if (!options.onOfferSDP) - return; - - peerConnection.createOffer(function(sessionDescription) { - peerConnection.setLocalDescription(sessionDescription); - options.onOfferSDP(sessionDescription); - }, onSdpError, constraints); - } - - function createAnswer() { - if (!options.onAnswerSDP) - return; - - options.offerSDP = new SessionDescription(options.offerSDP); - peerConnection.setRemoteDescription(options.offerSDP, onSdpSuccess, onSdpError); - - peerConnection.createAnswer(function(sessionDescription) { - peerConnection.setLocalDescription(sessionDescription); - options.onAnswerSDP(sessionDescription); - }, onSdpError, constraints); - } - - if ((options.onChannelMessage && !moz) || !options.onChannelMessage) { - createOffer(); - createAnswer(); - } - - var channel; - - function openOffererChannel() { - if (!options.onChannelMessage || (moz && !options.onOfferSDP)) - return; - - _openOffererChannel(); - - if (moz && !options.attachStream) { - navigator.mozGetUserMedia({ - audio: true, - fake: true - }, function(stream) { - peerConnection.addStream(stream); - createOffer(); - }, useless); - } - } - - function _openOffererChannel() { - channel = peerConnection.createDataChannel( - options.channel || 'RTCDataChannel', - moz ? { } : { - reliable: false - }); - - if (moz) channel.binaryType = 'blob'; - setChannelEvents(); - } - - function setChannelEvents() { - channel.onmessage = function(event) { - if (options.onChannelMessage) - options.onChannelMessage(event); - }; - - channel.onopen = function() { - if (options.onChannelOpened) - options.onChannelOpened(channel); - }; - channel.onclose = function(event) { - if (options.onChannelClosed) - options.onChannelClosed(event); - console.warn('WebRTC Data Channel closed.', event); - }; - channel.onerror = function(event) { - if (options.onChannelError) - options.onChannelError(event); - console.error('WebRTC Data Channel error:', event); - }; - } - - if (options.onAnswerSDP && moz && options.onChannelMessage) - openAnswererChannel(); - - function openAnswererChannel() { - peerConnection.ondatachannel = function(event) { - channel = event.channel; - channel.binaryType = 'blob'; - setChannelEvents(); - }; - - if (moz && !options.attachStream) { - navigator.mozGetUserMedia({ - audio: true, - fake: true - }, function(stream) { - peerConnection.addStream(stream); - createAnswer(); - }, useless); - } - } - - function useless() { - } - - function onSdpSuccess() { - } - - function onSdpError(e) { - console.error('sdp error:', e.name, e.message); - } - - return { - addAnswerSDP: function(sdp) { - sdp = new SessionDescription(sdp); - peerConnection.setRemoteDescription(sdp, onSdpSuccess, onSdpError); - }, - addICE: function(candidate) { - peerConnection.addIceCandidate(new IceCandidate({ - sdpMLineIndex: candidate.sdpMLineIndex, - candidate: candidate.candidate - })); - }, - - peer: peerConnection, - channel: channel, - sendData: function(message) { - channel && channel.send(message); - } - }; - }; - - var video_constraints = { - mandatory: { }, - optional: [] - }; - - function getUserMedia(options) { - var n = navigator, - media; - n.getMedia = n.webkitGetUserMedia || n.mozGetUserMedia; - n.getMedia(options.constraints || { - audio: true, - video: video_constraints - }, streaming, options.onerror || function(e) { - console.error(e); - }); - - function streaming(stream) { - var video = options.video; - if (video) { - video[moz ? 'mozSrcObject' : 'src'] = moz ? stream : window.webkitURL.createObjectURL(stream); - video.play(); - } - options.onsuccess && options.onsuccess(stream); - media = stream; - } - - return media; - } - - function RTCMultiSession(config) { - var session = config.session || Session.AudioVideo, - direction = config.direction || Direction.ManyToMany, - self = { - id: uniqueToken(), - sessionid: config.channel, - sockets: [], - socketObjects: { } - }, - channels = '--', - isbroadcaster, - isAcceptNewSession = true, - defaultSocket = { }, RTCDataChannels = []; - - function openDefaultSocket() { - defaultSocket = config.openSignalingChannel({ - onmessage: onDefaultSocketResponse, - callback: function(socket) { - defaultSocket = socket; - } - }); - } - - function onDefaultSocketResponse(response) { - if (response.userid == self.id) - return; - - if (isAcceptNewSession && response.sessionid && response.userid) - if (config.onNewSession) - config.onNewSession(response); - - if (response.newParticipant && self.joinedARoom && self.broadcasterid === response.userid) - onNewParticipant(response.newParticipant, response.extra); - - if (response.userid && response.targetUser == self.id && response.participant && channels.indexOf(response.userid) == -1) { - channels += response.userid + '--'; - openSubSocket({ - isofferer: true, - channel: response.channel || response.userid, - closeSocket: true, - extra: response.extra - }); - } - } - - function openSubSocket(_config) { - if (!_config.channel) - return; - var socketConfig = { - channel: _config.channel, - onmessage: socketResponse, - onopen: function() { - if (isofferer && !peer) - initPeer(); - - _config.socketIndex = socket.index = self.sockets.length; - self.socketObjects[socketConfig.channel] = socket; - self.sockets[_config.socketIndex] = socket; - } - }; - - socketConfig.callback = function(_socket) { - socket = _socket; - this.onopen(); - }; - - var socket = config.openSignalingChannel(socketConfig), - isofferer = _config.isofferer, - extra = _config.extra || { }, - isGotRemoteStream, - inner = { }, - mediaElement = document.createElement(session.isAudio() ? 'audio' : 'video'), - peer; - - var peerConfig = { - onICE: function(candidate) { - socket.send({ - id: self.id, - candidate: { - sdpMLineIndex: candidate.sdpMLineIndex, - candidate: JSON.stringify(candidate.candidate) - } - }); - }, - onChannelOpened: onChannelOpened, - onChannelMessage: function(event) { - if (config.onChannelMessage) - config.onChannelMessage(event.data); - }, - attachStream: config.attachStream, - onRemoteStream: function(stream) { - mediaElement[moz ? 'mozSrcObject' : 'src'] = moz ? stream : window.webkitURL.createObjectURL(stream); - mediaElement.autoplay = true; - mediaElement.controls = true; - mediaElement.play(); - - _config.stream = stream; - if (session.isAudio()) { - mediaElement.addEventListener('play', function() { - setTimeout(function() { - mediaElement.muted = false; - mediaElement.volume = 1; - window.audio = mediaElement; - afterRemoteStreamStartedFlowing(); - }, 3000); - }, false); - } else - onRemoteStreamStartsFlowing(); - }, - iceServers: config.iceServers, - session: session - }; - - function initPeer(offerSDP) { - if (direction === Direction.OneToOne && window.isFirstConnectionOpened) - return; - if (!offerSDP) { - peerConfig.onOfferSDP = sendsdp; - } else { - peerConfig.offerSDP = offerSDP; - peerConfig.onAnswerSDP = sendsdp; - } - - if (session.indexOf('data') === -1) - peerConfig.onChannelMessage = null; - - if (session.isAudio()) { - /* OfferToReceiveVideo MUST be false for audio-only streaming */ - peerConfig.constraints = { - optional: [], - mandatory: { - OfferToReceiveAudio: true, - OfferToReceiveVideo: false - } - }; - } - - peer = RTCPeerConnection(peerConfig); - } - - function onRemoteStreamStartsFlowing() { - if (!(mediaElement.readyState <= HTMLMediaElement.HAVE_CURRENT_DATA || mediaElement.paused || mediaElement.currentTime <= 0)) { - afterRemoteStreamStartedFlowing(); - } else - setTimeout(onRemoteStreamStartsFlowing, 300); - } - - function afterRemoteStreamStartedFlowing() { - if (config.onRemoteStream) - config.onRemoteStream({ - mediaElement: mediaElement, - stream: _config.stream, - session: session, - direction: direction, - blobURL: mediaElement.mozSrcObject || mediaElement.src, - type: 'remote', - extra: extra || { }, - userid: _config.userid - }); - - onSessionOpened(); - } - - function onChannelOpened(channel) { - RTCDataChannels[RTCDataChannels.length] = channel; - if (config.onChannelOpened) config.onChannelOpened(extra); - onSessionOpened(); - } - - function onSessionOpened() { - if (isGotRemoteStream) return; - window.isFirstConnectionOpened = isGotRemoteStream = true; - - if (direction === Direction.ManyToMany && isbroadcaster && channels.split('--').length > 3) { - if (!_config.alreadySentDetails) { - _config.alreadySentDetails = true; - defaultSocket.send({ - newParticipant: socket.channel, - userid: self.id, - extra: extra - }); - } - } - } - - function sendsdp(sdp) { - sdp = JSON.stringify(sdp); - var part = parseInt(sdp.length / 3); - - var firstPart = sdp.slice(0, part), - secondPart = sdp.slice(part, sdp.length - 1), - thirdPart = ''; - - if (sdp.length > part + part) { - secondPart = sdp.slice(part, part + part); - thirdPart = sdp.slice(part + part, sdp.length); - } - - socket.send({ - id: self.id, - firstPart: firstPart - }); - - socket.send({ - id: self.id, - secondPart: secondPart - }); - - socket.send({ - id: self.id, - thirdPart: thirdPart - }); - } - - function socketResponse(response) { - if (response.id == self.id) - return; - - if (response.firstPart || response.secondPart || response.thirdPart) { - if (response.firstPart) { - // sdp sender's user id passed over onRemoteStream and onopen - _config.userid = response.id; - - inner.firstPart = response.firstPart; - if (inner.secondPart && inner.thirdPart) - selfInvoker(); - } - if (response.secondPart) { - inner.secondPart = response.secondPart; - if (inner.firstPart && inner.thirdPart) - selfInvoker(); - } - - if (response.thirdPart) { - inner.thirdPart = response.thirdPart; - if (inner.firstPart && inner.secondPart) - selfInvoker(); - } - } - - if (response.candidate && !isGotRemoteStream) { - peer && peer.addICE({ - sdpMLineIndex: response.candidate.sdpMLineIndex, - candidate: JSON.parse(response.candidate.candidate) - }); - } - - if (response.left) { - if (peer && peer.peer) { - peer.peer.close(); - peer.peer = null; - } - - if (response.closeEntireSession) { - // room owner asked me to leave his room - leaveARoom(); - } else if (socket) { - socket.send({ - left: true, - extra: self.extra, - id: self.id - }); - - if (self.sockets[_config.socketIndex]) delete self.sockets[_config.socketIndex]; - if (self.socketObjects[socket.channel]) delete self.socketObjects[socket.channel]; - - socket = null; - } - - if (config.onleave) config.onleave(response.id, response.extra); - } - - if (response.playRoleOfBroadcaster) - setTimeout(function() { - self.id = response.id; - config.connection.open({ - extra: self.extra - }); - self.sockets = self.sockets.swap(); - }, 600); - } - - var invokedOnce = false; - - function selfInvoker() { - if (invokedOnce) - return; - - invokedOnce = true; - inner.sdp = JSON.parse(inner.firstPart + inner.secondPart + inner.thirdPart); - if (isofferer) - peer.addAnswerSDP(inner.sdp); - else - initPeer(inner.sdp); - } - } - - function onNewParticipant(channel, extra) { - if (!channel || channels.indexOf(channel) != -1 || channel == self.id) - return; - channels += channel + '--'; - - var new_channel = uniqueToken(); - openSubSocket({ - channel: new_channel, - closeSocket: true, - extra: extra || { } - }); - - defaultSocket.send({ - participant: true, - userid: self.id, - targetUser: channel, - channel: new_channel, - extra: self.extra - }); - } - - function uniqueToken() { - return (Math.random() * new Date().getTime()).toString(36).toUpperCase().replace( /\./g , '-'); - } - - function leaveARoom(channel) { - var alert = { - left: true, - extra: self.extra || { }, - id: self.id - }; - - // if room initiator is leaving the room; close the entire session - if (isbroadcaster) { - if (config.autoCloseEntireSession) alert.closeEntireSession = true; - else - self.sockets[0].send({ - playRoleOfBroadcaster: true, - id: self.id - }); - } - - if (!channel) { - // closing all sockets - var sockets = self.sockets, - length = sockets.length; - - for (var i = 0; i < length; i++) { - var socket = sockets[i]; - if (socket) { - socket.send(alert); - - if (self.socketObjects[socket.channel]) - delete self.socketObjects[socket.channel]; - - delete sockets[i]; - } - } - - that.left = true; - } - - // eject a specific user! - if (channel) { - socket = self.socketObjects[channel]; - if (socket) { - socket.send(alert); - - if (self.sockets[socket.index]) - delete self.sockets[socket.index]; - - delete self.socketObjects[channel]; - } - } - self.sockets = self.sockets.swap(); - } - - var that = this; - - window.onbeforeunload = function() { - leaveARoom(); - }; - - window.onkeyup = function(e) { - if (e.keyCode == 116) leaveARoom(); - }; - - (function() { - var anchors = document.querySelectorAll('a'), - length = anchors.length; - for (var i = 0; i < length; i++) { - a = anchors[i]; - if (a.href.indexOf('#') !== 0 && a.getAttribute('target') != '_blank') - a.onclick = function() { - leaveARoom(); - }; - } - })(); - - openDefaultSocket(); - return { - initSession: function(extra) { - isbroadcaster = true; - isAcceptNewSession = false; - - extra = extra || { }; - - extra.interval = extra.interval || 3000; - self.extra = extra.extra = extra.extra || { }; - - (function transmit() { - defaultSocket && defaultSocket.send({ - sessionid: self.sessionid, - userid: self.id, - session: config.session, - direction: config.direction, - extra: extra.extra - }); - - if (!config.transmitRoomOnce && !that.leaving) { - if (config.direction === Direction.OneToOne) { - if (!window.isFirstConnectionOpened) - setTimeout(transmit, extra.interval); - } else - setTimeout(transmit, extra.interval); - } - })(); - - self.extra = extra; - }, - joinSession: function(_config, extra) { - _config = _config || { }; - self.extra = extra; - - session = _config.session; - direction = _config.direction; - - self.joinedARoom = true; - - if (_config.sessionid) self.sessionid = _config.sessionid; - isAcceptNewSession = false; - - openSubSocket({ - channel: self.id, - extra: _config.extra || { } - }); - - defaultSocket.send({ - participant: true, - userid: self.id, - targetUser: _config.userid, - extra: extra || { } - }); - - // used to make sure each room's messages must be stay in the same room - // outsiders must be unable to acess them - self.broadcasterid = _config.userid; - }, - send: function(message) { - var _channels = RTCDataChannels, - data, length = _channels.length; - if (!length) return; - - if (moz && message.file) data = message.file; - else data = JSON.stringify(message); - - for (var i = 0; i < length; i++) - _channels[i].send(data); - }, - getSession: function() { - return { - userid: self.id, - sessionid: self.sessionid, - session: session, - direction: direction - }; - }, - leave: function(userid, autoCloseEntireSession) { - if (autoCloseEntireSession) config.autoCloseEntireSession = true; - leaveARoom(userid); - if (!userid) { - self.joinedARoom = isbroadcaster = false; - isAcceptNewSession = true; - } - } - }; - } - - var FileSender = { - send: function(config) { - var channel = config.channel, - file = config.file; - - /* if firefox nightly: share file blob directly */ - if (moz) { - /* used on the receiver side to set received file name */ - channel.send({ - fileName: file.name, - type: 'file' - }); - - /* sending the entire file at once */ - channel.send({ - file: file - }); - - if (config.onFileSent) - config.onFileSent(file); - } - - /* if chrome */ - if (!moz) { - var reader = new window.FileReader(); - reader.readAsDataURL(file); - reader.onload = onReadAsDataURL; - } - - var packetSize = 1000 /* chars */, - textToTransfer = '', - numberOfPackets = 0, - packets = 0; - - function onReadAsDataURL(event, text) { - var data = { - type: 'file' - }; - - if (event) { - text = event.target.result; - numberOfPackets = packets = data.packets = parseInt(text.length / packetSize); - } - - if (config.onFileProgress) - config.onFileProgress({ - remaining: packets--, - length: numberOfPackets, - sent: numberOfPackets - packets - }); - - if (text.length > packetSize) - data.message = text.slice(0, packetSize); - else { - data.message = text; - data.last = true; - data.name = file.name; - - if (config.onFileSent) - config.onFileSent(file); - } - - channel.send(data); - - textToTransfer = text.slice(data.message.length); - - if (textToTransfer.length) - setTimeout(function() { - onReadAsDataURL(null, textToTransfer); - }, 500); - } - } - }; - - function FileReceiver() { - var content = [], - fileName = '', - packets = 0, - numberOfPackets = 0; - - function receive(data, config) { - /* if firefox nightly & file blob shared */ - if (moz) { - if (data.fileName) - fileName = data.fileName; - if (data.size) { - var reader = new window.FileReader(); - reader.readAsDataURL(data); - reader.onload = function(event) { - FileSaver.SaveToDisk(event.target.result, fileName); - if (config.onFileReceived) - config.onFileReceived(fileName); - }; - } - } - - if (!moz) { - if (data.packets) - numberOfPackets = packets = parseInt(data.packets); - - if (config.onFileProgress) - config.onFileProgress({ - remaining: packets--, - length: numberOfPackets, - received: numberOfPackets - packets - }); - - content.push(data.message); - - if (data.last) { - FileSaver.SaveToDisk(content.join(''), data.name); - if (config.onFileReceived) - config.onFileReceived(data.name); - content = []; - } - } - } - - return { - receive: receive - }; - } - - var TextSender = { - send: function(config) { - var channel = config.channel, - initialText = config.text, - packetSize = 1000 /* chars */, - textToTransfer = ''; - - if (typeof initialText !== 'string') initialText = JSON.stringify(initialText); - - if (moz || initialText.length <= packetSize) channel.send(config.text); - else sendText(initialText); - - function sendText(textMessage, text) { - var data = { - type: 'text' - }; - - 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; - } - - channel.send(data); - - textToTransfer = text.slice(data.message.length); - - if (textToTransfer.length) - setTimeout(function() { - sendText(null, textToTransfer); - }, 500); - } - } - }; - - function TextReceiver() { - var content = []; - - function receive(data, onmessage) { - content.push(data.message); - if (data.last) { - if (onmessage) onmessage(content.join('')); - content = []; - } - } - - return { - receive: receive - }; - } - - var FileSaver = { - SaveToDisk: function(fileUrl, fileName) { - var save = document.createElement('a'); - save.href = fileUrl; - save.target = '_blank'; - save.download = fileName || fileUrl; - - var evt = document.createEvent('MouseEvents'); - evt.initMouseEvent('click', true, true, window, 1, 0, 0, 0, 0, false, false, false, false, 0, null); - - save.dispatchEvent(evt); - - (window.URL || window.webkitURL).revokeObjectURL(save.href); - } - }; -})(); diff --git a/RTCMultiConnection/Library/RTCMultiConnection-v1.3.js b/RTCMultiConnection/Library/RTCMultiConnection-v1.3.js deleted file mode 100644 index 3ecb6134..00000000 --- a/RTCMultiConnection/Library/RTCMultiConnection-v1.3.js +++ /dev/null @@ -1,1455 +0,0 @@ -// Muaz Khan - www.MuazKhan.com -// MIT License - www.WebRTC-Experiment.com/licence -// Documentation - www.RTCMultiConnection.org/docs/ - -// FAQ - www.RTCMultiConnection.org/FAQ/ -// Development News - trello.com/b/8bhi1G6n/RTCMultiConnection - -// v1.3 changes log - www.RTCMultiConnection.org/changes-log/#v1.3 -// _______________________ -// RTCMultiConnection-v1.3 - -(function() { - window.RTCMultiConnection = function(channel) { - this.channel = channel; - - this.open = function(_channel) { - if (_channel) - self.channel = _channel; - - if (self.socket) - self.socket.onDisconnect().remove(); - - self.isInitiator = true; - - prepareInit(function() { - init(); - captureUserMedia(rtcSession.initSession); - }); - }; - - this.connect = function(_channel) { - if (_channel) - self.channel = _channel; - - prepareInit(init); - }; - - this.join = joinSession; - - this.send = function(data, _channel) { - if (!data) - throw 'No file, data or text message to share.'; - - if (data.size) - FileSender.send({ - file: data, - channel: rtcSession, - onFileSent: self.onFileSent, - onFileProgress: self.onFileProgress, - _channel: _channel - }); - else - TextSender.send({ - text: data, - channel: rtcSession, - _channel: _channel - }); - }; - - var self = this, - rtcSession, fileReceiver, textReceiver; - - function prepareInit(callback) { - if (!self.openSignalingChannel) { - if (typeof self.transmitRoomOnce == 'undefined') - self.transmitRoomOnce = true; - - // for custom socket.io over node.js implementation - visit - https://github.com/muaz-khan/WebRTC-Experiment/blob/master/socketio-over-nodejs - self.openSignalingChannel = function(config) { - var channel = config.channel || self.channel || 'default-channel'; - var firebase = new window.Firebase('https://' + (self.firebase || 'chat') + '.firebaseIO.com/' + channel); - firebase.channel = channel; - firebase.on('child_added', function(data) { - config.onmessage(data.val()); - }); - - firebase.send = function(data) { - this.push(data); - }; - - if (!self.socket) - self.socket = firebase; - - if (channel != self.channel || (self.isInitiator && channel == self.channel)) - firebase.onDisconnect().remove(); - - if (config.onopen) - setTimeout(config.onopen, 1); - - return firebase; - }; - - if (!window.Firebase) { - var script = document.createElement('script'); - script.src = 'https://cdn.firebase.com/v0/firebase.js'; - script.onload = callback; - document.documentElement.appendChild(script); - } else - callback(); - } else - callback(); - } - - function init() { - if (self.config) - return; - - self.config = { - onNewSession: function(session) { - if (self.channel !== session.sessionid) - return false; - - if (!rtcSession) { - self._session = session; - return; - } - - if (self.onNewSession) - return self.onNewSession(session); - - if (self.joinedARoom) - return false; - self.joinedARoom = true; - - return joinSession(session); - }, - onmessage: function(e) { - if (!e.data.size) - e.data = JSON.parse(e.data); - - if (e.data.type === 'text') - textReceiver.receive({ - data: e.data, - connection: self - }); - - else if (e.data.size || e.data.type === 'file') - fileReceiver.receive({ - data: e.data, - connection: self - }); - else - self.onmessage(e); - } - }; - rtcSession = new RTCMultiSession(self); - - // bug: these two must be fixed. Must be able to receive many files concurrently. - // Note: this bug is fixed in v1.4 - fileReceiver = new FileReceiver(); - textReceiver = new TextReceiver(); - - if (self._session) - self.config.onNewSession(self._session); - } - - function joinSession(session) { - if (!session || !session.userid || !session.sessionid) - throw 'invalid data passed.'; - - self.session = session.session; - - extra = self.extra || session.extra || { }; - - if (session.oneway || session.data) - rtcSession.joinSession(session, extra); - else - captureUserMedia(function() { - rtcSession.joinSession(session, extra); - }); - } - - function captureUserMedia(callback, _session) { - var constraints, video_constraints; - var session = _session || self.session; - - log(JSON.stringify(session, null, '\t')); - - if (self.dontAttachStream) - return callback(); - - if (isData(session) || (!self.isInitiator && session.oneway)) { - self.attachStream = null; - return callback(); - } - - if (session.audio && !session.video) { - constraints = { - audio: true, - video: false - }; - } else if (session.screen) { - video_constraints = { - mandatory: { - chromeMediaSource: 'screen' - }, - optional: [] - }; - constraints = { - audio: false, - video: video_constraints - }; - } else if (session.video && !session.audio) { - video_constraints = { - mandatory: { }, - optional: [] - }; - constraints = { - audio: false, - video: video_constraints - }; - } - var mediaElement = document.createElement(session.audio && !session.video ? 'audio' : 'video'); - var mediaConfig = { - video: mediaElement, - onsuccess: function(stream) { - self.attachStream = stream; - var streamid = self.token(); - - self.onstream({ - stream: stream, - streamid: streamid, - mediaElement: mediaElement, - blobURL: mediaElement.mozSrcObject || mediaElement.src, - type: 'local' - }); - - self.streams[streamid] = self._getStream({ - stream: stream, - userid: self.userid - }); - - if (callback) - callback(stream); - - mediaElement.autoplay = true; - mediaElement.controls = true; - mediaElement.muted = true; - }, - onerror: function() { - if (session.audio && !session.video) - throw 'Microphone access is denied.'; - else if (session.screen) { - if (location.protocol === 'http:') - throw ' is mandatory to capture screen.'; - else - throw 'Multi-capturing of screen is not allowed. Capturing process is denied. Are you enabled flag: "Enable screen capture support in getUserMedia"?'; - } else - throw 'Webcam access is denied.'; - } - }; - - if (constraints) - mediaConfig.constraints = constraints; - - return getUserMedia(mediaConfig); - } - - this.captureUserMedia = captureUserMedia; - - this.leave = this.eject = function(userid) { - rtcSession.leave(userid); - - self.attachStream.stop(); - currentUserMediaRequest.streams = []; - }; - - this.close = function() { - self.autoCloseEntireSession = true; - rtcSession.leave(); - }; - - this.addStream = function(session, socket) { - captureUserMedia(function(stream) { - rtcSession.addStream({ - stream: stream, - renegotiate: session, - socket: socket - }); - }, session); - }; - - Defaulter(self); - }; - - function RTCMultiSession(root) { - var config = root.config; - var session = root.session; - - var self = { }; - var socketObjects = { }; - var sockets = []; - - self.userid = root.userid = root.userid || root.token(); - self.sessionid = root.channel; - - var channels = '--', - isbroadcaster, - isAcceptNewSession = true, - RTCDataChannels = []; - - function newPrivateSocket(_config) { - var socketConfig = { - channel: _config.channel, - onmessage: socketResponse, - onopen: function() { - if (isofferer && !peer) - initPeer(); - - _config.socketIndex = socket.index = sockets.length; - socketObjects[socketConfig.channel] = socket; - sockets[_config.socketIndex] = socket; - } - }; - - socketConfig.callback = function(_socket) { - socket = _socket; - socketConfig.onopen(); - }; - - var socket = root.openSignalingChannel(socketConfig), - isofferer = _config.isofferer, - peer, - mediaElement; - - var peerConfig = { - onopen: onChannelOpened, - onICE: function(candidate) { - socket && socket.send({ - userid: self.userid, - candidate: { - sdpMLineIndex: candidate.sdpMLineIndex, - candidate: JSON.stringify(candidate.candidate) - } - }); - }, - onmessage: function(event) { - config.onmessage({ - data: event.data, - userid: _config.userid, - extra: _config.extra - }); - }, - onstream: function(stream) { - mediaElement = document.createElement(session.audio && !session.video ? 'audio' : 'video'); - mediaElement[moz ? 'mozSrcObject' : 'src'] = moz ? stream : window.webkitURL.createObjectURL(stream); - mediaElement.autoplay = true; - mediaElement.controls = true; - mediaElement.play(); - - _config.stream = stream; - if (session.audio && !session.video) - mediaElement.addEventListener('play', function() { - setTimeout(function() { - mediaElement.muted = false; - mediaElement.volume = 1; - afterRemoteStreamStartedFlowing(); - }, 3000); - }, false); - else - afterRemoteStreamStartedFlowing(); - }, - - onclose: function(e) { - e.extra = _config.extra; - e.userid = _config.userid; - root.onclose(e); - }, - onerror: function(e) { - e.extra = _config.extra; - e.userid = _config.userid; - root.onerror(e); - }, - - attachStream: root.attachStream, - iceServers: root.iceServers, - bandwidth: root.bandwidth - }; - - function initPeer(offerSDP) { - if (!offerSDP) - peerConfig.onOfferSDP = function(sdp) { - sendsdp({ - sdp: sdp, - socket: socket - }); - }; - else { - peerConfig.offerSDP = offerSDP; - peerConfig.onAnswerSDP = function(sdp) { - sendsdp({ - sdp: sdp, - socket: socket - }); - }; - } - - if (!session.data) - peerConfig.onmessage = null; - peerConfig.session = session; - peer = new RTCPeerConnection(peerConfig); - } - - function afterRemoteStreamStartedFlowing() { - _config.stream.onended = function() { - root.onstreamended(streamedObject); - }; - - var streamid = root.token(); - var streamedObject = { - mediaElement: mediaElement, - - stream: _config.stream, - streamid: streamid, - session: session, - - blobURL: mediaElement.mozSrcObject || mediaElement.src, - type: 'remote', - - extra: _config.extra, - userid: _config.userid - }; - root.onstream(streamedObject); - - // connection.streams['stream-id'].mute({audio:true}) - root.streams[streamid] = root._getStream({ - stream: _config.stream, - userid: _config.userid, - socket: socket - }); - - onSessionOpened(); - } - - function onChannelOpened(channel) { - RTCDataChannels[RTCDataChannels.length] = _config.channel = channel; - - root.onopen({ - extra: _config.extra, - userid: _config.userid - }); - - // connection.channels['user-id'].send(data); - root.channels[_config.userid] = { - channel: _config.channel, - send: function(data) { - root.send(data, this.channel); - } - }; - - if (isData(session)) onSessionOpened(); - } - - function updateSocket() { - if (socket.userid == _config.userid) - return; - - socket.userid = _config.userid; - sockets[_config.socketIndex] = socket; - - // connection.peers['user-id'].addStream({audio:true}) - root.peers[_config.userid] = { - socket: socket, - peer: peer, - userid: _config.userid, - addStream: function(session) { - root.addStream(session, this.socket); - } - }; - } - - function onSessionOpened() { - // original conferencing infrastructure! - if (!session.oneway && !session.broadcast && isbroadcaster && channels.split('--').length > 3) - defaultSocket.send({ - newParticipant: socket.channel, - userid: self.userid, - extra: _config.extra || { } - }); - } - - function socketResponse(response) { - if (response.userid == self.userid) - return; - - if (response.sdp) { - _config.userid = response.userid; - _config.extra = response.extra; - _config.renegotiate = response.renegotiate; - - // to make sure user-id for socket object is set - // even if one-way streaming - updateSocket(); - - sdpInvoker(response.sdp); - } - - if (response.candidate) { - peer && peer.addICE({ - sdpMLineIndex: response.candidate.sdpMLineIndex, - candidate: JSON.parse(response.candidate.candidate) - }); - } - - if (response.mute || response.unmute) { - log(response); - } - - if (response.left) { - if (peer && peer.connection) { - peer.connection.close(); - peer.connection = null; - } - - if (response.closeEntireSession) - clearSession(); - else if (socket) { - socket.send({ - left: true, - extra: root.extra, - userid: self.userid - }); - - if (sockets[_config.socketIndex]) - delete sockets[_config.socketIndex]; - if (socketObjects[socket.channel]) - delete socketObjects[socket.channel]; - - socket = null; - } - - root.onleave({ - userid: response.userid, - extra: response.extra - }); - } - - if (response.playRoleOfBroadcaster) - setTimeout(function() { - root.dontAttachStream = true; - self.userid = response.userid; - root.open({ - extra: root.extra - }); - sockets = sockets.swap(); - root.dontAttachStream = false; - }, 600); - - if (response.suggestRenegotiation) { - renegotiate = response.renegotiate; - if (isData(renegotiate)) - createOffer(); - else - root.captureUserMedia(function(stream) { - peer.connection.addStream(stream); - createOffer(); - }, renegotiate); - - function createOffer() { - peer.recreateOffer(renegotiate, function(sdp) { - sendsdp({ - sdp: sdp, - socket: socket, - renegotiate: response.renegotiate - }); - }); - } - } - } - - function sdpInvoker(sdp) { - log(sdp.sdp); - - if (isofferer) - return peer.addAnswerSDP(sdp); - if (!_config.renegotiate) - return initPeer(sdp); - - session = root.session = _config.renegotiate; - if (session.oneway || isData(session)) { - createAnswer(); - } else { - if (_config.capturing) - return; - _config.capturing = true; - - root.captureUserMedia(function(stream) { - _config.capturing = false; - peer.connection.addStream(stream); - createAnswer(); - }, _config.renegotiate); - } - - delete _config.renegotiate; - - function createAnswer() { - peer.recreateAnswer(sdp, session, function(_sdp) { - sendsdp({ - sdp: _sdp, - socket: socket - }); - }); - } - } - } - - function sendsdp(e) { - e.socket.send({ - userid: self.userid, - sdp: e.sdp, - extra: root.extra, - renegotiate: e.renegotiate ? e.renegotiate : false - }); - } - - function onNewParticipant(channel, extra) { - if (!channel || channels.indexOf(channel) != -1 || channel == self.userid) - return; - channels += channel + '--'; - - var new_channel = root.token(); - newPrivateSocket({ - channel: new_channel, - closeSocket: true, - extra: extra || { } - }); - - defaultSocket.send({ - participant: true, - userid: self.userid, - targetUser: channel, - channel: new_channel, - extra: root.extra - }); - } - - function clearSession(channel) { - var alert = { - left: true, - extra: root.extra, - userid: self.userid - }; - - if (isbroadcaster) { - if (root.autoCloseEntireSession) - alert.closeEntireSession = true; - else - sockets[0] && sockets[0].send({ - playRoleOfBroadcaster: true, - userid: self.userid - }); - } - - if (!channel) { - var length = sockets.length; - for (var i = 0; i < length; i++) { - socket = sockets[i]; - if (socket) { - socket.send(alert); - if (socketObjects[socket.channel]) - delete socketObjects[socket.channel]; - delete sockets[i]; - } - } - } - - // eject a specific user! - if (channel) { - socket = socketObjects[channel]; - if (socket) { - socket.send(alert); - if (sockets[socket.index]) - delete sockets[socket.index]; - delete socketObjects[channel]; - } - } - - sockets = sockets.swap(); - } - - window.onbeforeunload = function() { - clearSession(); - }; - - window.onkeyup = function(e) { - if (e.keyCode == 116) - clearSession(); - }; - - var anchors = document.querySelectorAll('a'), - length = anchors.length; - for (var i = 0; i < length; i++) { - var a = anchors[i]; - if (a.href.indexOf('#') !== 0 && a.getAttribute('target') != '_blank') - a.onclick = function() { - clearSession(); - }; - } - - var that = this, - defaultSocket = root.openSignalingChannel({ - onmessage: function(response) { - if (response.userid == self.userid) - return; - if (isAcceptNewSession && response.sessionid && response.userid) { - session = root.session = response.session; - config.onNewSession(response); - } - - if (response.newParticipant && self.joinedARoom && self.broadcasterid === response.userid) - onNewParticipant(response.newParticipant, response.extra); - if (response.userid && response.targetUser == self.userid && response.participant && channels.indexOf(response.userid) == -1) { - channels += response.userid + '--'; - newPrivateSocket({ - isofferer: true, - channel: response.channel || response.userid, - closeSocket: true, - extra: response.extra - }); - } - }, - callback: function(socket) { - defaultSocket = socket; - } - }); - - this.initSession = function() { - isbroadcaster = true; - isAcceptNewSession = false; - (function transmit() { - defaultSocket && defaultSocket.send({ - sessionid: self.sessionid, - userid: self.userid, - session: session, - extra: root.extra - }); - - if (!root.transmitRoomOnce && !that.leaving) - setTimeout(transmit, root.interval || 3000); - })(); - }; - - this.joinSession = function(_config) { - _config = _config || { }; - - session = _config.session; - - self.joinedARoom = true; - - if (_config.sessionid) - self.sessionid = _config.sessionid; - - isAcceptNewSession = false; - - newPrivateSocket({ - channel: self.userid, - extra: _config.extra - }); - - defaultSocket.send({ - participant: true, - userid: self.userid, - targetUser: _config.userid, - extra: root.extra - }); - - self.broadcasterid = _config.userid; - }; - - this.send = function(message, _channel) { - var _channels = RTCDataChannels, - data, length = _channels.length; - if (!length) - return; - - if (moz && message.file) - data = message.file; - else - data = JSON.stringify(message); - - if (_channel) - _channel.send(data); - else - for (var i = 0; i < length; i++) - _channels[i].send(data); - }; - - this.leave = function(userid) { - clearSession(userid); - - if (!userid) { - self.joinedARoom = isbroadcaster = false; - isAcceptNewSession = true; - } - }; - - this.addStream = function(e) { - session = e.renegotiate; - - if (e.socket) - addStream(e.socket); - else - for (var i = 0; i < sockets.length; i++) - addStream(sockets[i]); - - function addStream(socket) { - peer = root.peers[socket.userid]; - - if (!peer) - throw 'No such peer exists.'; - - peer = peer.peer; - - // if offerer; renegotiate - if (peer.connection.localDescription.type == 'offer') { - if (session.audio || session.video) - peer.connection.addStream(e.stream); - - peer.recreateOffer(session, function(sdp) { - sendsdp({ - sdp: sdp, - socket: socket, - renegotiate: session - }); - }); - } else { - // otherwise; suggest other user to play role of renegotiator - socket.send({ - userid: self.userid, - renegotiate: session, - suggestRenegotiation: true - }); - } - } - }; - } - - var FileSender = { - send: function(config) { - var channel = config.channel; - var file = config.file; - var _channel = config._channel; - - if (moz) { - channel.send({ - fileName: file.name, - type: 'file' - }, _channel); - - channel.send({ - file: file - }, _channel); - - config.onFileSent(file); - } - - if (!moz) { - var reader = new window.FileReader(); - reader.readAsDataURL(file); - reader.onload = onReadAsDataURL; - } - - var packetSize = 1000, - textToTransfer = '', - numberOfPackets = 0, - packets = 0; - - function onReadAsDataURL(event, text) { - var data = { - type: 'file' - }; - - if (event) { - text = event.target.result; - numberOfPackets = packets = data.packets = parseInt(text.length / packetSize); - } - - config.onFileProgress({ - remaining: packets--, - length: numberOfPackets, - sent: numberOfPackets - packets - }); - - if (text.length > packetSize) - data.message = text.slice(0, packetSize); - else { - data.message = text; - data.last = true; - data.name = file.name; - - config.onFileSent(file); - } - - channel.send(data, _channel); - - textToTransfer = text.slice(data.message.length); - - if (textToTransfer.length) - setTimeout(function() { - onReadAsDataURL(null, textToTransfer); - }, 500); - } - } - }; - - function FileReceiver() { - var content = [], - fileName = '', - packets = 0, - numberOfPackets = 0; - - this.receive = function(e) { - var data = e.data; - var connection = e.connection; - - if (moz) { - if (data.fileName) - fileName = data.fileName; - - if (data.size) { - var reader = new window.FileReader(); - reader.readAsDataURL(data); - reader.onload = function(event) { - FileSaver.SaveToDisk({ - fileURL: event.target.result, - fileName: fileName - }); - connection.onFileReceived(fileName); - }; - } - } - - if (!moz) { - if (data.packets) - numberOfPackets = packets = parseInt(data.packets); - - if (connection.onFileProgress) - connection.onFileProgress({ - remaining: packets--, - length: numberOfPackets, - received: numberOfPackets - packets - }); - - content.push(data.message); - - if (data.last) { - FileSaver.SaveToDisk({ - fileURL: content.join(''), - fileName: data.name - }); - connection.onFileReceived(data.name); - content = []; - } - } - }; - } - - var TextSender = { - send: function(config) { - var channel = config.channel, - initialText = config.text, - packetSize = 1000, - textToTransfer = '', - _channel = config._channel; - - if (typeof initialText !== 'string') - initialText = JSON.stringify(initialText); - - if (moz || initialText.length <= packetSize) - channel.send(config.text, _channel); - else - sendText(initialText); - - function sendText(textMessage, text) { - var data = { - type: 'text' - }; - - 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; - } - - channel.send(data, _channel); - - textToTransfer = text.slice(data.message.length); - - if (textToTransfer.length) - setTimeout(function() { - sendText(null, textToTransfer); - }, 500); - } - } - }; - - function TextReceiver() { - var content = []; - - function receive(e) { - data = e.data; - connection = e.connection; - - content.push(data.message); - if (data.last) { - connection.onmessage(content.join('')); - content = []; - } - } - - return { - receive: receive - }; - } - - var FileSaver = { - SaveToDisk: function(e) { - var save = document.createElement('a'); - save.href = e.fileURL; - save.target = '_blank'; - save.download = e.fileName || e.fileURL; - - var evt = document.createEvent('MouseEvents'); - evt.initMouseEvent('click', true, true, window, 1, 0, 0, 0, 0, false, false, false, false, 0, null); - - save.dispatchEvent(evt); - (window.URL || window.webkitURL).revokeObjectURL(save.href); - } - }; - - window.MediaStream = window.MediaStream || window.webkitMediaStream; - - window.moz = !!navigator.mozGetUserMedia; - var RTCPeerConnection = function(options) { - var w = window, - PeerConnection = w.mozRTCPeerConnection || w.webkitRTCPeerConnection, - SessionDescription = w.mozRTCSessionDescription || w.RTCSessionDescription, - IceCandidate = w.mozRTCIceCandidate || w.RTCIceCandidate; - - var STUN = { - url: !moz ? 'stun:stun.l.google.com:19302' : 'stun:23.21.150.121' - }; - - var TURN = { - url: 'turn:homeo@turn.bistri.com:80', - credential: 'homeo' - }; - - var iceServers = { - iceServers: options.iceServers || [STUN] - }; - - if (!moz && !options.iceServers) { - if (parseInt(navigator.userAgent.match( /Chrom(e|ium)\/([0-9]+)\./ )[2]) >= 28) - TURN = { - url: 'turn:turn.bistri.com:80', - credential: 'homeo', - username: 'homeo' - }; - - iceServers.iceServers = [STUN, TURN]; - } - - var optional = { - optional: [] - }; - - if (!moz) { - optional.optional = [{ - DtlsSrtpKeyAgreement: true - }]; - - if (options.onmessage) - optional.optional = [{ - RtpDataChannels: true - }]; - } - - var peer = new PeerConnection(iceServers, optional); - - openOffererChannel(); - - peer.onicecandidate = function(event) { - if (event && event.candidate && !options.renegotiate) - options.onICE(event.candidate); - }; - - if (options.attachStream) - peer.addStream(options.attachStream); - peer.onaddstream = function(event) { - log('on:add:stream', event.stream); - - if (!event || !options.onstream) - return; - options.onstream(event.stream); - options.renegotiate = false; - }; - - var constraints; - - function setConstraints() { - var session = options.session; - constraints = options.constraints || { - optional: [], - mandatory: { - OfferToReceiveAudio: !!session.audio, - OfferToReceiveVideo: !!session.video || !!session.screen - } - }; - } - - setConstraints(); - - function createOffer() { - if (!options.onOfferSDP) - return; - - peer.createOffer(function(sessionDescription) { - sessionDescription.sdp = setBandwidth(sessionDescription.sdp); - peer.setLocalDescription(sessionDescription); - options.onOfferSDP(sessionDescription); - }, onSdpError, constraints); - } - - function createAnswer() { - if (!options.onAnswerSDP) - return; - - options.offerSDP = new SessionDescription(options.offerSDP); - peer.setRemoteDescription(options.offerSDP, onSdpSuccess, onSdpError); - - peer.createAnswer(function(sessionDescription) { - sessionDescription.sdp = setBandwidth(sessionDescription.sdp); - peer.setLocalDescription(sessionDescription); - options.onAnswerSDP(sessionDescription); - }, onSdpError, constraints); - } - - if ((options.onmessage && !moz) || !options.onmessage) { - createOffer(); - createAnswer(); - } - - var bandwidth = options.bandwidth; - - function setBandwidth(sdp) { - // Firefox has no support of "b=AS" - if (moz) return sdp; - - // remove existing bandwidth lines - sdp = sdp.replace( /b=AS([^\r\n]+\r\n)/g , ''); - - sdp = sdp.replace( /a=mid:audio\r\n/g , 'a=mid:audio\r\nb=AS:' + (bandwidth.audio || 50) + '\r\n'); - sdp = sdp.replace( /a=mid:video\r\n/g , 'a=mid:video\r\nb=AS:' + (bandwidth.video || 256) + '\r\n'); - sdp = sdp.replace( /a=mid:data\r\n/g , 'a=mid:data\r\nb=AS:' + (bandwidth.data || 1638400) + '\r\n'); - - return sdp; - } - - var channel; - - function openOffererChannel() { - if (!options.onmessage || (moz && !options.onOfferSDP)) - return; - - _openOffererChannel(); - - if (moz && !options.attachStream) { - navigator.mozGetUserMedia({ - audio: true, - fake: true - }, function(stream) { - peer.addStream(stream); - createOffer(); - }, useless); - } - } - - function _openOffererChannel() { - channel = peer.createDataChannel(options.channel || 'RTCDataChannel', moz ? { } : { - reliable: false - }); - - if (moz) - channel.binaryType = 'blob'; - setChannelEvents(); - } - - function setChannelEvents() { - channel.onmessage = options.onmessage; - channel.onopen = function() { - options.onopen(channel); - }; - channel.onclose = options.onclose; - channel.onerror = options.onerror; - } - - if (options.onAnswerSDP && moz && options.onmessage) - openAnswererChannel(); - - function openAnswererChannel() { - peer.ondatachannel = function(event) { - channel = event.channel; - channel.binaryType = 'blob'; - setChannelEvents(); - }; - - if (moz && !options.attachStream) { - navigator.mozGetUserMedia({ - audio: true, - fake: true - }, function(stream) { - peer.addStream(stream); - createAnswer(); - }, useless); - } - } - - function useless() { - } - - function onSdpSuccess() { - } - - function onSdpError(e) { - console.error('sdp error:', e.name, e.message); - } - - return { - connection: peer, - addAnswerSDP: function(sdp) { - peer.setRemoteDescription(new SessionDescription(sdp), onSdpSuccess, onSdpError); - }, - addICE: function(candidate) { - peer.addIceCandidate(new IceCandidate({ - sdpMLineIndex: candidate.sdpMLineIndex, - candidate: candidate.candidate - })); - }, - recreateAnswer: function(sdp, session, callback) { - options.renegotiate = true; - - options.session = session; - setConstraints(); - - options.onAnswerSDP = callback; - options.offerSDP = sdp; - createAnswer(); - }, - recreateOffer: function(session, callback) { - options.renegotiate = true; - - options.session = session; - setConstraints(); - - options.onOfferSDP = callback; - createOffer(); - } - }; - }; - - var video_constraints = { - mandatory: { }, - optional: [] - }; - - /* by @FreCap pull request #41 */ - var currentUserMediaRequest = { - streams: [], - mutex: false, - queueRequests: [] - }; - - function getUserMedia(options) { - if (currentUserMediaRequest.mutex === true) { - currentUserMediaRequest.queueRequests.push(options); - return; - } - currentUserMediaRequest.mutex = true; - var n = navigator, - resourcesNeeded = options.constraints || { - audio: true, - video: video_constraints - }; - - // easy way to match - var idInstance = JSON.stringify(resourcesNeeded); - - function streaming(stream) { - var video = options.video; - if (video) { - video[moz ? 'mozSrcObject' : 'src'] = moz ? stream : window.webkitURL.createObjectURL(stream); - video.play(); - } - - options.onsuccess(stream); - currentUserMediaRequest.streams[idInstance] = stream; - currentUserMediaRequest.mutex = false; - if (currentUserMediaRequest.queueRequests.length) - getUserMedia(currentUserMediaRequest.queueRequests.shift()); - } - - if (currentUserMediaRequest.streams[idInstance]) - streaming(currentUserMediaRequest.streams[idInstance]); - else { - n.getMedia = n.webkitGetUserMedia || n.mozGetUserMedia; - n.getMedia(resourcesNeeded, streaming, options.onerror || function(e) { - console.error(e); - }); - } - } - - function isData(session) { - return !session.audio && !session.video && !session.screen && session.data; - } - - Array.prototype.swap = function() { - var swapped = [], - arr = this, - length = arr.length; - for (var i = 0; i < length; i++) - if (arr[i] && arr[i] !== true) - swapped[swapped.length] = arr[i]; - return swapped; - }; - - function log(a, b, c, d, e, f) { - if (f) - console.log(a, b, c, d, e, f); - else if (e) - console.log(a, b, c, d, e); - else if (d) - console.log(a, b, c, d); - else if (c) - console.log(a, b, c); - else if (b) - console.log(a, b); - else if (a) - console.log(a); - } - - function Defaulter(self) { - self.onmessage = function(e) { - log(e.userid, 'posted', e.data); - }; - - self.onopen = function(e) { - log('Data connection is opened between you and', e.userid); - }; - - self.onerror = function(e) { - console.error('Error in data connection between you and', e.userid, e); - }; - - self.onclose = function(e) { - console.warn('Data connection between you and', e.userid, 'is closed.', e); - }; - - self.onFileReceived = function(fileName) { - log('File <', fileName, '> received successfully.'); - }; - - self.onFileSent = function(file) { - log('File <', file.name, '> sent successfully.'); - }; - - self.onFileProgress = function(packets) { - log('<', packets.remaining, '> items remaining.'); - }; - - self.onstream = function(stream) { - log('stream:', stream); - }; - - self.onleave = function(e) { - log(e.userid, 'left!'); - }; - - self.onstreamended = function(e) { - log('onstreamended', e); - }; - - self.peers = { }; - self.streams = { }; - self.channels = { }; - self.extra = { }; - - self.session = { - audio: true, - video: true, - data: true - }; - - self.bandwidth = { - audio: 50, - video: 256, - data: 1638400 - }; - - self._getStream = function(e) { - return { - stream: e.stream, - userid: e.userid, - socket: e.socket, - mute: function(session) { - this._private(session, true); - }, - unmute: function(session) { - this._private(session, false); - }, - _private: function(session, enabled) { - var stream = this.stream; - - if (e.socket) - e.socket.send({ - userid: this.userid, - mute: !!enabled, - unmute: !enabled - }); - - // for local streams only - else - log('No socket to send mute/unmute notification message.'); - - session = session || { - audio: true, - video: true - }; - - if (session.audio) { - var audioTracks = stream.getAudioTracks()[0]; - if (audioTracks) - audioTracks.enabled = !enabled; - } - - if (session.video) { - var videoTracks = stream.getVideoTracks()[0]; - if (videoTracks) - videoTracks.enabled = !enabled; - } - } - }; - }; - - self.token = function() { - return (Math.random() * new Date().getTime()).toString(36).toUpperCase().replace( /\./g , '-'); - }; - } -})(); diff --git a/RTCMultiConnection/Library/RTCMultiConnection-v1.4.js b/RTCMultiConnection/Library/RTCMultiConnection-v1.4.js deleted file mode 100644 index 7987f411..00000000 --- a/RTCMultiConnection/Library/RTCMultiConnection-v1.4.js +++ /dev/null @@ -1,2329 +0,0 @@ -// Muaz Khan - www.MuazKhan.com -// MIT License - www.WebRTC-Experiment.com/licence -// Documentation - www.RTCMultiConnection.org/docs/ - -// FAQ - www.RTCMultiConnection.org/FAQ/ -// Development News - trello.com/b/8bhi1G6n/RTCMultiConnection - -// v1.4 changes log - www.RTCMultiConnection.org/changes-log/#v1.4 -// _______________________ -// RTCMultiConnection-v1.4 - -(function() { - window.RTCMultiConnection = function(channel) { - this.channel = channel || location.href.replace( /\/|:|#|%|\.|\[|\]/g , ''); - - this.open = function(_channel) { - self.joinedARoom = true; - - if (_channel) - self.channel = _channel; - - // if firebase && if session initiator - if (self.socket && self.socket.onDisconnect) - self.socket.onDisconnect().remove(); - - self.isInitiator = true; - - prepareInit(function() { - init(); - captureUserMedia(rtcSession.initSession); - }); - }; - - // check pre-opened connections - this.connect = function(_channel) { - if (_channel) - self.channel = _channel; - - prepareInit(init); - }; - - // join a session - this.join = joinSession; - - // send file/data or /text - this.send = function(data, _channel) { - if (!data) - throw 'No file, data or text message to share.'; - - if (!!data.forEach) { - for (var i = 0; i < data.length; i++) { - self.send(data[i], _channel); - } - return; - } - - if (data.size) { - FileSender.send({ - file: data, - channel: rtcSession, - _channel: _channel, - root: self - }); - } else - TextSender.send({ - text: data, - channel: rtcSession, - _channel: _channel, - preferSCTP: self.preferSCTP - }); - }; - - var self = this, - rtcSession, fileReceiver, textReceiver; - - // verify openSignalingChannel method's presence - - function prepareInit(callback) { - if (!self.openSignalingChannel) { - if (typeof self.transmitRoomOnce == 'undefined') - self.transmitRoomOnce = true; - - // for custom socket.io over node.js implementation - visit - https://github.com/muaz-khan/WebRTC-Experiment/blob/master/socketio-over-nodejs - self.openSignalingChannel = function(config) { - var channel = config.channel || self.channel; - var firebase = new Firebase('https://' + (self.firebase || 'chat') + '.firebaseIO.com/' + channel); - firebase.channel = channel; - firebase.on('child_added', function(data) { - config.onmessage(data.val()); - }); - - firebase.send = function(data) { - this.push(data); - }; - - if (!self.socket) - self.socket = firebase; - - if (channel != self.channel || (self.isInitiator && channel == self.channel)) - firebase.onDisconnect().remove(); - - if (config.onopen) - setTimeout(config.onopen, 1); - - return firebase; - }; - - if (!window.Firebase) { - loadScript('https://cdn.firebase.com/v0/firebase.js', callback); - } else - callback(); - } else - callback(); - } - - // set config passed over RTCMultiSession - - function init() { - if (self.config) - return; - - self.config = { - onNewSession: function(session) { - if (!rtcSession) { - self._session = session; - return; - } - - if (self.onNewSession) - return self.onNewSession(session); - - if (self.joinedARoom) return; - - self.joinedARoom = true; - - return joinSession(session); - }, - onmessage: function(e) { - if (!e.data.size) - e.data = JSON.parse(e.data); - - if (e.data.type === 'text') - textReceiver.receive(e.data, self.onmessage, e.userid, e.extra); - - else if (e.data.maxChunks) - fileReceiver.receive(e.data); - else - self.onmessage(e); - } - }; - rtcSession = new RTCMultiSession(self); - fileReceiver = new FileReceiver(self); - textReceiver = new TextReceiver(); - - if (self._session) - self.config.onNewSession(self._session); - } - - function joinSession(session) { - if (!session || !session.userid || !session.sessionid) - throw 'invalid data passed.'; - - self.session = session.session; - - extra = self.extra || session.extra || { }; - - if (session.oneway || session.data) - rtcSession.joinSession(session, extra); - else - captureUserMedia(function() { - rtcSession.joinSession(session, extra); - }); - } - - // capture user's media resources - - function captureUserMedia(callback, _session) { - var session = _session || self.session; - - if (self.dontAttachStream) - return callback(); - - if (isData(session) || (!self.isInitiator && session.oneway)) { - self.attachStreams = []; - return callback(); - } - - var constraints = { - audio: !!session.audio, - video: !!session.video - }; - var screen_constraints = { - audio: false, - video: { - mandatory: { - chromeMediaSource: 'screen' - }, - optional: [] - } - }; - - if (session.screen) { - _captureUserMedia(screen_constraints, constraints.audio || constraints.video ? function() { - _captureUserMedia(constraints, callback); - } : callback); - } else _captureUserMedia(constraints, callback, session.audio && !session.video); - - function _captureUserMedia(forcedConstraints, forcedCallback, isRemoveVideoTracks) { - var mediaConfig = { - onsuccess: function(stream, returnBack) { - if (returnBack) return forcedCallback && forcedCallback(stream); - - if (isRemoveVideoTracks && !moz) { - stream = new window.webkitMediaStream(stream.getAudioTracks()); - } - - var mediaElement = getMediaElement(stream, session); - mediaElement.muted = true; - - stream.onended = function() { - if (self.onstreamended) - self.onstreamended(streamedObject); - else if (mediaElement.parentNode) - mediaElement.parentNode.removeChild(mediaElement); - }; - - var streamid = getRandomString(); - - stream.streamid = streamid; - - var streamedObject = { - stream: stream, - streamid: streamid, - mediaElement: mediaElement, - blobURL: mediaElement.mozSrcObject || mediaElement.src, - type: 'local', - userid: self.userid || 'self', - extra: self.extra - }; - - var sObject = { - stream: stream, - userid: self.userid || 'self', - streamid: streamid, - type: 'local', - streamObject: streamedObject, - mediaElement: mediaElement - }; - - self.attachStreams.push(stream); - self.__attachStreams.push(sObject); - - self.streams[streamid] = self._getStream(sObject); - - self.onstream(streamedObject); - if (forcedCallback) forcedCallback(stream); - }, - onerror: function() { - var error; - - if (session.audio && !session.video) - error = 'Microphone access is denied.'; - else if (session.screen) { - if (location.protocol === 'http:') - error = ' is mandatory to capture screen.'; - else - error = 'Multi-capturing of screen is not allowed. Capturing process is denied. Are you enabled flag: "Enable screen capture support in getUserMedia"?'; - } else - error = 'Webcam access is denied.'; - - if (!self.onMediaError) throw error; - - self.onMediaError(error); - }, - mediaConstraints: self.mediaConstraints || { } - }; - - mediaConfig.constraints = forcedConstraints || constraints; - mediaConfig.media = self.media; - getUserMedia(mediaConfig); - } - } - - this.captureUserMedia = captureUserMedia; - - // eject a user; or leave the session - this.leave = this.eject = function(userid) { - rtcSession.leave(userid); - - if (!userid) { - var streams = self.attachStreams; - for (var i = 0; i < streams.length; i++) { - stopTracks(streams[i]); - } - currentUserMediaRequest.streams = []; - self.attachStreams = []; - } - - // if firebase; remove data from firebase servers - if (self.isInitiator && !!self.socket && !!self.socket.remove) { - self.socket.remove(); - } - }; - - // close entire session - this.close = function() { - self.autoCloseEntireSession = true; - rtcSession.leave(); - }; - - // renegotiate new media stream - this.addStream = function(session, socket) { - captureUserMedia(function(stream) { - rtcSession.addStream({ - stream: stream, - renegotiate: session, - socket: socket - }); - }, session); - }; - - // detach pre-attached streams - this.removeStream = function(streamid) { - if (!this.streams[streamid]) return console.warn('No such stream exists. Stream-id:', streamid); - this.detachStreams.push(streamid); - }; - - // set RTCMultiConnection defaults on constructor invocation - this.setDefaults(); - - this.__attachStreams = []; - }; - - function RTCMultiSession(root) { - var config = root.config; - var session = root.session; - - var self = { }; - var socketObjects = { }; - var sockets = []; - - self.userid = root.userid = root.userid || root.token(); - self.sessionid = root.channel; - - var participants = { }, - isbroadcaster, - isAcceptNewSession = true; - - function newPrivateSocket(_config) { - var socketConfig = { - channel: _config.channel, - onmessage: socketResponse, - onopen: function() { - if (isofferer && !peer) - initPeer(); - - _config.socketIndex = socket.index = sockets.length; - socketObjects[socketConfig.channel] = socket; - sockets[_config.socketIndex] = socket; - - for (var i = 0; i < root.__attachStreams.length; i++) { - var streamid = root.__attachStreams[i].streamid; - if (root.streams[streamid]) root.streams[streamid].socket = socket; - } - root.__attachStreams = []; - } - }; - - socketConfig.callback = function(_socket) { - socket = _socket; - socketConfig.onopen(); - }; - - var socket = root.openSignalingChannel(socketConfig), - isofferer = _config.isofferer, - peer; - - var peerConfig = { - onopen: onChannelOpened, - onICE: function(candidate) { - if (!root.candidates) throw 'ICE candidates are mandatory.'; - if (!root.candidates.host && candidate.candidate.indexOf('typ host') != -1) return; - if (!root.candidates.relay && candidate.candidate.indexOf('relay') != -1) return; - if (!root.candidates.reflexive && candidate.candidate.indexOf('srflx') != -1) return; - - log(candidate.candidate); - socket && socket.send({ - userid: self.userid, - candidate: { - sdpMLineIndex: candidate.sdpMLineIndex, - candidate: JSON.stringify(candidate.candidate) - } - }); - }, - onmessage: function(data) { - config.onmessage({ - data: data, - userid: _config.userid, - extra: _config.extra - }); - }, - onstream: function(stream) { - if (_config.streaminfo) { - if (_config.streaminfo[0]) { - stream.streamid = _config.streaminfo[0].streamid; - delete _config.streaminfo[0]; - _config.streaminfo = swap(_config.streaminfo); - } - } - - var mediaElement = getMediaElement(stream, session); - - _config.stream = stream; - if (mediaElement.tagName.toLowerCase() == 'audio') - mediaElement.addEventListener('play', function() { - setTimeout(function() { - mediaElement.muted = false; - afterRemoteStreamStartedFlowing(mediaElement); - }, 3000); - }, false); - else - waitUntilRemoteStreamStartsFlowing(mediaElement); - }, - - onclose: function(e) { - e.extra = _config.extra; - e.userid = _config.userid; - root.onclose(e); - - // suggested in #71 by "efaj" - if (root.channels[e.userid]) - delete root.channels[e.userid]; - }, - onerror: function(e) { - e.extra = _config.extra; - e.userid = _config.userid; - root.onerror(e); - }, - - attachStreams: root.attachStreams, - iceServers: root.iceServers, - bandwidth: root.bandwidth, - sdpConstraints: root.sdpConstraints || { }, - disableDtlsSrtp: root.disableDtlsSrtp, - preferSCTP: !!root.preferSCTP - }; - - function initPeer(offerSDP) { - if (!offerSDP) - peerConfig.onOfferSDP = function(sdp, streaminfo) { - sendsdp({ - sdp: sdp, - socket: socket, - streaminfo: streaminfo - }); - }; - else { - peerConfig.offerSDP = offerSDP; - peerConfig.onAnswerSDP = function(sdp, streaminfo) { - sendsdp({ - sdp: sdp, - socket: socket, - streaminfo: streaminfo - }); - }; - } - - if (!session.data) peerConfig.onmessage = null; - peerConfig.session = session; - peer = new RTCPeerConnection(peerConfig); - } - - function waitUntilRemoteStreamStartsFlowing(mediaElement) { - if (!(mediaElement.readyState <= HTMLMediaElement.HAVE_CURRENT_DATA - || mediaElement.paused || mediaElement.currentTime <= 0)) { - afterRemoteStreamStartedFlowing(mediaElement); - } else - setTimeout(function() { - waitUntilRemoteStreamStartsFlowing(mediaElement); - }, 50); - } - - function afterRemoteStreamStartedFlowing(mediaElement) { - var stream = _config.stream; - stream.onended = function() { - if (root.onstreamended) - root.onstreamended(streamedObject); - else if (mediaElement.parentNode) - mediaElement.parentNode.removeChild(mediaElement); - }; - - // var streamid = getRandomString(); - var streamedObject = { - mediaElement: mediaElement, - - stream: stream, - streamid: stream.streamid, - session: session, - - blobURL: mediaElement.mozSrcObject || mediaElement.src, - type: 'remote', - - extra: _config.extra, - userid: _config.userid - }; - - // connection.streams['stream-id'].mute({audio:true}) - root.streams[stream.streamid] = root._getStream({ - stream: stream, - userid: _config.userid, - streamid: stream.streamid, - socket: socket, - type: 'remote', - streamObject: streamedObject, - mediaElement: mediaElement - }); - - root.onstream(streamedObject); - - onSessionOpened(); - - // mic/speaker activity detection - // voiceActivityDetection(peer.connection); - } - - function onChannelOpened(channel) { - _config.channel = channel; - - // connection.channels['user-id'].send(data); - root.channels[_config.userid] = { - channel: _config.channel, - send: function(data) { - root.send(data, this.channel); - } - }; - - root.onopen({ - extra: _config.extra, - userid: _config.userid - }); - - // fetch files from file-queue - for (var q in root.fileQueue) { - root.send(root.fileQueue[q], channel); - } - - if (isData(session)) onSessionOpened(); - } - - function updateSocket() { - if (socket.userid == _config.userid) - return; - - socket.userid = _config.userid; - sockets[_config.socketIndex] = socket; - - // connection.peers['user-id'].addStream({audio:true}) - root.peers[_config.userid] = { - socket: socket, - peer: peer, - userid: _config.userid, - addStream: function(session00) { - root.addStream(session00, this.socket); - } - }; - } - - function onSessionOpened() { - // admin/guest is one-to-one relationship - if (root.userType && !root.session['many-to-many']) return; - - // original conferencing infrastructure! - if (!session.oneway && !session.broadcast && isbroadcaster && getLength(participants) > 1 && getLength(participants) <= root.maxParticipantsAllowed) { - defaultSocket.send({ - newParticipant: _config.userid || socket.channel, - userid: self.userid, - extra: _config.extra || { } - }); - } - } - - function socketResponse(response) { - if (response.userid == self.userid) - return; - - if (response.sdp) { - _config.userid = response.userid; - _config.extra = response.extra; - _config.renegotiate = response.renegotiate; - _config.streaminfo = response.streaminfo; - - // to make sure user-id for socket object is set - // even if one-way streaming - updateSocket(); - - sdpInvoker(JSON.parse(response.sdp), response.labels); - } - - if (response.candidate) { - peer && peer.addICE({ - sdpMLineIndex: response.candidate.sdpMLineIndex, - candidate: JSON.parse(response.candidate.candidate) - }); - } - - if (response.mute || response.unmute) { - response.mediaElement = root.streams[response.streamid].mediaElement; - - if (response.mute && root.onmute) root.onmute(response); - if (response.unmute && root.onunmute) root.onunmute(response); - } - - if (response.stopped) { - response.mediaElement = root.streams[response.streamid].mediaElement; - - if (root.onstreamended) root.onstreamended(response); - else if (response.mediaElement.parentNode) - response.mediaElement.parentNode.removeChild(response.mediaElement); - } - - if (response.left) { - if (peer && peer.connection) { - peer.connection.close(); - peer.connection = null; - - // firefox is unable to stop remote streams - // firefox doesn't auto stop streams when peer.close() is called. - if (moz) { - var userLeft = response.userid; - for (var stream in root.streams) { - stream = root.streams[stream]; - if (stream.userid == userLeft) { - stopTracks(stream); - stream.stream.onended(stream.streamObject); - } - } - } - } - - if (response.closeEntireSession) { - root.leave(); - - } else if (socket) { - socket.send({ - left: true, - extra: root.extra, - userid: self.userid - }); - - if (sockets[_config.socketIndex]) - delete sockets[_config.socketIndex]; - if (socketObjects[socket.channel]) - delete socketObjects[socket.channel]; - - socket = null; - } - - if (participants[response.userid]) delete participants[response.userid]; - - root.onleave({ - userid: response.userid, - extra: response.extra - }); - - if (root.userType) root.busy = false; - } - - // keeping session active even if initiator leaves - if (response.playRoleOfBroadcaster) - setTimeout(function() { - root.dontAttachStream = true; - self.userid = response.userid; - root.open({ - extra: root.extra - }); - sockets = swap(sockets); - root.dontAttachStream = false; - }, 600); - - // if renegotiation process initiated by answerer - if (response.suggestRenegotiation) { - renegotiate = response.renegotiate; - - // detaching old streams - detachMediaStream(root.detachStreams, peer.connection); - - if (isData(renegotiate)) - createOffer(); - else - root.captureUserMedia(function(stream00) { - peer.connection.addStream(stream00); - createOffer(); - }, renegotiate); - - function createOffer() { - peer.recreateOffer(renegotiate, function(sdp, streaminfo) { - sendsdp({ - sdp: sdp, - socket: socket, - renegotiate: response.renegotiate, - labels: root.detachStreams, - streaminfo: streaminfo - }); - root.detachStreams = []; - }); - } - } - } - - function sdpInvoker(sdp, labels) { - log(sdp.sdp); - - if (isofferer) - return peer.addAnswerSDP(sdp); - if (!_config.renegotiate) - return initPeer(sdp); - - session = _config.renegotiate; - // detach streams - detachMediaStream(labels, peer.connection); - - if (session.oneway || isData(session)) { - createAnswer(); - } else { - if (_config.capturing) - return; - - _config.capturing = true; - - root.captureUserMedia(function(stream) { - _config.capturing = false; - - peer.connection.addStream(stream); - createAnswer(); - }, _config.renegotiate); - } - - delete _config.renegotiate; - - function createAnswer() { - peer.recreateAnswer(sdp, session, function(_sdp, streaminfo) { - sendsdp({ - sdp: _sdp, - socket: socket, - streaminfo: streaminfo - }); - }); - } - } - } - - function detachMediaStream(labels, peer) { - if (!labels) return; - for (var i = 0; i < labels.length; i++) { - var label = labels[i]; - if (root.streams[label]) { - var stream = root.streams[label].stream; - stopTracks(stream); - peer.removeStream(stream); - } - } - } - - function sendsdp(e) { - e.socket.send({ - userid: self.userid, - sdp: JSON.stringify(e.sdp), - extra: root.extra, - renegotiate: e.renegotiate ? e.renegotiate : false, - streaminfo: e.streaminfo || [], - labels: e.labels || [] - }); - } - - // sharing new user with existing participants - - function onNewParticipant(channel, extra) { - if (!channel || !!participants[channel] || channel == self.userid) - return; - - participants[channel] = channel; - - var new_channel = root.token(); - newPrivateSocket({ - channel: new_channel, - extra: extra || { } - }); - - defaultSocket.send({ - participant: true, - userid: self.userid, - targetUser: channel, - channel: new_channel, - extra: root.extra - }); - } - - // if a user leaves - - function clearSession(channel) { - var alert = { - left: true, - extra: root.extra, - userid: self.userid, - sessionid: self.sessionid - }; - - if (isbroadcaster) { - if (root.autoCloseEntireSession) { - alert.closeEntireSession = true; - } else if (sockets[0]) { - sockets[0].send({ - playRoleOfBroadcaster: true, - userid: self.userid - }); - } - } - - if (!channel) { - var length = sockets.length; - for (var i = 0; i < length; i++) { - socket = sockets[i]; - if (socket) { - socket.send(alert); - if (socketObjects[socket.channel]) - delete socketObjects[socket.channel]; - delete sockets[i]; - } - } - } - - // eject a specific user! - if (channel) { - socket = socketObjects[channel]; - if (socket) { - socket.send(alert); - if (sockets[socket.index]) - delete sockets[socket.index]; - delete socketObjects[channel]; - } - } - - sockets = swap(sockets); - } - - window.onbeforeunload = function() { - clearSession(); - }; - - window.onkeyup = function(e) { - if (e.keyCode == 116) - clearSession(); - }; - - function initDefaultSocket() { - defaultSocket = root.openSignalingChannel({ - onmessage: function(response) { - if (response.userid == self.userid) return; - - if (isAcceptNewSession && response.sessionid && response.userid) { - root.session = session = response.session; - config.onNewSession(response); - } - if (response.newParticipant && self.joinedARoom && self.broadcasterid === response.userid) - onNewParticipant(response.newParticipant, response.extra); - - if (getLength(participants) < root.maxParticipantsAllowed && response.userid && response.targetUser == self.userid && response.participant && !participants[response.userid]) { - acceptRequest(response.channel || response.userid, response.extra, response.userid); - } - - if (response.userType && response.userType != root.userType) { - if (!root.busy) { - if (response.userType == 'admin') { - if (root.onAdmin) root.onAdmin(response); - else root.accept(response.userid); - } - if (response.userType == 'guest') { - if (root.onGuest) root.onGuest(response); - else root.accept(response.userid); - } - } else { - if (response.userType != root.userType) { - defaultSocket.send({ - rejectedRequestOf: response.userid, - userid: self.userid, - extra: root.extra || { } - }); - } - } - } - - if (response.acceptedRequestOf == self.userid) { - if (root.onstats) root.onstats('accepted', response); - } - - if (response.rejectedRequestOf == self.userid) { - if (root.onstats) root.onstats('busy', response); - sendRequest(); - } - }, - callback: function(socket) { - defaultSocket = socket; - if (root.userType) sendRequest(); - } - }); - } - - var that = this, defaultSocket; - - initDefaultSocket(); - - function sendRequest() { - defaultSocket.send({ - userType: root.userType, - userid: root.userid, - extra: root.extra || { } - }); - } - - function setDirections() { - if (root.direction == 'one-way') root.session.oneway = true; - if (root.direction == 'one-to-one') root.maxParticipantsAllowed = 1; - if (root.direction == 'one-to-many') root.session.broadcast = true; - if (root.direction == 'many-to-many') { - root.maxParticipantsAllowed = 256; - } - } - - // open new session - this.initSession = function() { - that.isOwnerLeaving = false; - root.isInitiator = true; - - setDirections(); - session = root.session; - - isbroadcaster = true; - participants = { }; - - self.sessionid = root.sessionid || root.channel; - - this.isOwnerLeaving = isAcceptNewSession = false; - - (function transmit() { - if (getLength(participants) < root.maxParticipantsAllowed && !that.isOwnerLeaving) { - defaultSocket && defaultSocket.send({ - sessionid: self.sessionid, - userid: root.userid, - session: session, - extra: root.extra - }); - } - - if (!root.transmitRoomOnce && !that.isOwnerLeaving) - setTimeout(transmit, root.interval || 3000); - })(); - }; - - // join existing session - this.joinSession = function(_config) { - _config = _config || { }; - - participants = { }; - - session = _config.session; - - self.joinedARoom = true; - self.broadcasterid = _config.userid; - - if (_config.sessionid) - self.sessionid = _config.sessionid; - - isAcceptNewSession = false; - - var channel = getRandomString(); - newPrivateSocket({ - channel: channel, - extra: root.extra - }); - - defaultSocket.send({ - participant: true, - userid: self.userid, - channel: channel, - targetUser: _config.userid, - extra: root.extra - }); - }; - - // send file/data or text message - this.send = function(message, _channel) { - message = JSON.stringify(message); - - if (_channel) { - if (_channel.readyState == 'open') { - _channel.send(message); - } - return; - } - - for (var dataChannel in root.channels) { - var channel = root.channels[dataChannel].channel; - if (channel.readyState == 'open') { - channel.send(message); - } - } - }; - - // leave session - this.leave = function(userid) { - clearSession(userid); - - if (isbroadcaster) { - that.isOwnerLeaving = true; - root.isInitiator = false; - } - - // to stop/remove self streams - for (var i = 0; i < root.attachStreams.length; i++) { - stopTracks(root.attachStreams[i]); - } - root.attachStreams = []; - - // to allow capturing of identical streams - currentUserMediaRequest = { - streams: [], - mutex: false, - queueRequests: [] - }; - - if (!userid) { - // self.userid = root.userid = root.token(); - root.joinedARoom = self.joinedARoom = isbroadcaster = false; - isAcceptNewSession = true; - } - - root.busy = false; - }; - - // renegotiate new stream - this.addStream = function(e) { - session = e.renegotiate; - - if (e.socket) - addStream(e.socket); - else - for (var i = 0; i < sockets.length; i++) - addStream(sockets[i]); - - function addStream(socket) { - peer = root.peers[socket.userid]; - - if (!peer) - throw 'No such peer exists.'; - - peer = peer.peer; - - // if offerer; renegotiate - if (peer && peer.connection.localDescription.type == 'offer') { - // detaching old streams - detachMediaStream(root.detachStreams, peer.connection); - - if (session.audio || session.video || session.screen) - peer.connection.addStream(e.stream); - - peer.recreateOffer(session, function(sdp, streaminfo) { - sendsdp({ - sdp: sdp, - socket: socket, - renegotiate: session, - labels: root.detachStreams, - streaminfo: streaminfo - }); - root.detachStreams = []; - }); - } else { - // otherwise; suggest other user to play role of renegotiator - socket.send({ - userid: self.userid, - renegotiate: session, - suggestRenegotiation: true - }); - } - } - }; - - root.request = function(userid) { - if (!root.session['many-to-many']) root.busy = true; - - root.captureUserMedia(function() { - // open private socket that will be used to receive offer-sdp - newPrivateSocket({ - channel: self.userid, - extra: root.extra || { } - }); - - // ask other user to create offer-sdp - defaultSocket.send({ - participant: true, - userid: self.userid, - extra: root.extra || { }, - targetUser: userid - }); - }); - }; - - function acceptRequest(channel, extra, userid) { - if (root.busy) return; - - if (root.onRequest) root.onRequest(channel, extra, userid); - else _accept(channel, extra, userid); - } - - function _accept(channel, extra, userid) { - if (root.userType) { - if (!root.session['many-to-many']) root.busy = true; - defaultSocket.send({ - acceptedRequestOf: userid, - userid: self.userid, - extra: root.extra || { } - }); - } - - participants[userid] = userid; - newPrivateSocket({ - isofferer: true, - userid: userid, - channel: channel, - extra: extra || { } - }); - } - - root.accept = function(userid, extra) { - root.captureUserMedia(function() { - _accept(userid, extra); - }); - }; - } - - function getRandomString() { - return (Math.random() * new Date().getTime()).toString(36).toUpperCase().replace( /\./g , '-'); - } - - // _______ - // File.js - - var File = { - Send: function(config) { - var file = config.file; - var socket = config.channel; - - var chunkSize = config.chunkSize || 40 * 1000; // 64k max sctp limit (AFAIK!) - var sliceId = 0; - var cacheSize = chunkSize; - - var chunksPerSlice = Math.floor(Math.min(100000000, cacheSize) / chunkSize); - var sliceSize = chunksPerSlice * chunkSize; - var maxChunks = Math.ceil(file.size / chunkSize); - - // uuid is used to uniquely identify sending instance - var uuid = (Math.random() * new Date().getTime()).toString(36).toUpperCase().replace( /\./g , '-'); - - socket.send({ - uuid: uuid, - maxChunks: maxChunks, - size: file.size, - name: file.name, - lastModifiedDate: file.lastModifiedDate, - type: file.type, - start: true - }, config.extra); - - file.maxChunks = maxChunks; - file.uuid = uuid; - if (config.onBegin) config.onBegin(file); - - var blob, reader = new FileReader(); - reader.onloadend = function(evt) { - if (evt.target.readyState == FileReader.DONE) { - addChunks(file.name, evt.target.result, function() { - sliceId++; - if ((sliceId + 1) * sliceSize < file.size) { - blob = file.slice(sliceId * sliceSize, (sliceId + 1) * sliceSize); - reader.readAsArrayBuffer(blob); - } else if (sliceId * sliceSize < file.size) { - blob = file.slice(sliceId * sliceSize, file.size); - reader.readAsArrayBuffer(blob); - } else { - socket.send({ - uuid: uuid, - maxChunks: maxChunks, - size: file.size, - name: file.name, - lastModifiedDate: file.lastModifiedDate, - type: file.type, - end: true - }, config.extra); - - file.url = URL.createObjectURL(file); - if (config.onEnd) config.onEnd(file); - } - }); - } - }; - - blob = file.slice(sliceId * sliceSize, (sliceId + 1) * sliceSize); - reader.readAsArrayBuffer(blob); - - var numOfChunksInSlice; - var currentPosition = 0; - var hasEntireFile; - var chunks = []; - - function addChunks(fileName, binarySlice, callback) { - numOfChunksInSlice = Math.ceil(binarySlice.byteLength / chunkSize); - for (var i = 0; i < numOfChunksInSlice; i++) { - var start = i * chunkSize; - chunks[currentPosition] = binarySlice.slice(start, Math.min(start + chunkSize, binarySlice.byteLength)); - - FileConverter.ArrayBufferToDataURL(chunks[currentPosition], function(str) { - socket.send({ - uuid: uuid, - value: str, - currentPosition: currentPosition, - maxChunks: maxChunks - }, config.extra); - }); - - currentPosition++; - } - - if (config.onProgress) { - config.onProgress({ - currentPosition: currentPosition, - maxChunks: maxChunks, - uuid: uuid - }); - } - - if (currentPosition == maxChunks) { - hasEntireFile = true; - } - - if (config.interval == 0 || typeof config.interval == 'undefined') - callback(); - else - setTimeout(callback, config.interval); - } - }, - - Receiver: function(config) { - var packets = { }; - - function receive(chunk) { - if (chunk.start && !packets[chunk.uuid]) { - packets[chunk.uuid] = []; - if (config.onBegin) config.onBegin(chunk); - } - - if (!chunk.end && chunk.value) packets[chunk.uuid].push(chunk.value); - - if (chunk.end) { - var _packets = packets[chunk.uuid]; - var finalArray = [], length = _packets.length; - - for (var i = 0; i < length; i++) { - if (!!_packets[i]) { - FileConverter.DataURLToBlob(_packets[i], function(buffer) { - finalArray.push(buffer); - }); - } - } - - var blob = new Blob(finalArray, { type: chunk.type }); - blob = merge(blob, chunk); - blob.url = URL.createObjectURL(blob); - blob.uuid = chunk.uuid; - - if (!blob.size) console.error('Something went wrong. Blob Size is 0.'); - - if (config.onEnd) config.onEnd(blob); - } - - if (chunk.value && config.onProgress) config.onProgress(chunk); - } - - return { - receive: receive - }; - }, - SaveToDisk: function(fileUrl, fileName) { - var hyperlink = document.createElement('a'); - hyperlink.href = fileUrl; - hyperlink.target = '_blank'; - hyperlink.download = fileName || fileUrl; - - var mouseEvent = new MouseEvent('click', { - view: window, - bubbles: true, - cancelable: true - }); - - hyperlink.dispatchEvent(mouseEvent); - (window.URL || window.webkitURL).revokeObjectURL(hyperlink.href); - } - }; - - // ________________ - // FileConverter.js - var FileConverter = { - ArrayBufferToDataURL: function(buffer, callback) { - window.BlobBuilder = window.MozBlobBuilder || window.WebKitBlobBuilder || window.BlobBuilder; - - // getting blob from array-buffer - var blob = new Blob([buffer]); - - // reading file as binary-string - var fileReader = new FileReader(); - fileReader.onload = function(e) { - callback(e.target.result); - }; - fileReader.readAsDataURL(blob); - }, - DataURLToBlob: function(dataURL, callback) { - var binary = atob(dataURL.substr(dataURL.indexOf(',') + 1)), - i = binary.length, - view = new Uint8Array(i); - - while (i--) { - view[i] = binary.charCodeAt(i); - } - - callback(new Blob([view])); - } - }; - - // _____________ - // FileSender.js - var FileSender = { - send: function(config) { - var root = config.root; - - var interval = 100; - if (!root.preferSCTP && !moz) interval = 500; - - // using File.js to send files - File.Send({ - channel: config.channel, - extra: config._channel, - file: config.file, - interval: interval, - chunkSize: config.preferSCTP ? 40 * 1000 : 1000, // 64k max sctp limit (AFAIK!) - onProgress: function(file) { - if (root.onFileProgress) { - root.onFileProgress({ - // old one; for backward compatibility - remaining: file.maxChunks - file.currentPosition, - length: file.maxChunks, - sent: file.currentPosition, - - // NEW properties - maxChunks: file.maxChunks, - currentPosition: file.currentPosition, - uuid: file.uuid - }, file.uuid); - } - }, - onBegin: root.onFileStart, - onEnd: function(file) { - if (root.onFileSent) { - root.onFileSent(file, file.uuid); - } - - if (!root.fileQueue[file.name]) - root.fileQueue[file.name] = file; - } - }); - } - }; - - // _______________ - // FileReceiver.js - - function FileReceiver(root) { - var receiver = new File.Receiver({ - onProgress: function(file) { - if (root.onFileProgress) { - root.onFileProgress({ - // old one; for backward compatibility - remaining: file.maxChunks - file.currentPosition, - length: file.maxChunks, - received: file.currentPosition, - - // NEW properties - maxChunks: file.maxChunks, - currentPosition: file.currentPosition, - uuid: file.uuid - }, file.uuid); - } - }, - onBegin: root.onFileStart, - onEnd: function(file) { - if (root.autoSaveToDisk) { - File.SaveToDisk(file.dataURL, file.name); - } - - if (root.onFileReceived) { - root.onFileReceived(file.name, file); - } - } - }); - - return { - receive: function(data) { - receiver.receive(data); - } - }; - } - - // _____________ - // TextSender.js - var TextSender = { - send: function(config) { - var channel = config.channel, - _channel = config._channel, - initialText = config.text, - packetSize = 1000, - textToTransfer = '', - isobject = false; - - if (typeof initialText !== 'string') { - 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) { - if (config.preferSCTP || moz) { - setTimeout(function() { - sendText(null, textToTransfer); - }, 100); - } else - setTimeout(function() { - sendText(null, textToTransfer); - }, 500); - } - } - } - }; - - // _______________ - // TextReceiver.js - - function TextReceiver() { - var content = { }; - - function receive(data, onmessage, 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; - - if (onmessage) - onmessage({ - data: message, - userid: userid, - extra: extra, - latency: latency - }); - - delete content[uuid]; - } - } - - return { - receive: receive - }; - } - - window.MediaStream = window.MediaStream || window.webkitMediaStream; - - window.moz = !!navigator.mozGetUserMedia; - var RTCPeerConnection = function(options) { - var w = window, - PeerConnection = w.mozRTCPeerConnection || w.webkitRTCPeerConnection, - SessionDescription = w.mozRTCSessionDescription || w.RTCSessionDescription, - IceCandidate = w.mozRTCIceCandidate || w.RTCIceCandidate; - - // protocol: 'text/chat', preset: true, stream: 16 - var dataChannelDict = { }, i; - - var STUN = { - url: !moz ? 'stun:stun.l.google.com:19302' : 'stun:23.21.150.121' - }; - - var TURN = { - url: 'turn:homeo@turn.bistri.com:80', - credential: 'homeo' - }; - - var iceServers = { - iceServers: options.iceServers || [STUN] - }; - - if (!moz && !options.iceServers) { - if (parseInt(navigator.userAgent.match( /Chrom(e|ium)\/([0-9]+)\./ )[2]) >= 28) - TURN = { - url: 'turn:turn.bistri.com:80', - credential: 'homeo', - username: 'homeo' - }; - - iceServers.iceServers = [STUN, TURN]; - } - - var optional = { - optional: [] - }; - - if (!moz) { - optional.optional = [{ - DtlsSrtpKeyAgreement: true - }]; - - if (options.disableDtlsSrtp) - optional = { - optional: [] - }; - - if (options.onmessage && !options.preferSCTP) - optional.optional = [{ - RtpDataChannels: true - }]; - } - - // local/host candidates can also be used for peer connection - if (!navigator.onLine) { - iceServers = null; - console.warn('No internet connection detected. No STUN/TURN server is used to make sure local/host candidates are used for peers connection.'); - } else log('iceServers', JSON.stringify(iceServers, null, '\t')); - - var peer = new PeerConnection(iceServers, optional); - - openOffererChannel(); - - peer.onicecandidate = function(event) { - if (event.candidate) - options.onICE(event.candidate); - }; - - // adding media streams to the PeerConnection - if (options.attachStreams && options.attachStreams.length) { - var streams = options.attachStreams; - for (i = 0; i < streams.length; i++) { - peer.addStream(streams[i]); - } - } - - peer.onaddstream = function(event) { - log('on:add:stream', event.stream); - - if (!event || !options.onstream) return; - - options.onstream(event.stream); - options.renegotiate = false; - }; - - peer.onsignalingstatechange = function() { - log('onsignalingstatechange:', toStr({ - iceGatheringState: peer.iceGatheringState, - signalingState: peer.signalingState - })); - }; - peer.oniceconnectionstatechange = function() { - log('oniceconnectionstatechange:', toStr({ - iceGatheringState: peer.iceGatheringState, - signalingState: peer.signalingState - })); - }; - - peer.onremoveStream = function(event) { - log('on:remove:stream', event.stream); - }; - - peer.onconnecting = function(event) { - log('on:connecting', event); - }; - - peer.onnegotiationneeded = function(event) { - log('on:negotiation:needed', event); - }; - - var constraints; - - function setConstraints() { - var session = options.session; - - var sdpConstraints = options.sdpConstraints; - constraints = options.constraints || { - optional: [], - mandatory: { - OfferToReceiveAudio: !!session.audio, - OfferToReceiveVideo: !!session.video || !!session.screen - } - }; - - if (sdpConstraints.mandatory) - constraints.mandatory = merge(constraints.mandatory, sdpConstraints.mandatory); - - if (sdpConstraints.optional) - constraints.optional[0] = merge({ }, sdpConstraints.optional); - - log('sdp constraints', JSON.stringify(constraints, null, '\t')); - } - - setConstraints(); - - var streaminfo = []; - for (i = 0; i < options.attachStreams.length; i++) { - var stream = options.attachStreams[i]; - streaminfo.push({ - streamid: stream.streamid - }); - } - - function createOffer() { - if (!options.onOfferSDP) - return; - - peer.createOffer(function(sessionDescription) { - sessionDescription.sdp = setBandwidth(sessionDescription.sdp); - peer.setLocalDescription(sessionDescription); - - options.onOfferSDP(sessionDescription, streaminfo); - }, onSdpError, constraints); - } - - function createAnswer() { - if (!options.onAnswerSDP) - return; - - options.offerSDP = new SessionDescription(options.offerSDP, onSdpSuccess, onSdpError); - peer.setRemoteDescription(options.offerSDP); - - peer.createAnswer(function(sessionDescription) { - sessionDescription.sdp = setBandwidth(sessionDescription.sdp); - peer.setLocalDescription(sessionDescription); - options.onAnswerSDP(sessionDescription, streaminfo); - }, onSdpError, constraints); - } - - createOffer(); - createAnswer(); - - if (options.preferSCTP || (options.onAnswerSDP && options.onmessage && moz)) - openAnswererChannel(); - - var bandwidth = options.bandwidth; - - function setBandwidth(sdp) { - if (!bandwidth || moz) return sdp; - - // remove existing bandwidth lines - sdp = sdp.replace( /b=AS([^\r\n]+\r\n)/g , ''); - - if (bandwidth.audio) { - sdp = sdp.replace( /a=mid:audio\r\n/g , 'a=mid:audio\r\nb=AS:' + bandwidth.audio + '\r\n'); - } - - if (bandwidth.video) { - sdp = sdp.replace( /a=mid:video\r\n/g , 'a=mid:video\r\nb=AS:' + bandwidth.video + '\r\n'); - } - - if (bandwidth.data && !options.preferSCTP) { - sdp = sdp.replace( /a=mid:data\r\n/g , 'a=mid:data\r\nb=AS:' + bandwidth.data + '\r\n'); - } - - return sdp; - } - - var channel; - - function openOffererChannel() { - if (!options.onmessage || (moz && !options.onOfferSDP)) - return; - - _openOffererChannel(); - - if (!moz) return; - navigator.mozGetUserMedia({ - audio: true, - fake: true - }, function(mozStream) { - peer.addStream(mozStream); - }, useless); - } - - function _openOffererChannel() { - if (!options.preferSCTP) dataChannelDict.reliable = false; - - channel = peer.createDataChannel(options.channel || 'data-channel', dataChannelDict); - setChannelEvents(); - } - - function setChannelEvents() { - channel.onmessage = function(event) { - options.onmessage(event.data); - }; - channel.onopen = function() { - options.onopen(channel); - }; - - channel.onerror = function(e) { - options.onerror(e); - }; - - channel.onclose = function(e) { - options.onclose(e); - }; - - channel.push = channel.send; - channel.send = function(data) { - try { - channel.push(data); - } catch(e) { - setTimeout(function() { - channel.send(data); - }, 1); - } - }; - } - - function openAnswererChannel() { - peer.ondatachannel = function(event) { - channel = event.channel; - setChannelEvents(); - }; - - if (!moz) return; - navigator.mozGetUserMedia({ - audio: true, - fake: true - }, function(mozStream) { - peer.addStream(mozStream); - }, useless); - } - - function useless() { - log('Error in fake:true'); - } - - function onSdpSuccess() { - } - - function onSdpError(e) { - console.error('sdp error:', JSON.stringify(e, null, '\t')); - } - - return { - connection: peer, - addAnswerSDP: function(sdp) { - peer.setRemoteDescription(new SessionDescription(sdp), onSdpSuccess, onSdpError); - }, - addICE: function(candidate) { - peer.addIceCandidate(new IceCandidate({ - sdpMLineIndex: candidate.sdpMLineIndex, - candidate: candidate.candidate - })); - }, - recreateAnswer: function(sdp, session, callback) { - options.renegotiate = true; - - options.session = session; - setConstraints(); - - options.onAnswerSDP = callback; - options.offerSDP = sdp; - createAnswer(); - }, - recreateOffer: function(session, callback) { - options.renegotiate = true; - - options.session = session; - setConstraints(); - - options.onOfferSDP = callback; - createOffer(); - } - }; - }; - - var video_constraints = { - mandatory: { }, - optional: [] - }; - - /* by @FreCap pull request #41 */ - var currentUserMediaRequest = { - streams: [], - mutex: false, - queueRequests: [] - }; - - function getUserMedia(options) { - if (currentUserMediaRequest.mutex === true) { - currentUserMediaRequest.queueRequests.push(options); - return; - } - currentUserMediaRequest.mutex = true; - - // http://tools.ietf.org/html/draft-alvestrand-constraints-resolution-00 - var mediaConstraints = options.mediaConstraints || { }; - var n = navigator, - hints = options.constraints || { - audio: true, - video: video_constraints - }; - - if (hints.video == true) hints.video = video_constraints; - - // connection.mediaConstraints.audio = false; - if (typeof mediaConstraints.audio != 'undefined') - hints.audio = mediaConstraints.audio; - - // connection.media.min(320,180); - // connection.media.max(1920,1080); - var media = options.media; - if (!moz) { - var mandatory = { - minWidth: media.minWidth, - minHeight: media.minHeight, - maxWidth: media.maxWidth, - maxHeight: media.maxHeight, - minAspectRatio: media.minAspectRatio - }; - - // https://code.google.com/p/chromium/issues/detail?id=143631#c9 - 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) { - console.error('The min/max width/height constraints you passed "seems" NOT supported.', toStr(mandatory)); - } - - if (mandatory.minWidth > mandatory.maxWidth || mandatory.minHeight > mandatory.maxHeight) { - console.error('Minimum value must not exceed maximum value.', toStr(mandatory)); - } - - if (mandatory.minWidth >= 1280 && mandatory.minHeight >= 720) { - console.info('Enjoy HD video! min/' + mandatory.minWidth + ':' + mandatory.minHeight + ', max/' + mandatory.maxWidth + ':' + mandatory.maxHeight); - } - - hints.video.mandatory = merge(hints.video.mandatory, mandatory); - } - - if (mediaConstraints.mandatory) - hints.video.mandatory = merge(hints.video.mandatory, mediaConstraints.mandatory); - - // mediaConstraints.optional.bandwidth = 1638400; - if (mediaConstraints.optional) - hints.video.optional[0] = merge({ }, mediaConstraints.optional); - - log('media hints:', toStr(hints)); - - // easy way to match - var idInstance = JSON.stringify(hints); - - function streaming(stream, returnBack) { - var video = options.video; - if (video) { - video[moz ? 'mozSrcObject' : 'src'] = moz ? stream : window.webkitURL.createObjectURL(stream); - video.play(); - } - - options.onsuccess(stream, returnBack); - currentUserMediaRequest.streams[idInstance] = stream; - currentUserMediaRequest.mutex = false; - if (currentUserMediaRequest.queueRequests.length) - getUserMedia(currentUserMediaRequest.queueRequests.shift()); - } - - if (currentUserMediaRequest.streams[idInstance]) { - streaming(currentUserMediaRequest.streams[idInstance], true); - } else { - n.getMedia = n.webkitGetUserMedia || n.mozGetUserMedia; - n.getMedia(hints, streaming, options.onerror || function(e) { - console.error(e); - }); - } - } - - function isData(session) { - return !session.audio && !session.video && !session.screen && session.data; - } - - 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 log(a, b, c, d, e, f) { - if (window.skipRTCMultiConnectionLogs) return; - if (f) - console.log(a, b, c, d, e, f); - else if (e) - console.log(a, b, c, d, e); - else if (d) - console.log(a, b, c, d); - else if (c) - console.log(a, b, c); - else if (b) - console.log(a, b); - else if (a) - console.log(a); - } - - function toStr(obj) { - return JSON.stringify(obj, function(key, value) { - if (value && value.sdp) { - console.log(value.sdp.type, '---', value.sdp.sdp); - return ''; - } else return value; - }, '---'); - } - - function getLength(obj) { - var length = 0; - for (var o in obj) - if (o) length++; - return length; - } - - // Get HTMLAudioElement/HTMLVideoElement accordingly - - function getMediaElement(stream, session) { - var isAudio = session.audio && !session.video && !session.screen; - if (!moz && stream.getAudioTracks && stream.getVideoTracks) { - isAudio = stream.getAudioTracks().length && !stream.getVideoTracks().length; - } - - var mediaElement = document.createElement(isAudio ? 'audio' : 'video'); - mediaElement[moz ? 'mozSrcObject' : 'src'] = moz ? stream : window.webkitURL.createObjectURL(stream); - mediaElement.autoplay = true; - mediaElement.controls = true; - mediaElement.volume = 1; - mediaElement.play(); - return mediaElement; - } - - function merge(mergein, mergeto) { - if (!mergein) mergein = { }; - if (!mergeto) return mergein; - - for (var item in mergeto) { - mergein[item] = mergeto[item]; - } - return mergein; - } - - // the purpose of this method is to detect mic/speaker activity - - function voiceActivityDetection(peer) { - if (moz) return; - - peer.getStats(function(stats) { - var output = { }; - var sr = stats.result(); - for (var i = 0; i < sr.length; i++) { - var obj = sr[i].remote; - if (obj) { - var nspk = 0.0; - var nmic = 0.0; - if (obj.stat('audioInputLevel')) { - nmic = obj.stat('audioInputLevel'); - } - if (nmic > 0.0) { - output.mic = Math.floor(Math.max((Math.LOG2E * Math.log(nmic) - 4.0), 0.0)); - } - if (obj.stat('audioOutputLevel')) { - nspk = obj.stat('audioOutputLevel'); - } - if (nspk > 0.0) { - output.speaker = Math.floor(Math.max((Math.LOG2E * Math.log(nspk) - 4.0), 0.0)); - } - } - } - log('mic intensity:', output.mic); - log('speaker intensity:', output.speaker); - log('Type to stop this logger.'); - }); - - if (!window.skipRTCMultiConnectionLogs) - setTimeout(function() { - voiceActivityDetection(peer); - }, 2000); - } - - function loadScript(src, onload) { - var script = document.createElement('script'); - script.src = src; - if (onload) script.onload = onload; - document.documentElement.appendChild(script); - } - - function muteOrUnmute(e) { - var stream = e.stream, - root = e.root, - session = e.session || { }, - enabled = e.enabled; - - if (!session.audio && !session.video) { - session = merge(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('session', JSON.stringify(session, null, '\t')); - - // enable/disable audio/video tracks - - if (session.audio) { - var audioTracks = stream.getAudioTracks()[0]; - if (audioTracks) - audioTracks.enabled = !enabled; - } - - if (session.video) { - var videoTracks = stream.getVideoTracks()[0]; - if (videoTracks) - videoTracks.enabled = !enabled; - } - - // socket message to change media element look - if (root.socket) - root.socket.send({ - userid: root.userid, - streamid: root.streamid, - mute: !!enabled, - unmute: !enabled - }); - } - - RTCMultiConnection.prototype.setDefaults = DefaultSettings; - - function DefaultSettings() { - this.onmessage = function(e) { - log(e.userid, 'posted', e.data); - }; - - this.onopen = function(e) { - log('Data connection is opened between you and', e.userid); - }; - - this.onerror = function(e) { - console.error('Error in data connection between you and', e.userid, e); - }; - - this.onclose = function(e) { - console.warn('Data connection between you and', e.userid, 'is closed.', e); - }; - - this.onFileReceived = function(fileName) { - log('File <', fileName, '> received successfully.'); - }; - - this.onFileSent = function(file) { - log('File <', file.name, '> sent successfully.'); - }; - - this.onFileProgress = function(packets) { - log('<', packets.remaining, '> items remaining.'); - }; - - this.onstream = function(e) { - log('on:add:stream', e.stream); - }; - - this.onleave = function(e) { - log(e.userid, 'left!'); - }; - - this.peers = { }; - - this.streams = { - mute: function(session) { - this._private(session, true); - }, - unmute: function(session) { - this._private(session, false); - }, - _private: function(session, enabled) { - // implementation from #68 - for (var stream in this) { - if (stream != 'mute' && stream != 'unmute' && stream != '_private') { - var root = this[stream]; - muteOrUnmute({ - root: root, - session: session, - stream: root.stream, - enabled: enabled - }); - } - } - } - }; - this.channels = { }; - this.extra = { }; - - this.session = { - audio: true, - video: true - }; - - this.bandwidth = { - data: 1638400 - }; - - // preferring SCTP data channels! - this.preferSCTP = false; - - // file queue: to store previous file objects in memory; - // and stream over newly connected peers - this.fileQueue = { }; - - this.media = { - min: function(width, height) { - this.minWidth = width; - this.minHeight = height; - }, - minWidth: 640, // 1920 - minHeight: 360, // 1080 - max: function(width, height) { - this.maxWidth = width; - this.maxHeight = height; - }, - maxWidth: 1920, - maxHeight: 1080, - bandwidth: 256, - minFrameRate: 32, - minAspectRatio: 1.77 - }; - - this.candidates = { - host: true, - relay: true, - reflexive: true - }; - - this.mediaConstraints = { }; - this.sdpConstraints = { }; - - this.attachStreams = []; - this.detachStreams = []; - - this.maxParticipantsAllowed = 256; - this.autoSaveToDisk = true; - - // 'many-to-many' / 'one-to-many' / 'one-to-one' / 'one-way' - this.direction = 'many-to-many'; - - this._getStream = function(e) { - return { - stream: e.stream, - userid: e.userid, - streamid: e.streamid, - socket: e.socket, - type: e.type, - mediaElement: e.mediaElement, - stop: function() { - // socket message to change media element look - if (this.socket) { - this.socket.send({ - userid: this.userid, - streamid: this.streamid, - stopped: true - }); - } - - var stream = this.stream; - if (stream && stream.stop) { - stopTracks(stream); - } - }, - mute: function(session) { - this._private(session, true); - }, - unmute: function(session) { - this._private(session, false); - }, - _private: function(session, enabled) { - muteOrUnmute({ - root: this, - session: session, - enabled: enabled, - stream: this.stream - }); - }, - startRecording: function(session) { - if (!session) session = { audio: true, video: true }; - if (!window.RecordRTC) { - var self = this; - return loadScript('https://www.webrtc-experiment.com/RecordRTC.js', function() { - self.startRecording(session); - }); - } - - var stream = this.stream; - if (session.audio) { - this.recordAudio = RecordRTC(stream, session); - this.recordAudio.startRecording(); - } - - // video recording on firefox has some issues - if (!moz && session.video) { - this.recordVideo = RecordRTC(stream, merge(session, { - type: 'video' - })); - this.recordVideo.startRecording(); - } - }, - stopRecording: function(onBlob, session) { - if (!session) session = { audio: true, video: true }; - else - session = { - audio: session == 'audio', - video: session == 'video' - }; - - if (session.audio && this.recordAudio) { - this.recordAudio.stopRecording(); - - var blob = this.recordAudio.getBlob(); - blob.recordingType = 'audio'; - if (onBlob) onBlob(blob); - } - - if (!moz && session.video && this.recordVideo) { - this.recordVideo.stopRecording(); - - blob = this.recordVideo.getBlob(); - blob.recordingType = 'video'; - if (onBlob) onBlob(blob); - } - } - }; - }; - - this.token = function() { - return (Math.random() * new Date().getTime()).toString(36).replace( /\./g , ''); - }; - } - - function stopTracks(mediaStream) { - var fallback = false, i; - - // MediaStream.stop should be avoided. It still exist and works but - // it is removed from the spec and instead MediaStreamTrack.stop should be used - var audioTracks = mediaStream.getAudioTracks(); - var videoTracks = mediaStream.getVideoTracks(); - - for (i = 0; i < audioTracks.length; i++) { - if (audioTracks[i].stop) - audioTracks[i].stop(); - else { - fallback = true; - continue; - } - } - - for (i = 0; i < videoTracks.length; i++) { - if (videoTracks[i].stop) - videoTracks[i].stop(); - else { - fallback = true; - continue; - } - } - - if (fallback && mediaStream.stop) mediaStream.stop(); - } -})(); diff --git a/RTCMultiConnection/Library/RTCMultiConnection-v1.5.js b/RTCMultiConnection/Library/RTCMultiConnection-v1.5.js deleted file mode 100644 index ce9a2178..00000000 --- a/RTCMultiConnection/Library/RTCMultiConnection-v1.5.js +++ /dev/null @@ -1,2900 +0,0 @@ -// Last time updated at Tuesday, 23 January 2014, 06:46:23 - -// Muaz Khan - www.MuazKhan.com -// MIT License - www.WebRTC-Experiment.com/licence -// Documentation - www.RTCMultiConnection.org/docs/ - -// FAQ - www.RTCMultiConnection.org/FAQ/ -// Development News - trello.com/b/8bhi1G6n/RTCMultiConnection - -// v1.5 changes log - www.RTCMultiConnection.org/changes-log/#v1.5 -// _______________________ -// RTCMultiConnection-v1.5 - -(function () { - - // www.RTCMultiConnection.org/docs/ - window.RTCMultiConnection = function (channel) { - // www.RTCMultiConnection.org/docs/channel-id/ - this.channel = channel || location.href.replace(/\/|:|#|%|\.|\[|\]/g, ''); - - // www.RTCMultiConnection.org/docs/open/ - this.open = function (_channel) { - self.joinedARoom = true; - - if (_channel) - self.channel = _channel; - - // if firebase && if session initiator - if (self.socket && self.socket.onDisconnect) - self.socket.onDisconnect().remove(); - - // www.RTCMultiConnection.org/docs/session-initiator/ - self.isInitiator = true; - - prepareInit(function () { - init(); - captureUserMedia(rtcSession.initSession); - }); - }; - - // www.RTCMultiConnection.org/docs/connect/ - this.connect = function (_channel) { - if (_channel) - self.channel = _channel; - - prepareInit(init); - - return this; - }; - - // www.RTCMultiConnection.org/docs/join/ - this.join = joinSession; - - // www.RTCMultiConnection.org/docs/send/ - this.send = function (data, _channel) { - // send file/data or /text - if (!data) - throw 'No file, data or text message to share.'; - - // connection.send([file1, file2, file3]) - if (!!data.forEach) { - for (var i = 0; i < data.length; i++) { - self.send(data[i], _channel); - } - return; - } - - if (data.size) { - FileSender.send({ - file: data, - channel: rtcSession, - root: self, - _channel: _channel - }); - } else - TextSender.send({ - text: data, - channel: rtcSession, - _channel: _channel, - preferSCTP: self.preferSCTP - }); - }; - - var self = this, - rtcSession, fileReceiver, textReceiver; - - function prepareInit(callback) { - if (self.openSignalingChannel) return callback(); - - // make sure firebase.js is loaded before using their JavaScript API - if (!window.Firebase) { - return loadScript('//cdn.firebase.com/v0/firebase.js', function () { - prepareInit(callback); - }); - } - - // Single socket is a preferred solution! - var socketCallbacks = {}; - var firebase = new Firebase('//' + (self.firebase || 'muazkh') + '.firebaseio.com/' + self.channel); - firebase.on('child_added', function (snap) { - var data = snap.val(); - if (data.sender == self.userid) return; - - if (socketCallbacks[data.channel] && socketCallbacks[data.channel].onmessage) { - socketCallbacks[data.channel].onmessage(data.message); - } - - snap.ref().remove(); - }); - - self.socket = firebase; - - // www.RTCMultiConnection.org/docs/openSignalingChannel/ - self.openSignalingChannel = function (config) { - var callbackid = config.channel || self.channel; - socketCallbacks[callbackid] = config; - - if (config.onopen) setTimeout(config.onopen, 1000); - return { - send: function (message) { - firebase.push({ - sender: self.userid, - channel: callbackid, - message: message - }); - }, - channel: channel - }; - }; - callback(); - } - - // set config passed over RTCMultiSession - - function init() { - if (self.config) - return; - - self.config = { - onNewSession: function (session) { - if (!rtcSession) { - self._session = session; - return; - } - - if (self.onNewSession) - return self.onNewSession(session); - - // user is already connected to a session - if (self.joinedARoom) return; - - self.joinedARoom = true; - - return joinSession(session); - }, - onmessage: function (e) { - if (!e.data.size) - e.data = JSON.parse(e.data); - - if (e.data.type === 'text') { - textReceiver.receive(e.data, self.onmessage, e.userid, e.extra); - } else if (e.data.type === 'file') { - fileReceiver.receive(e.data); - } else { - self.onmessage(e); - } - } - }; - rtcSession = new RTCMultiSession(self); - - fileReceiver = new FileReceiver(self); - textReceiver = new TextReceiver(); - - if (self._session) - self.config.onNewSession(self._session); - } - - function joinSession(session) { - if (!session || !session.userid || !session.sessionid) - throw 'invalid data passed.'; - - self.session = session.session; - - extra = self.extra || session.extra || {}; - - if (session.oneway || session.data) - rtcSession.joinSession(session, extra); - else - captureUserMedia(function () { - rtcSession.joinSession(session, extra); - }); - } - - // www.RTCMultiConnection.org/docs/captureUserMedia/ - - function captureUserMedia(callback, _session) { - // capture user's media resources - var session = _session || self.session; - - // www.RTCMultiConnection.org/docs/dontAttachStream/ - if (self.dontAttachStream) - return callback(); - - if (isData(session) || (!self.isInitiator && session.oneway)) { - // www.RTCMultiConnection.org/docs/attachStreams/ - self.attachStreams = []; - return callback(); - } - - var constraints = { - audio: !!session.audio, - video: !!session.video - }; - - // if custom audio device is selected - if (self._mediaSources.audio) { - constraints.audio = { - optional: [{ sourceId: self._mediaSources.audio}] - }; - } - - // if custom video device is selected - if (self._mediaSources.video) { - constraints.video = { - optional: [{ sourceId: self._mediaSources.video}] - }; - } - - var screen_constraints = { - audio: false, - video: { - mandatory: { - chromeMediaSource: 'screen' - }, - optional: [] - } - }; - - if (session.screen) { - _captureUserMedia(screen_constraints, constraints.audio || constraints.video ? function () { - _captureUserMedia(constraints, callback); - } : callback); - } else _captureUserMedia(constraints, callback, session.audio && !session.video); - - function _captureUserMedia(forcedConstraints, forcedCallback, isRemoveVideoTracks) { - var mediaConfig = { - onsuccess: function (stream, returnBack, idInstance) { - if (returnBack) return forcedCallback && forcedCallback(stream); - - if (isRemoveVideoTracks && !moz) { - stream = new window.webkitMediaStream(stream.getAudioTracks()); - } - - var mediaElement = getMediaElement(stream, session); - mediaElement.muted = true; - - stream.onended = function () { - self.onstreamended(streamedObject); - - // if user clicks "stop" button to close screen sharing - var _stream = self.streams[streamid]; - if (_stream && _stream.socket) { - _stream.socket.send({ - userid: self.userid, - streamid: _stream.streamid, - stopped: true - }); - } - - // to make sure same stream can be captured again! - if (currentUserMediaRequest.streams[idInstance]) { - delete currentUserMediaRequest.streams[idInstance]; - } - }; - - var streamid = getRandomString(); - - stream.streamid = streamid; - - var streamedObject = { - stream: stream, - streamid: streamid, - mediaElement: mediaElement, - blobURL: mediaElement.mozSrcObject || mediaElement.src, - type: 'local', - userid: self.userid || 'self', - extra: self.extra - }; - - var sObject = { - stream: stream, - userid: self.userid || 'self', - streamid: streamid, - type: 'local', - streamObject: streamedObject, - mediaElement: mediaElement, - rtcMultiConnection: self - }; - - self.attachStreams.push(stream); - self.__attachStreams.push(sObject); - - self.streams[streamid] = self._getStream(sObject); - - self.onstream(streamedObject); - if (forcedCallback) forcedCallback(stream); - - if (self.onspeaking) { - var soundMeter = new SoundMeter({ - context: self._audioContext, - root: self, - event: streamedObject - }); - soundMeter.connectToSource(stream); - } - }, - onerror: function () { - var _error; - - if (session.audio && !session.video) - _error = 'Microphone access is denied.'; - else if (session.screen) { - if (location.protocol !== 'https:') { - _error = ' is mandatory to capture screen.'; - } else { - self.caniuse.checkIfScreenSharingFlagEnabled(function (isFlagEnabled, warning) { - if (isFlagEnabled) { - _error = 'Multi-capturing of screen is not allowed. Capturing process is denied. Try chrome >= M31.'; - } - - if (warning) _error = warning; - else if (!isFlagEnabled) { - _error = 'It seems that "Enable screen capture support in getUserMedia" flag is not enabled.'; - } - }); - } - } else - _error = 'Webcam access is denied.'; - - self.onMediaError(_error); - }, - mediaConstraints: self.mediaConstraints || {} - }; - - mediaConfig.constraints = forcedConstraints || constraints; - mediaConfig.media = self.media; - getUserMedia(mediaConfig); - } - } - - // www.RTCMultiConnection.org/docs/captureUserMedia/ - this.captureUserMedia = captureUserMedia; - - // www.RTCMultiConnection.org/docs/leave/ - this.leave = function (userid) { - // eject a user; or leave the session - rtcSession.leave(userid); - - if (!userid) { - var streams = self.attachStreams; - for (var i = 0; i < streams.length; i++) { - stopTracks(streams[i]); - } - currentUserMediaRequest.streams = []; - self.attachStreams = []; - } - - // if firebase; remove data from firebase servers - if (self.isInitiator && !!self.socket && !!self.socket.remove) { - self.socket.remove(); - } - }; - - // www.RTCMultiConnection.org/docs/eject/ - this.eject = function (userid) { - if (!connection.isInitiator) throw 'Only session-initiator can eject a user.'; - this.leave(userid); - }; - - // www.RTCMultiConnection.org/docs/close/ - this.close = function () { - // close entire session - self.autoCloseEntireSession = true; - rtcSession.leave(); - }; - - // www.RTCMultiConnection.org/docs/addStream/ - this.addStream = function (session, socket) { - // www.RTCMultiConnection.org/docs/renegotiation/ - - // renegotiate new media stream - if (session) { - var isOneWayStreamFromParticipant; - if (!self.isInitiator && session.oneway) { - session.oneway = false; - isOneWayStreamFromParticipant = true; - } - - captureUserMedia(function (stream) { - if (isOneWayStreamFromParticipant) { - session.oneway = true; - } - addStream(stream); - }, session); - } else addStream(); - - function addStream(stream) { - rtcSession.addStream({ - stream: stream, - renegotiate: session || self.session, - socket: socket - }); - } - }; - - // www.RTCMultiConnection.org/docs/removeStream/ - this.removeStream = function (streamid) { - // detach pre-attached streams - if (!this.streams[streamid]) return warn('No such stream exists. Stream-id:', streamid); - - // www.RTCMultiConnection.org/docs/detachStreams/ - this.detachStreams.push(streamid); - }; - - // set RTCMultiConnection defaults on constructor invocation - setDefaults(this); - - this.__attachStreams = []; - }; - - function RTCMultiSession(root) { - var config = root.config; - var session = root.session; - - var self = {}; - var socketObjects = {}; - var sockets = []; - - self.userid = root.userid = root.userid || root.token(); - self.sessionid = root.channel; - - var participants = {}; - var isAcceptNewSession = true; - - function updateSocketForLocalStreams(socket) { - for (var i = 0; i < root.__attachStreams.length; i++) { - var streamid = root.__attachStreams[i].streamid; - if (root.streams[streamid]) root.streams[streamid].socket = socket; - } - root.__attachStreams = []; - } - - function newPrivateSocket(_config) { - var socketConfig = { - channel: _config.channel, - onmessage: socketResponse, - onopen: function (_socket) { - if (_socket) socket = _socket; - - if (isofferer && !peer) - initPeer(); - - _config.socketIndex = socket.index = sockets.length; - socketObjects[socketConfig.channel] = socket; - sockets[_config.socketIndex] = socket; - - updateSocketForLocalStreams(socket); - } - }; - - socketConfig.callback = function (_socket) { - socket = _socket; - socketConfig.onopen(); - }; - - var socket = root.openSignalingChannel(socketConfig), - isofferer = _config.isofferer, - peer; - - var peerConfig = { - onopen: onChannelOpened, - onICE: function (candidate) { - if (!root.candidates) throw 'ICE candidates are mandatory.'; - if (!root.candidates.host && candidate.candidate.indexOf('typ host') != -1) return; - if (!root.candidates.relay && candidate.candidate.indexOf('relay') != -1) return; - if (!root.candidates.reflexive && candidate.candidate.indexOf('srflx') != -1) return; - - log(candidate.candidate); - socket && socket.send({ - userid: self.userid, - candidate: { - sdpMLineIndex: candidate.sdpMLineIndex, - candidate: JSON.stringify(candidate.candidate) - } - }); - }, - onmessage: function (data) { - config.onmessage({ - data: data, - userid: _config.userid, - extra: _config.extra - }); - }, - onstream: function (stream) { - if (_config.streaminfo) { - var streaminfo = _config.streaminfo.split('----'); - if (streaminfo[0]) { - stream.streamid = streaminfo[0]; - delete streaminfo[0]; - _config.streaminfo = swap(streaminfo).join('----'); - } - } - - var __session = session; - __session.remote = true; - - var mediaElement = getMediaElement(stream, __session); - - _config.stream = stream; - if (mediaElement.tagName.toLowerCase() == 'audio') - mediaElement.addEventListener('play', function () { - setTimeout(function () { - mediaElement.muted = false; - afterRemoteStreamStartedFlowing(mediaElement); - }, 3000); - }, false); - else - waitUntilRemoteStreamStartsFlowing(mediaElement); - }, - - onclose: function (e) { - e.extra = _config.extra; - e.userid = _config.userid; - root.onclose(e); - - // suggested in #71 by "efaj" - if (root.channels[e.userid]) - delete root.channels[e.userid]; - }, - onerror: function (e) { - e.extra = _config.extra; - e.userid = _config.userid; - root.onerror(e); - }, - - attachStreams: root.attachStreams, - iceServers: root.iceServers, - bandwidth: root.bandwidth, - sdpConstraints: root.sdpConstraints || {}, - disableDtlsSrtp: root.disableDtlsSrtp, - preferSCTP: !!root.preferSCTP - }; - - function initPeer(offerSDP) { - if (!offerSDP) - peerConfig.onOfferSDP = function (sdp, streaminfo) { - sendsdp({ - sdp: sdp, - socket: socket, - streaminfo: streaminfo - }); - }; - else { - peerConfig.offerSDP = offerSDP; - peerConfig.onAnswerSDP = function (sdp, streaminfo) { - sendsdp({ - sdp: sdp, - socket: socket, - streaminfo: streaminfo - }); - }; - } - - if (!session.data) peerConfig.onmessage = null; - peerConfig.session = session; - peer = new RTCPeerConnection(peerConfig); - } - - function waitUntilRemoteStreamStartsFlowing(mediaElement) { - if (!(mediaElement.readyState <= HTMLMediaElement.HAVE_CURRENT_DATA - || mediaElement.paused || mediaElement.currentTime <= 0)) { - afterRemoteStreamStartedFlowing(mediaElement); - } else - setTimeout(function () { - waitUntilRemoteStreamStartsFlowing(mediaElement); - }, 50); - } - - function afterRemoteStreamStartedFlowing(mediaElement) { - var stream = _config.stream; - - stream.onended = function () { - root.onstreamended(streamedObject); - }; - - var streamedObject = { - mediaElement: mediaElement, - - stream: stream, - streamid: stream.streamid, - session: session, - - blobURL: mediaElement.mozSrcObject || mediaElement.src, - type: 'remote', - - extra: _config.extra, - userid: _config.userid - }; - - // connection.streams['stream-id'].mute({audio:true}) - root.streams[stream.streamid] = root._getStream({ - stream: stream, - userid: _config.userid, - streamid: stream.streamid, - socket: socket, - type: 'remote', - streamObject: streamedObject, - mediaElement: mediaElement, - rtcMultiConnection: root - }); - - root.onstream(streamedObject); - - onSessionOpened(); - - if (root.onspeaking) { - var soundMeter = new SoundMeter({ - context: root._audioContext, - root: root, - event: streamedObject - }); - soundMeter.connectToSource(stream); - } - } - - function onChannelOpened(channel) { - _config.channel = channel; - - // connection.channels['user-id'].send(data); - root.channels[_config.userid] = { - channel: _config.channel, - send: function (data) { - root.send(data, this.channel); - } - }; - - root.onopen({ - extra: _config.extra, - userid: _config.userid - }); - - // fetch files from file-queue - for (var q in root.fileQueue) { - root.send(root.fileQueue[q], channel); - } - - if (isData(session)) onSessionOpened(); - } - - function updateSocket() { - if (socket.userid == _config.userid) - return; - - socket.userid = _config.userid; - sockets[_config.socketIndex] = socket; - - // connection.peers['user-id'].addStream({audio:true}) - root.peers[_config.userid] = { - socket: socket, - peer: peer, - userid: _config.userid, - addStream: function (session00) { - root.addStream(session00, this.socket); - } - }; - } - - function onSessionOpened() { - // admin/guest is one-to-one relationship - if (root.userType && root.direction !== 'many-to-many') return; - - // original conferencing infrastructure! - if (!session.oneway && !session.broadcast && root.isInitiator && getLength(participants) > 1 && getLength(participants) <= root.maxParticipantsAllowed) { - defaultSocket.send({ - newParticipant: _config.userid || socket.channel, - userid: self.userid, - extra: _config.extra || {} - }); - } - } - - function socketResponse(response) { - if (response.userid == self.userid) - return; - - if (response.sdp) { - _config.userid = response.userid; - _config.extra = response.extra; - _config.renegotiate = response.renegotiate; - _config.streaminfo = response.streaminfo; - - // to make sure user-id for socket object is set - // even if one-way streaming - updateSocket(); - - sdpInvoker(JSON.parse(response.sdp), response.labels); - } - - if (response.candidate) { - peer && peer.addICE({ - sdpMLineIndex: response.candidate.sdpMLineIndex, - candidate: JSON.parse(response.candidate.candidate) - }); - } - - if (response.mute || response.unmute) { - if (response.promptMuteUnmute) { - if (response.mute) root.streams[response.streamid].mute(response.session); - if (response.unmute) root.streams[response.streamid].unmute(response.session); - } else { - if (root.streams[response.streamid]) - response.mediaElement = root.streams[response.streamid].mediaElement; - - if (response.mute) root.onmute(response); - if (response.unmute) root.onunmute(response); - } - } - - // to stop local stream - if (response.stopped) { - if (root.streams[response.streamid]) - response.mediaElement = root.streams[response.streamid].mediaElement; - - root.onstreamended(response); - } - - // to stop remote stream - if (response.promptStreamStop && !root.isInitiator) { - warn('What if initiator invoked stream.stop for remote user?'); - /* - if (root.streams[response.streamid]) { - root.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 (moz) { - var userLeft = response.userid; - for (var stream in root.streams) { - stream = root.streams[stream]; - if (stream.userid == userLeft) { - stopTracks(stream); - stream.stream.onended(stream.streamObject); - } - } - } - - if (peer && peer.connection) { - peer.connection.close(); - peer.connection = null; - } - - if (response.closeEntireSession) { - root.close(); - } else if (socket && response.ejected) { - // if user is ejected; his stream MUST be removed - // from all other users' side - socket.send({ - left: true, - extra: root.extra, - userid: self.userid - }); - - if (sockets[_config.socketIndex]) - delete sockets[_config.socketIndex]; - if (socketObjects[socket.channel]) - delete socketObjects[socket.channel]; - - socket = null; - } - - if (participants[response.userid]) delete participants[response.userid]; - - var obj = { - userid: response.userid, - extra: response.extra - }; - - if (response.closeEntireSession) { - root.onclose(obj); - } else root.onleave(obj); - - if (root.userType) root.busy = false; - } - - // keeping session active even if initiator leaves - if (response.playRoleOfBroadcaster) - setTimeout(function () { - root.dontAttachStream = true; - self.userid = response.userid; - root.open({ - extra: root.extra - }); - sockets = swap(sockets); - root.dontAttachStream = false; - }, 600); - - // if renegotiation process initiated by answerer - if (response.suggestRenegotiation) { - renegotiate = response.renegotiate; - - // detaching old streams - detachMediaStream(root.detachStreams, peer.connection); - - if (isData(renegotiate)) - createOffer(); - else - root.captureUserMedia(function (stream00) { - if (stream00) peer.connection.addStream(stream00); - createOffer(); - }, renegotiate); - - function createOffer() { - peer.recreateOffer(renegotiate, function (sdp, streaminfo) { - sendsdp({ - sdp: sdp, - socket: socket, - renegotiate: response.renegotiate, - labels: root.detachStreams, - streaminfo: streaminfo - }); - root.detachStreams = []; - }); - } - } - } - - var haveSDP = false; - - function sdpInvoker(sdp, labels) { - // in renegotiation process; sometimes same sdp is shared multiple times - if (haveSDP) return; - haveSDP = true; - setTimeout(function () { - haveSDP = false; - }, 5000); - - log('sdpInvoker', sdp.type, sdp.sdp); - - if (isofferer) - return peer.addAnswerSDP(sdp); - if (!_config.renegotiate) - return initPeer(sdp); - - session = _config.renegotiate; - // detach streams - detachMediaStream(labels, peer.connection); - - if (session.oneway || isData(session)) { - createAnswer(); - } else { - if (_config.capturing) - return; - - _config.capturing = true; - - root.captureUserMedia(function (stream) { - _config.capturing = false; - - peer.connection.addStream(stream); - createAnswer(); - }, _config.renegotiate); - } - - delete _config.renegotiate; - - function createAnswer() { - peer.recreateAnswer(sdp, session, function (_sdp, streaminfo) { - sendsdp({ - sdp: _sdp, - socket: socket, - streaminfo: streaminfo - }); - }); - } - } - } - - function detachMediaStream(labels, peer) { - if (!labels) return; - for (var i = 0; i < labels.length; i++) { - var label = labels[i]; - if (root.streams[label]) { - peer.removeStream(root.streams[label].stream); - } - } - } - - function sendsdp(e) { - e.socket.send({ - userid: self.userid, - sdp: JSON.stringify(e.sdp), - extra: root.extra, - renegotiate: !!e.renegotiate ? e.renegotiate : false, - streaminfo: e.streaminfo || '', - labels: e.labels || [] - }); - } - - // sharing new user with existing participants - - function onNewParticipant(channel, extra) { - if (!channel || !!participants[channel] || channel == self.userid) - return; - - participants[channel] = channel; - - var new_channel = root.token(); - newPrivateSocket({ - channel: new_channel, - extra: extra || {} - }); - - defaultSocket.send({ - participant: true, - userid: self.userid, - targetUser: channel, - channel: new_channel, - extra: root.extra - }); - } - - // if a user leaves - - function clearSession(channel) { - var alert = { - left: true, - extra: root.extra, - userid: self.userid, - sessionid: self.sessionid - }; - - if (root.isInitiator) { - if (root.autoCloseEntireSession) { - alert.closeEntireSession = true; - } else if (sockets[0]) { - sockets[0].send({ - playRoleOfBroadcaster: true, - userid: self.userid - }); - } - } - - if (!channel) { - var length = sockets.length; - for (var i = 0; i < length; i++) { - socket = sockets[i]; - if (socket) { - socket.send(alert); - - if (socketObjects[socket.channel]) - delete socketObjects[socket.channel]; - - delete sockets[i]; - } - } - } - - // eject a specific user! - if (channel) { - socket = socketObjects[channel]; - if (socket) { - alert.ejected = true; - socket.send(alert); - - if (sockets[socket.index]) - delete sockets[socket.index]; - - delete socketObjects[channel]; - } - } - - sockets = swap(sockets); - } - - // www.RTCMultiConnection.org/docs/remove/ - root.remove = function (userid) { - if (root.peers[userid]) { - root.peers[userid].peer.connection.close(); - root.peers[userid].peer.connection = null; - delete root.peers[userid]; - } - if (participants[userid]) { - delete participants[userid]; - } - for (var stream in root.streams) { - stream = root.streams[stream]; - if (stream.userid == userid) { - root.onstreamended(stream.streamObject); - if (stream.stop) stream.stop(); - delete root.streams[stream]; - } - } - if (socketObjects[userid]) { - delete socketObjects[userid]; - } - }; - - // www.RTCMultiConnection.org/docs/refresh/ - root.refresh = function () { - participants = []; - root.joinedARoom = self.joinedARoom = false; - isAcceptNewSession = true; - root.busy = false; - - // to stop/remove self streams - for (var i = 0; i < root.attachStreams.length; i++) { - stopTracks(root.attachStreams[i]); - } - root.attachStreams = []; - - // to allow capturing of identical streams - currentUserMediaRequest = { - streams: [], - mutex: false, - queueRequests: [] - }; - that.isOwnerLeaving = true; - root.isInitiator = false; - }; - - // www.RTCMultiConnection.org/docs/reject/ - root.reject = function (userid) { - if (typeof userid != 'string') userid = userid.userid; - defaultSocket.send({ - rejectedRequestOf: userid, - userid: root.userid, - extra: root.extra || {} - }); - }; - - window.addEventListener('beforeunload', function () { - clearSession(); - }, false); - - window.addEventListener('keyup', function (e) { - if (e.keyCode == 116) - clearSession(); - }, false); - - function initDefaultSocket() { - defaultSocket = root.openSignalingChannel({ - onmessage: function (response) { - if (response.userid == self.userid) return; - - if (isAcceptNewSession && response.sessionid && response.userid) { - root.session = session = response.session; - config.onNewSession(response); - } - - if (response.newParticipant && self.joinedARoom && self.broadcasterid === response.userid) { - onNewParticipant(response.newParticipant, response.extra); - } - - if (getLength(participants) < root.maxParticipantsAllowed && response.userid && response.targetUser == self.userid && response.participant && !participants[response.userid]) { - acceptRequest(response.channel || response.userid, response.extra, response.userid); - } - - if (response.userType && response.userType != root.userType) { - if (!root.busy) { - if (response.userType == 'admin') { - if (root.onAdmin) root.onAdmin(response); - else root.accept(response.userid); - } - if (response.userType == 'guest') { - if (root.onGuest) root.onGuest(response); - else root.accept(response.userid); - } - } else { - if (response.userType != root.userType) { - root.reject(response.userid); - } - } - } - - if (response.acceptedRequestOf == self.userid) { - if (root.onstats) root.onstats('accepted', response); - } - - if (response.rejectedRequestOf == self.userid) { - if (root.onstats) root.onstats(root.userType ? 'busy' : 'rejected', response); - sendRequest(); - } - - if (response.customMessage) { - if (response.message.drop) { - root.ondrop(); - - root.attachStreams = []; - // "drop" should detach all local streams - for (var stream in root.streams) { - if (root._skip.indexOf(stream) == -1) { - stream = root.streams[stream]; - if (stream.type == 'local') { - root.detachStreams.push(stream.streamid); - } else root.onstreamended(stream.streamObject); - } - } - - // renegotiate; so "peer.removeStream" happens. - root.addStream(); - } else if (root.onCustomMessage) - root.onCustomMessage(response.message); - } - }, - callback: function (socket) { - if (socket) defaultSocket = socket; - if (root.userType) sendRequest(socket || defaultSocket); - }, - onopen: function (socket) { - if (socket) defaultSocket = socket; - if (root.userType) sendRequest(socket || defaultSocket); - } - }); - } - - var that = this, defaultSocket; - - initDefaultSocket(); - - function sendRequest(socket) { - if (!socket) { - return setTimeout(function () { - sendRequest(defaultSocket); - }, 1000); - } - - socket.send({ - userType: root.userType, - userid: root.userid, - extra: root.extra || {} - }); - } - - function setDirections() { - if (root.direction == 'one-way') root.session.oneway = true; - if (root.direction == 'one-to-one') root.maxParticipantsAllowed = 1; - if (root.direction == 'one-to-many') root.session.broadcast = true; - if (root.direction == 'many-to-many') { - root.maxParticipantsAllowed = 256; - } - } - - // open new session - this.initSession = function () { - that.isOwnerLeaving = false; - root.isInitiator = true; - - setDirections(); - session = root.session; - participants = {}; - - self.sessionid = root.sessionid || root.channel; - - this.isOwnerLeaving = isAcceptNewSession = false; - self.joinedARoom = true; - - (function transmit() { - if (getLength(participants) < root.maxParticipantsAllowed && !that.isOwnerLeaving) { - defaultSocket && defaultSocket.send({ - sessionid: self.sessionid, - userid: root.userid, - session: session, - extra: root.extra - }); - } - - if (!root.transmitRoomOnce && !that.isOwnerLeaving) - setTimeout(transmit, root.interval || 3000); - })(); - }; - - // join existing session - this.joinSession = function (_config) { - _config = _config || {}; - participants = {}; - session = _config.session; - self.broadcasterid = _config.userid; - - if (_config.sessionid) - self.sessionid = _config.sessionid; - - isAcceptNewSession = false; - self.joinedARoom = true; - - var channel = getRandomString(); - newPrivateSocket({ - channel: channel, - extra: root.extra - }); - - defaultSocket.send({ - participant: true, - userid: self.userid, - channel: channel, - targetUser: _config.userid, - extra: root.extra - }); - }; - - // send file/data or text message - this.send = function (message, _channel) { - message = JSON.stringify(message); - - if (_channel) { - if (_channel.readyState == 'open') { - _channel.send(message); - } - return; - } - - for (var dataChannel in root.channels) { - var channel = root.channels[dataChannel].channel; - if (channel.readyState == 'open') { - channel.send(message); - } - } - }; - - // leave session - this.leave = function (userid) { - clearSession(userid); - - if (root.isInitiator) { - that.isOwnerLeaving = true; - root.isInitiator = false; - } - - // to stop/remove self streams - for (var i = 0; i < root.attachStreams.length; i++) { - stopTracks(root.attachStreams[i]); - } - root.attachStreams = []; - - // to allow capturing of identical streams - currentUserMediaRequest = { - streams: [], - mutex: false, - queueRequests: [] - }; - - if (!userid) { - root.joinedARoom = self.joinedARoom = false; - isAcceptNewSession = true; - } - - root.busy = false; - }; - - // renegotiate new stream - this.addStream = function (e) { - session = e.renegotiate; - - if (e.socket) - addStream(e.socket); - else - for (var i = 0; i < sockets.length; i++) - addStream(sockets[i]); - - function addStream(socket) { - updateSocketForLocalStreams(socket); - - peer = root.peers[socket.userid]; - - if (!peer) - throw 'No such peer exists.'; - - peer = peer.peer; - - // if offerer; renegotiate - if (peer && peer.connection.localDescription.type == 'offer') { - // detaching old streams - detachMediaStream(root.detachStreams, peer.connection); - - if (session.audio || session.video || session.screen) - peer.connection.addStream(e.stream); - - peer.recreateOffer(session, function (sdp, streaminfo) { - sendsdp({ - sdp: sdp, - socket: socket, - renegotiate: session, - labels: root.detachStreams, - streaminfo: streaminfo - }); - root.detachStreams = []; - }); - } else { - // otherwise; suggest other user to play role of re-negotiator - socket.send({ - userid: self.userid, - renegotiate: session, - suggestRenegotiation: true - }); - } - } - }; - - root.request = function (userid) { - if (root.direction === 'many-to-many') root.busy = true; - - root.captureUserMedia(function () { - // open private socket that will be used to receive offer-sdp - newPrivateSocket({ - channel: self.userid, - extra: root.extra || {} - }); - - // ask other user to create offer-sdp - defaultSocket.send({ - participant: true, - userid: self.userid, - extra: root.extra || {}, - targetUser: userid - }); - }); - }; - - function acceptRequest(channel, extra, userid) { - if (!self.requestsFrom) self.requestsFrom = {}; - if (root.busy || self.requestsFrom[userid]) return; - - var obj = { - userid: userid, - extra: extra, - channel: channel - }; - - self.requestsFrom[userid] = obj; - - // www.RTCMultiConnection.org/docs/onRequest/ - if (root.onRequest) { - root.onRequest(obj); - } else _accept(obj); - } - - function _accept(e) { - if (root.userType) { - if (root.direction === 'many-to-many') root.busy = true; - defaultSocket.send({ - acceptedRequestOf: e.userid, - userid: self.userid, - extra: root.extra || {} - }); - } - - participants[e.userid] = e.userid; - newPrivateSocket({ - isofferer: true, - userid: e.userid, - channel: e.channel, - extra: e.extra || {} - }); - } - - // www.RTCMultiConnection.org/docs/sendMessage/ - root.sendCustomMessage = function (message) { - if (!defaultSocket) { - return setTimeout(function () { - root.sendMessage(message); - }, 1000); - } - - defaultSocket.send({ - userid: root.userid, - customMessage: true, - message: message - }); - }; - - // www.RTCMultiConnection.org/docs/accept/ - root.accept = function (e) { - - // for backward compatibility - if (arguments.length > 1 && typeof arguments[0] == 'string') { - e = {}; - if (arguments[0]) e.userid = arguments[0]; - if (arguments[1]) e.extra = arguments[1]; - if (arguments[2]) e.channel = arguments[2]; - } - - root.captureUserMedia(function () { - _accept(e); - }); - }; - } - - function getRandomString() { - return (Math.random() * new Date().getTime()).toString(36).toUpperCase().replace(/\./g, '-'); - } - - var FileSender = { - send: function (config) { - var root = config.root; - var channel = config.channel; - var privateChannel = config._channel; - var file = config.file; - - if (!config.file) { - console.error('You must attach/select a file.'); - return; - } - - // max chunk sending limit on chrome is 64k - // max chunk receiving limit on firefox is 16k - var packetSize = (!!navigator.mozGetUserMedia || root.preferSCTP) ? 15 * 1000 : 1 * 1000; - - var textToTransfer = ''; - var numberOfPackets = 0; - var packets = 0; - - file.uuid = getRandomString(); - - function processInWebWorker(_function) { - var blob = URL.createObjectURL(new Blob(['', - _function.toString(), - 'this.onmessage = function (e) {readFile(e.data);}'], { - type: 'application/javascript' - })); - - var worker = new Worker(blob); - URL.revokeObjectURL(blob); - return worker; - } - - var webWorker = processInWebWorker(function readFile(_file) { - var reader = new FileReaderSync(); - postMessage(reader.readAsDataURL(_file)); - }); - - webWorker.onmessage = function (event) { - onReadAsDataURL(event.data); - }; - - webWorker.postMessage(file); - - function onReadAsDataURL(dataURL, text) { - var data = { - type: 'file', - uuid: file.uuid, - maxChunks: numberOfPackets, - currentPosition: numberOfPackets - packets, - name: file.name, - filType: file.type, - size: file.size - }; - - if (dataURL) { - text = dataURL; - numberOfPackets = packets = data.packets = parseInt(text.length / packetSize); - - file.maxChunks = data.maxChunks = numberOfPackets; - data.currentPosition = numberOfPackets - packets; - - root.onFileStart(file); - } - - root.onFileProgress({ - remaining: packets--, - length: numberOfPackets, - sent: numberOfPackets - packets, - - maxChunks: numberOfPackets, - uuid: file.uuid, - currentPosition: numberOfPackets - packets - }, file.uuid); - - if (text.length > packetSize) data.message = text.slice(0, packetSize); - else { - data.message = text; - data.last = true; - data.name = file.name; - - file.url = URL.createObjectURL(file); - root.onFileEnd(file); - } - - channel.send(data, privateChannel); - - textToTransfer = text.slice(data.message.length); - if (textToTransfer.length) { - setTimeout(function () { - onReadAsDataURL(null, textToTransfer); - }, (!!navigator.mozGetUserMedia || root.preferSCTP) ? 100 : 500); - } - } - } - }; - - function FileReceiver(root) { - var content = {}, - packets = {}, - numberOfPackets = {}; - - function receive(data) { - var uuid = data.uuid; - - if (typeof data.packets !== 'undefined') { - numberOfPackets[uuid] = packets[uuid] = parseInt(data.packets); - root.onFileStart(data); - } - - root.onFileProgress({ - remaining: packets[uuid]--, - length: numberOfPackets[uuid], - received: numberOfPackets[uuid] - packets[uuid], - - maxChunks: numberOfPackets[uuid], - uuid: uuid, - currentPosition: numberOfPackets[uuid] - packets[uuid] - }, uuid); - - if (!content[uuid]) content[uuid] = []; - - content[uuid].push(data.message); - - // if it is last packet - if (data.last) { - var dataURL = content[uuid].join(''); - - FileConverter.DataURLToBlob(dataURL, function (blob) { - blob.uuid = uuid; - blob.name = data.name; - blob.type = data.filType; - - blob.url = (window.URL || window.webkitURL).createObjectURL(blob); - - if (root.autoSaveToDisk) { - FileSaver.SaveToDisk(blob.url, data.name); - } - - root.onFileEnd(blob); - - delete content[uuid]; - }); - } - } - - return { - receive: receive - }; - } - - var FileSaver = { - SaveToDisk: function (fileUrl, fileName) { - var hyperlink = document.createElement('a'); - hyperlink.href = fileUrl; - hyperlink.target = '_blank'; - hyperlink.download = fileName || fileUrl; - - var mouseEvent = new MouseEvent('click', { - view: window, - bubbles: true, - cancelable: true - }); - - hyperlink.dispatchEvent(mouseEvent); - (window.URL || window.webkitURL).revokeObjectURL(hyperlink.href); - } - }; - - var FileConverter = { - DataURLToBlob: function (dataURL, callback) { - function processInWebWorker(_function) { - var blob = URL.createObjectURL(new Blob(['', - _function.toString(), - 'this.onmessage = function (e) {getBlob(e.data);}'], { - type: 'application/javascript' - })); - - var worker = new Worker(blob); - URL.revokeObjectURL(blob); - return worker; - } - - var webWorker = processInWebWorker(function getBlob(_dataURL) { - var binary = atob(_dataURL.substr(_dataURL.indexOf(',') + 1)), - i = binary.length, - view = new Uint8Array(i); - - while (i--) { - view[i] = binary.charCodeAt(i); - } - - postMessage(new Blob([view])); - }); - - webWorker.onmessage = function (event) { - callback(event.data); - }; - - webWorker.postMessage(dataURL); - } - }; - - // _____________ - // TextSender.js - var TextSender = { - send: function (config) { - var channel = config.channel, - _channel = config._channel, - initialText = config.text, - packetSize = 1000, - textToTransfer = '', - isobject = false; - - if (typeof initialText !== 'string') { - 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) { - if (config.preferSCTP || moz) { - setTimeout(function () { - sendText(null, textToTransfer); - }, 100); - } else - setTimeout(function () { - sendText(null, textToTransfer); - }, 500); - } - } - } - }; - - // _______________ - // TextReceiver.js - - function TextReceiver() { - var content = {}; - - function receive(data, onmessage, 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; - - if (onmessage) - onmessage({ - data: message, - userid: userid, - extra: extra, - latency: latency - }); - - delete content[uuid]; - } - } - - return { - receive: receive - }; - } - - window.MediaStream = window.MediaStream || window.webkitMediaStream; - window.AudioContext = window.AudioContext || window.webkitAudioContext; - - window.moz = !!navigator.mozGetUserMedia; - var RTCPeerConnection = function (options) { - var w = window, - PeerConnection = w.mozRTCPeerConnection || w.webkitRTCPeerConnection, - SessionDescription = w.mozRTCSessionDescription || w.RTCSessionDescription, - IceCandidate = w.mozRTCIceCandidate || w.RTCIceCandidate; - - // protocol: 'text/chat', preset: true, stream: 16 - var dataChannelDict = {}, i; - - var iceServers = []; - - if (!moz) iceServers.push({ url: 'stun:stun.l.google.com:19302' }); - - if (!moz && parseInt(navigator.userAgent.match(/Chrom(e|ium)\/([0-9]+)\./)[2]) >= 28) { - iceServers.push({ - url: 'turn:turn.bistri.com:80', - credential: 'homeo', - username: 'homeo' - }); - - iceServers.push({ - url: 'turn:turn.anyfirewall.com:443?transport=tcp', - credential: 'webrtc', - username: 'webrtc' - }); - } - - iceServers.push({ url: 'stun:23.21.150.121:3478' }); - - if (options.iceServers) iceServers = options.iceServers; - - iceServers = { - iceServers: iceServers - }; - - var optional = { - optional: [] - }; - - if (!moz) { - optional.optional = [{ - DtlsSrtpKeyAgreement: true - }]; - - if (options.disableDtlsSrtp) - optional = { - optional: [] - }; - - if (options.onmessage && !options.preferSCTP) - optional.optional = [{ - RtpDataChannels: true - }]; - } - - // local/host candidates can also be used for peer connection - if (!navigator.onLine) { - iceServers = null; - warn('No internet connection detected. No STUN/TURN server is used to make sure local/host candidates are used for peers connection.'); - } else - log('iceServers', JSON.stringify(iceServers, null, '\t')); - - var peer = new PeerConnection(iceServers, optional); - - openOffererChannel(); - - peer.onicecandidate = function (event) { - if (event.candidate) - options.onICE(event.candidate); - }; - - // adding media streams to the PeerConnection - if (options.attachStreams && options.attachStreams.length) { - var streams = options.attachStreams; - for (i = 0; i < streams.length; i++) { - peer.addStream(streams[i]); - } - } - - peer.onaddstream = function (event) { - log('on:add:stream', event.stream); - - if (!event || !options.onstream) return; - - options.onstream(event.stream); - options.renegotiate = false; - }; - - peer.onsignalingstatechange = function () { - log('onsignalingstatechange:', toStr({ - iceGatheringState: peer.iceGatheringState, - signalingState: peer.signalingState - })); - }; - peer.oniceconnectionstatechange = function () { - log('oniceconnectionstatechange:', toStr({ - iceGatheringState: peer.iceGatheringState, - signalingState: peer.signalingState - })); - }; - - peer.onremoveStream = function (event) { - warn('on:remove:stream', toStr(event)); - }; - - peer.onconnecting = function (event) { - log('on:connecting', toStr(event)); - }; - - peer.onnegotiationneeded = function (event) { - warn('on:negotiation:needed', toStr(event)); - }; - - var constraints; - - function setConstraints() { - var session = options.session; - - var sdpConstraints = options.sdpConstraints; - constraints = options.constraints || { - optional: [], - mandatory: { - OfferToReceiveAudio: !!session.audio, - OfferToReceiveVideo: !!session.video || !!session.screen - } - }; - - if (sdpConstraints.mandatory) - constraints.mandatory = merge(constraints.mandatory, sdpConstraints.mandatory); - - if (sdpConstraints.optional) - constraints.optional[0] = merge({}, sdpConstraints.optional); - - log('sdp constraints', JSON.stringify(constraints, null, '\t')); - } - - setConstraints(); - - var streaminfo = ''; - for (i = 0; i < options.attachStreams.length; i++) { - var stream = options.attachStreams[i]; - - if (i == 0) streaminfo = stream.streamid; - else - streaminfo += '----' + stream.streamid; - } - - function createOffer() { - if (!options.onOfferSDP) - return; - - function onCreatingOffer(sessionDescription) { - sessionDescription.sdp = setBandwidth(sessionDescription.sdp); - peer.setLocalDescription(sessionDescription); - - options.onOfferSDP(sessionDescription, streaminfo); - } - - if (!moz && options.preferSCTP) peer.createOffer(onCreatingOffer); - else peer.createOffer(onCreatingOffer, onSdpError, constraints); - } - - function createAnswer() { - if (!options.onAnswerSDP) - return; - - options.offerSDP = new SessionDescription(options.offerSDP, onSdpSuccess, onSdpError); - peer.setRemoteDescription(options.offerSDP); - - function onCreatingAnswer(sessionDescription) { - sessionDescription.sdp = setBandwidth(sessionDescription.sdp); - peer.setLocalDescription(sessionDescription); - options.onAnswerSDP(sessionDescription, streaminfo); - } - - if (!moz && options.preferSCTP) peer.createAnswer(onCreatingAnswer); - else peer.createAnswer(onCreatingAnswer, onSdpError, constraints); - } - - createOffer(); - createAnswer(); - - if (options.preferSCTP || (options.onAnswerSDP && options.onmessage && moz)) - openAnswererChannel(); - - var bandwidth = options.bandwidth; - - function setBandwidth(sdp) { - if (!bandwidth || moz) return sdp; - - // remove existing bandwidth lines - sdp = sdp.replace(/b=AS([^\r\n]+\r\n)/g, ''); - - if (bandwidth.audio) { - sdp = sdp.replace(/a=mid:audio\r\n/g, 'a=mid:audio\r\nb=AS:' + bandwidth.audio + '\r\n'); - } - - if (bandwidth.video) { - sdp = sdp.replace(/a=mid:video\r\n/g, 'a=mid:video\r\nb=AS:' + bandwidth.video + '\r\n'); - } - - if (bandwidth.data && !options.preferSCTP) { - sdp = sdp.replace(/a=mid:data\r\n/g, 'a=mid:data\r\nb=AS:' + bandwidth.data + '\r\n'); - } - - return sdp; - } - - var channel; - - function openOffererChannel() { - if (!options.onmessage || (moz && !options.onOfferSDP)) - return; - - _openOffererChannel(); - - if (!moz) return; - navigator.mozGetUserMedia({ - audio: true, - fake: true - }, function (mozStream) { - peer.addStream(mozStream); - }, useless); - } - - function _openOffererChannel() { - if (!options.preferSCTP) dataChannelDict.reliable = false; - - channel = peer.createDataChannel(options.channel || 'data-channel', dataChannelDict); - setChannelEvents(); - } - - function setChannelEvents() { - channel.onmessage = function (event) { - options.onmessage(event.data); - }; - channel.onopen = function () { - options.onopen(channel); - }; - - channel.onerror = function (e) { - options.onerror(e); - }; - - channel.onclose = function (e) { - options.onclose(e); - }; - - channel.push = channel.send; - channel.send = function (data) { - try { - channel.push(data); - } catch (e) { - setTimeout(function () { - channel.send(data); - }, 1); - } - }; - } - - function openAnswererChannel() { - peer.ondatachannel = function (event) { - channel = event.channel; - setChannelEvents(); - }; - - if (!moz) return; - navigator.mozGetUserMedia({ - audio: true, - fake: true - }, function (mozStream) { - peer.addStream(mozStream); - }, useless); - } - - function useless() { - log('Error in fake:true'); - } - - function onSdpSuccess() { - } - - function onSdpError(e) { - var message = JSON.stringify(e, null, '\t'); - - if (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); - } - - return { - connection: peer, - addAnswerSDP: function (sdp) { - peer.setRemoteDescription(new SessionDescription(sdp), onSdpSuccess, onSdpError); - }, - addICE: function (candidate) { - peer.addIceCandidate(new IceCandidate({ - sdpMLineIndex: candidate.sdpMLineIndex, - candidate: candidate.candidate - })); - }, - recreateAnswer: function (sdp, session, callback) { - options.renegotiate = true; - - options.session = session; - setConstraints(); - - options.onAnswerSDP = callback; - options.offerSDP = sdp; - createAnswer(); - }, - recreateOffer: function (session, callback) { - options.renegotiate = true; - - options.session = session; - setConstraints(); - - options.onOfferSDP = callback; - createOffer(); - } - }; - }; - - var video_constraints = { - mandatory: {}, - optional: [] - }; - - /* by @FreCap pull request #41 */ - var currentUserMediaRequest = { - streams: [], - mutex: false, - queueRequests: [] - }; - - function getUserMedia(options) { - if (currentUserMediaRequest.mutex === true) { - currentUserMediaRequest.queueRequests.push(options); - return; - } - currentUserMediaRequest.mutex = true; - - // http://tools.ietf.org/html/draft-alvestrand-constraints-resolution-00 - var mediaConstraints = options.mediaConstraints || {}; - var n = navigator, - hints = options.constraints || { - audio: true, - video: video_constraints - }; - - if (hints.video == true) hints.video = video_constraints; - - // connection.mediaConstraints.audio = false; - if (typeof mediaConstraints.audio != 'undefined') - hints.audio = mediaConstraints.audio; - - // connection.media.min(320,180); - // connection.media.max(1920,1080); - var media = options.media; - if (!moz) { - var mandatory = { - minWidth: media.minWidth, - minHeight: media.minHeight, - maxWidth: media.maxWidth, - maxHeight: media.maxHeight, - minAspectRatio: media.minAspectRatio - }; - - // https://code.google.com/p/chromium/issues/detail?id=143631#c9 - 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 (mediaConstraints.mandatory) - hints.video.mandatory = merge(hints.video.mandatory, mediaConstraints.mandatory); - - // mediaConstraints.optional.bandwidth = 1638400; - if (mediaConstraints.optional) - hints.video.optional[0] = merge({}, mediaConstraints.optional); - - log('media hints:', toStr(hints)); - - // easy way to match - var idInstance = JSON.stringify(hints); - - function streaming(stream, returnBack) { - var video = options.video; - if (video) { - video[moz ? 'mozSrcObject' : 'src'] = moz ? stream : window.webkitURL.createObjectURL(stream); - video.play(); - } - - options.onsuccess(stream, returnBack, idInstance); - currentUserMediaRequest.streams[idInstance] = stream; - currentUserMediaRequest.mutex = false; - if (currentUserMediaRequest.queueRequests.length) - getUserMedia(currentUserMediaRequest.queueRequests.shift()); - } - - if (currentUserMediaRequest.streams[idInstance]) { - streaming(currentUserMediaRequest.streams[idInstance], true); - } else { - n.getMedia = n.webkitGetUserMedia || n.mozGetUserMedia; - n.getMedia(hints, streaming, options.onerror || function (e) { - error(toStr(e)); - }); - } - } - - function isData(session) { - return !session.audio && !session.video && !session.screen && session.data; - } - - 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 log() { - if (window.skipRTCMultiConnectionLogs) return; - console.log(Array.prototype.slice.call(arguments).join('\n')); - } - - function error() { - console.error(Array.prototype.slice.call(arguments).join('\n')); - } - - function warn() { - console.warn(Array.prototype.slice.call(arguments).join('\n')); - } - - function toStr(obj) { - return JSON.stringify(obj, function (key, value) { - if (value && value.sdp) { - console.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 getMediaElement(stream, session) { - var isAudio = session.audio && !session.video && !session.screen; - if (!moz && stream.getAudioTracks && stream.getVideoTracks) { - isAudio = stream.getAudioTracks().length && !stream.getVideoTracks().length; - } - - var mediaElement = document.createElement(isAudio ? 'audio' : 'video'); - - // "mozSrcObject" is always preferred over "src"!! - mediaElement[moz ? 'mozSrcObject' : 'src'] = moz ? stream : window.webkitURL.createObjectURL(stream); - - mediaElement.controls = true; - mediaElement.autoplay = !!session.remote; - mediaElement.volume = session.remote ? 1 : 0; - - if (!session.remote) { - mediaElement.muted = true; - } - - mediaElement.play(); - - return mediaElement; - } - - 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; - if (onload) script.onload = onload; - document.documentElement.appendChild(script); - } - - function muteOrUnmute(e) { - var stream = e.stream, - root = e.root, - session = e.session || {}, - enabled = e.enabled; - - if (!session.audio && !session.video) { - session = merge(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('session', JSON.stringify(session, null, '\t')); - - // enable/disable audio/video tracks - - if (session.audio) { - var audioTracks = stream.getAudioTracks()[0]; - if (audioTracks) - audioTracks.enabled = !enabled; - } - - if (session.video) { - var videoTracks = stream.getVideoTracks()[0]; - if (videoTracks) - videoTracks.enabled = !enabled; - } - - // socket message to change media element look - if (root.socket) { - if (root.type == 'local') - root.socket.send({ - userid: root.rtcMultiConnection.userid, - streamid: root.streamid, - mute: !!enabled, - unmute: !enabled, - session: session - }); - - if (root.type == 'remote') - root.socket.send({ - userid: root.rtcMultiConnection.userid, - promptMuteUnmute: true, - streamid: root.streamid, - mute: !!enabled, - unmute: !enabled, - session: session - }); - } - - // According to issue #135, onmute/onumute must be fired for self - root.streamObject.session = session; - if (!!enabled) { - root.rtcMultiConnection.onmute(root.streamObject); - } - - if (!enabled) { - root.rtcMultiConnection.onunmute(root.streamObject); - } - } - - function stopTracks(mediaStream) { - // getAudioTracks is not implemented in firefox - if (moz && mediaStream.stop) { - mediaStream.stop(); - return; - } - - var fallback = false, i; - - // MediaStream.stop should be avoided. It still exist and works but - // it is removed from the spec and instead MediaStreamTrack.stop should be used - var audioTracks = mediaStream.getAudioTracks(); - var videoTracks = mediaStream.getVideoTracks(); - - for (i = 0; i < audioTracks.length; i++) { - if (audioTracks[i].stop) - audioTracks[i].stop(); - else { - fallback = true; - continue; - } - } - - for (i = 0; i < videoTracks.length; i++) { - if (videoTracks[i].stop) - videoTracks[i].stop(); - else { - fallback = true; - continue; - } - } - - if (fallback && mediaStream.stop) mediaStream.stop(); - } - - // Sound meter is used to detect speaker - - function SoundMeter(config) { - var root = config.root; - var context = config.context; - this.context = context; - this.volume = 0.0; - this.slow_volume = 0.0; - this.clip = 0.0; - - // Legal values are (256, 512, 1024, 2048, 4096, 8192, 16384) - this.script = context.createScriptProcessor(256, 1, 1); - that = this; - - this.script.onaudioprocess = function (event) { - var input = event.inputBuffer.getChannelData(0); - var i; - var sum = 0.0; - var clipcount = 0; - for (i = 0; i < input.length; ++i) { - sum += input[i] * input[i]; - if (Math.abs(input[i]) > 0.99) { - clipcount += 1; - } - } - that.volume = Math.sqrt(sum / input.length); - - var volume = that.volume.toFixed(2); - - if (volume >= .1 && root.onspeaking) { - root.onspeaking(config.event); - } - - if (volume < .1 && root.onsilence) { - root.onsilence(config.event); - } - }; - } - - SoundMeter.prototype.connectToSource = function (stream) { - this.mic = this.context.createMediaStreamSource(stream); - this.mic.connect(this.script); - this.script.connect(this.context.destination); - }; - - SoundMeter.prototype.stop = function () { - this.mic.disconnect(); - this.script.disconnect(); - }; - - function setDefaults(connection) { - // 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)); - }; - - var progressHelper = {}; - - // www.RTCMultiConnection.org/docs/body/ - connection.body = document.body; - - // www.RTCMultiConnection.org/docs/autoSaveToDisk/ - // to make sure file-saver dialog is not invoked. - connection.autoSaveToDisk = false; - - // 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) { - warn('Now, "autoSaveToDisk" is false. Read more here: http://www.RTCMultiConnection.org/docs/autoSaveToDisk/'); - 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/onstreamended/ - connection.onstreamended = function (e) { - if (e.mediaElement && e.mediaElement.parentNode) { - e.mediaElement.parentNode.removeChild(e.mediaElement); - } - }; - - // www.RTCMultiConnection.org/docs/onmute/ - connection.onmute = function (e) { - if (e.session.video) { - e.mediaElement.setAttribute('poster', '//www.webrtc-experiment.com/images/muted.png'); - } - }; - - // www.RTCMultiConnection.org/docs/onunmute/ - connection.onunmute = function (e) { - if (e.session.video) { - e.mediaElement.removeAttribute('poster'); - } - }; - - // www.RTCMultiConnection.org/docs/onleave/ - connection.onleave = function (e) { - log('onleave', toStr(e)); - }; - - // www.RTCMultiConnection.org/docs/peers/ - connection.peers = {}; - - connection._skip = ['stop', 'mute', 'unmute', '_private']; - - // 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) { - // implementation from #68 - for (var stream in this) { - if (connection._skip.indexOf(stream) == -1) { - this[stream]._private(session, enabled); - } - } - }, - stop: function (type) { - // connection.streams.stop('local'); - var _stream; - for (var stream in this) { - if (stream != 'stop' && stream != 'mute' && stream != 'unmute' && stream != '_private') { - _stream = this[stream]; - - if (!type) _stream.stop(); - - if (type == 'local' && _stream.type == 'local') - _stream.stop(); - - if (type == 'remote' && _stream.type == 'remote') - _stream.stop(); - } - } - } - }; - - // www.RTCMultiConnection.org/docs/channels/ - connection.channels = {}; - - // www.RTCMultiConnection.org/docs/extra/ - connection.extra = {}; - - // www.RTCMultiConnection.org/docs/session/ - connection.session = { - audio: true, - video: true - }; - - // www.RTCMultiConnection.org/docs/bandwidth/ - connection.bandwidth = { - data: 1638400 - }; - - if (!moz && parseInt(navigator.userAgent.match(/Chrom(e|ium)\/([0-9]+)\./)[2]) >= 28) { - connection.bandwidth.audio = 80; - connection.bandwidth.video = 2048; - - warn('bandwidth', toStr(connection.bandwidth)); - } - - // www.RTCMultiConnection.org/docs/preferSCTP/ - connection.preferSCTP = false; // preferring SCTP data channels! - - // file queue: to store previous file objects in memory; - // and stream over newly connected peers - // www.RTCMultiConnection.org/docs/fileQueue/ - connection.fileQueue = {}; - - // www.RTCMultiConnection.org/docs/media/ - connection.media = { - min: function (width, height) { - this.minWidth = width; - this.minHeight = height; - }, - minWidth: 640, // 1920 - minHeight: 360, // 1080 - max: function (width, height) { - this.maxWidth = width; - this.maxHeight = height; - }, - maxWidth: 1920, - maxHeight: 1080, - bandwidth: 256, - minFrameRate: 32, - minAspectRatio: 1.77 - }; - - // www.RTCMultiConnection.org/docs/candidates/ - connection.candidates = { - host: true, - relay: true, - reflexive: true - }; - - // www.RTCMultiConnection.org/docs/mediaConstraints/ - connection.mediaConstraints = {}; - - // www.RTCMultiConnection.org/docs/sdpConstraints/ - connection.sdpConstraints = {}; - - // www.RTCMultiConnection.org/docs/attachStreams/ - connection.attachStreams = []; - - // www.RTCMultiConnection.org/docs/detachStreams/ - connection.detachStreams = []; - - // 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'; - - connection._getStream = function (e) { - return { - rtcMultiConnection: e.rtcMultiConnection, - streamObject: e.streamObject, - stream: e.stream, - userid: e.userid, - streamid: e.streamid, - socket: e.socket, - type: e.type, - mediaElement: e.mediaElement, - stop: function () { - if (this.socket) { - if (this.type == 'local') - this.socket.send({ - userid: this.rtcMultiConnection.userid, - streamid: this.streamid, - stopped: true - }); - - if (this.type == 'remote') - this.socket.send({ - userid: this.rtcMultiConnection.userid, - promptStreamStop: true, - streamid: this.streamid - }); - } - - var stream = this.stream; - if (stream && stream.stop) { - stopTracks(stream); - } - }, - mute: function (session) { - this._private(session, true); - }, - unmute: function (session) { - this._private(session, false); - }, - _private: function (session, enabled) { - muteOrUnmute({ - root: this, - session: session, - enabled: enabled, - stream: this.stream - }); - }, - startRecording: function (session) { - if (!session) session = { audio: true, video: true }; - if (!window.RecordRTC) { - var self = this; - return loadScript('//www.webrtc-experiment.com/RecordRTC.js', function () { - self.startRecording(session); - }); - } - - if (session.audio && session.video) { - // to synchronize audio/video - session.bufferSize = 2048; - } - - var stream = this.stream; - if (session.audio) { - this.recordAudio = RecordRTC(stream, session); - this.recordAudio.startRecording(); - } - - // video recording on firefox has some issues - if (!moz && session.video) { - this.recordVideo = RecordRTC(stream, merge(session, { - type: moz ? 'gif' : 'video' - })); - this.recordVideo.startRecording(); - } - }, - stopRecording: function (callback, session) { - if (!session) session = { audio: true, video: true }; - else if (typeof session == 'string') { - session = { - audio: session == 'audio', - video: session == 'video' - }; - } - - var audioBlob, videoBlob; - - if (session.audio && this.recordAudio) { - this.recordAudio.stopRecording(); - - audioBlob = this.recordAudio.getBlob(); - audioBlob.recordingType = 'audio'; - } - - if (/*!moz &&*/session.video && this.recordVideo) { - this.recordVideo.stopRecording(); - - videoBlob = this.recordVideo.getBlob(); - videoBlob.recordingType = 'video'; - } - - if (session.audio && session.video && callback) { - callback(audioBlob, videoBlob); - } else if (callback) callback(audioBlob || videoBlob); - } - }; - }; - - connection.token = function () { - return (Math.random() * new Date().getTime()).toString(36).replace(/\./g, ''); - }; - - // new RTCMultiConnection().set({override}).connect() - connection.set = function (properties) { - for (var property in properties) { - this[property] = properties[property]; - } - return this; - }; - - // www.RTCMultiConnection.org/docs/firebase/ - connection.firebase = 'chat'; - - // www.RTCMultiConnection.org/docs/onMediaError/ - connection.onMediaError = function (_error) { - alert(_error); - }; - - // www.RTCMultiConnection.org/docs/stats/ - connection.stats = {}; - - // www.RTCMultiConnection.org/docs/getStats/ - connection.getStats = function (callback) { - var numberOfConnectedUsers = 0; - for (var peer in this.peers) { - numberOfConnectedUsers++; - } - - this.stats.numberOfConnectedUsers = numberOfConnectedUsers; - - // numberOfSessions - - if (callback) callback(this.stats); - }; - - // www.RTCMultiConnection.org/docs/caniuse/ - connection.caniuse = { - RTCPeerConnection: !!RTCPeerConnection, - getUserMedia: !!getUserMedia, - AudioContext: !!AudioContext, - - // there is no way to check whether "getUserMedia" flag is enabled or not! - ScreenSharing: !moz && parseInt(navigator.userAgent.match(/Chrome\/(.*) /)[1], 10) >= 26 && location.protocol == 'https:', - checkIfScreenSharingFlagEnabled: function (callback) { - var warning; - if (moz) { - warning = 'Screen sharing is NOT supported on Firefox.'; - error(warning); - if (callback) callback(false); - } - - if (location.protocol !== 'https:') { - warning = 'Screen sharing is NOT supported on ' + location.protocol + ' Try https!'; - error(warning); - if (callback) return callback(false); - } - - if (parseInt(navigator.userAgent.match(/Chrome\/(.*) /)[1], 10) < 26) { - warning = 'Screen sharing support is suspicious!'; - warn(warning); - } - - var screen_constraints = { - video: { - mandatory: { - chromeMediaSource: 'screen' - } - } - }; - - var invocationInterval = 0, stop; - (function selfInvoker() { - invocationInterval++; - if (!stop) setTimeout(selfInvoker, 10); - })(); - - navigator.webkitGetUserMedia(screen_constraints, onsuccess, onfailure); - - function onsuccess(stream) { - if (stream.stop) { - stream.stop(); - } - - if (callback) { - callback(true); - } - } - - function onfailure() { - stop = true; - if (callback) callback(invocationInterval > 5, warning); - } - }, - - RtpDataChannels: !moz && parseInt(navigator.userAgent.match(/Chrome\/(.*) /)[1], 10) >= 25, - SctpDataChannels: !moz && parseInt(navigator.userAgent.match(/Chrome\/(.*) /)[1], 10) >= 31 - }; - - // www.RTCMultiConnection.org/docs/snapshots/ - connection.snapshots = {}; - - // www.RTCMultiConnection.org/docs/takeSnapshot/ - connection.takeSnapshot = function (userid, callback) { - for (var stream in this.streams) { - stream = this.streams[stream]; - if (stream.userid == userid) { - var video = stream.streamObject.mediaElement; - 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); - - this.snapshots[userid] = canvas.toDataURL(); - callback && callback(this.snapshots[userid]); - continue; - } - } - }; - - // www.WebRTC-Experiment.com/demos/MediaStreamTrack.getSources.html - connection._mediaSources = {}; - - // 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/devices/ - connection.devices = {}; - - // www.RTCMultiConnection.org/docs/getDevices/ - connection.getDevices = function (callback) { - if (!!window.MediaStreamTrack && !!MediaStreamTrack.getSources) { - MediaStreamTrack.getSources(function (media_sources) { - var sources = []; - for (var i = 0; i < media_sources.length; i++) { - sources.push(media_sources[i]); - } - - getAllUserMedias(sources); - - if (callback) callback(connection.devices); - }); - - var index = 0; - - function getAllUserMedias(media_sources) { - var media_source = media_sources[index]; - if (!media_source) return; - - connection.devices[media_source.id] = media_source; - - index++; - getAllUserMedias(media_sources); - } - } - }; - - // www.RTCMultiConnection.org/docs/onCustomMessage/ - connection.onCustomMessage = function (message) { - log('Custom message', message); - }; - - // www.RTCMultiConnection.org/docs/ondrop/ - connection.ondrop = function () { - log('Connection is dropped!'); - }; - - // www.RTCMultiConnection.org/docs/drop/ - connection.drop = function () { - this.attachStreams = []; - - // "drop" should detach all local streams - for (var stream in this.streams) { - if (this._skip.indexOf(stream) == -1) { - stream = this.streams[stream]; - if (stream.type == 'local') { - this.detachStreams.push(stream.streamid); - } else this.onstreamended(stream.streamObject); - } - } - - // www.RTCMultiConnection.org/docs/sendCustomMessage/ - this.sendCustomMessage({ - drop: true - }); - }; - - // used for SoundMeter - if (!!window.AudioContext) { - connection._audioContext = new AudioContext(); - } - } -})(); diff --git a/RTCMultiConnection/Library/RTCMultiConnection-v1.6.js b/RTCMultiConnection/Library/RTCMultiConnection-v1.6.js deleted file mode 100644 index d37824f6..00000000 --- a/RTCMultiConnection/Library/RTCMultiConnection-v1.6.js +++ /dev/null @@ -1,3360 +0,0 @@ -// Last time updated at 27 Feb 2014, 12:26:35 -// Latest file can be found here: https://www.webrtc-experiment.com/RTCMultiConnection-v1.6.js - -// Muaz Khan - www.MuazKhan.com -// MIT License - www.WebRTC-Experiment.com/licence -// Documentation - www.RTCMultiConnection.org/docs -// FAQ - www.RTCMultiConnection.org/FAQ -// v1.6 changes log - www.RTCMultiConnection.org/changes-log/#v1.6 -// _______________________ -// RTCMultiConnection-v1.6 - -/* issues/features need to be fixed & implemented: - --. hold/unhold of audio or video track instead of "all tracks". --. "channel" object in the openSignalingChannel shouldn't be mandatory! --. JSON parse/stringify options for data transmitted using data-channels; e.g. connection.preferJSON = true; (not-implemented) --. "onspeaking" and "onsilence" fires too often! (not-fixed) --. removeTrack() and addTracks() instead of "stop" (not-implemented) --. session-duration & statistics (not-implemented) - -// via http://www.slideshare.net/MuazKhan/echo-in-webrtc-why -var audio_constraints = { -mandatory: { -googEchoCancellation: true, -googAutoGainControl: true, -googNoiseSuppression: true, -googHighpassFilter: true, -googNoiseReduction: true, -} -}; -*/ - -(function() { - // www.RTCMultiConnection.org/docs/ - window.RTCMultiConnection = function(channel) { - // www.RTCMultiConnection.org/docs/channel-id/ - this.channel = channel || location.href.replace( /\/|:|#|%|\.|\[|\]/g , ''); - - // www.RTCMultiConnection.org/docs/open/ - this.open = function(_channel) { - self.joinedARoom = true; - - if (_channel) - self.channel = _channel; - - // if firebase && if session initiator - if (self.socket && self.socket.onDisconnect) - self.socket.onDisconnect().remove(); - - // www.RTCMultiConnection.org/docs/session-initiator/ - self.isInitiator = true; - - prepareInit(function() { - init(); - captureUserMedia(rtcSession.initSession); - }); - }; - - // www.RTCMultiConnection.org/docs/connect/ - this.connect = function(_channel) { - if (_channel) - self.channel = _channel; - - prepareInit(init); - - return this; - }; - - // www.RTCMultiConnection.org/docs/join/ - this.join = joinSession; - - // www.RTCMultiConnection.org/docs/send/ - this.send = function(data, _channel) { - // send file/data or /text - if (!data) - throw 'No file, data or text message to share.'; - - // connection.send([file1, file2, file3]) - if (!!data.forEach) { - for (var i = 0; i < data.length; i++) { - self.send(data[i], _channel); - } - return; - } - - if (typeof data.size != 'undefined' && typeof data.type != 'undefined') { - FileSender.send({ - file: data, - channel: rtcSession, - _channel: _channel, - root: self - }); - } else { - TextSender.send({ - text: data, - channel: rtcSession, - _channel: _channel, - root: self - }); - } - }; - - var self = this; - var rtcSession; - - function prepareInit(callback) { - if (self.openSignalingChannel) return callback(); - - // make sure firebase.js is loaded before using their JavaScript API - if (!window.Firebase) { - return loadScript('//www.webrtc-experiment.com/firebase.js', function() { - prepareInit(callback); - }); - } - - // Single socket is a preferred solution! - var socketCallbacks = { }; - var firebase = new Firebase('//' + self.firebase + '.firebaseio.com/' + self.channel); - firebase.on('child_added', function(snap) { - var data = snap.val(); - if (data.sender == self.userid) return; - - if (socketCallbacks[data.channel]) { - socketCallbacks[data.channel](data.message); - } - snap.ref().remove(); - }); - - // www.RTCMultiConnection.org/docs/openSignalingChannel/ - // github.com/muaz-khan/WebRTC-Experiment/blob/master/Signaling.md - self.openSignalingChannel = function(args) { - var callbackid = args.channel || self.channel; - socketCallbacks[callbackid] = args.onmessage; - - if (args.onopen) setTimeout(args.onopen, 1000); - return { - send: function(message) { - firebase.push({ - sender: self.userid, - channel: callbackid, - message: message - }); - }, - channel: channel /* todo: remove this "channel" object */ - }; - }; - - firebase.onDisconnect().remove(); - - callback(); - } - - // initializing RTCMultiSession - - function init() { - if (rtcSession) return; - - rtcSession = new RTCMultiSession(self); - } - - function joinSession(session) { - if (!session || !session.userid || !session.sessionid) - throw 'invalid data passed over "join" method'; - - self.session = session.session; - - extra = self.extra || session.extra || { }; - - if (session.oneway || session.data) { - rtcSession.joinSession(session, extra); - } else { - captureUserMedia(function() { - rtcSession.joinSession(session, extra); - }); - } - } - - // www.RTCMultiConnection.org/docs/captureUserMedia/ - - function captureUserMedia(callback, _session) { - // capture user's media resources - var session = _session || self.session; - - if (isEmpty(session)) { - if (callback) callback(); - return; - } - - if (self.dontAttachStream) - return callback(); - - if (isData(session) || (!self.isInitiator && session.oneway)) { - // www.RTCMultiConnection.org/docs/attachStreams/ - self.attachStreams = []; - return callback(); - } - - var constraints = { - audio: !!session.audio, - video: !!session.video - }; - - // if custom audio device is selected - if (self._mediaSources.audio) { - constraints.audio = { - optional: [{ - sourceId: self._mediaSources.audio - }] - }; - } - - // if custom video device is selected - if (self._mediaSources.video) { - constraints.video = { - optional: [{ - sourceId: self._mediaSources.video - }] - }; - } - - var screen_constraints = { - audio: false, - video: { - mandatory: { - chromeMediaSource: 'screen' - }, - optional: [] - } - }; - - if (session.screen) { - _captureUserMedia(screen_constraints, constraints.audio || constraints.video ? function() { - _captureUserMedia(constraints, callback); - } : callback); - } else _captureUserMedia(constraints, callback, session.audio && !session.video); - - function _captureUserMedia(forcedConstraints, forcedCallback, isRemoveVideoTracks) { - var mediaConfig = { - onsuccess: function(stream, returnBack, idInstance) { - if (returnBack) return forcedCallback && forcedCallback(stream); - - if (isRemoveVideoTracks && isChrome) { - stream = new window.webkitMediaStream(stream.getAudioTracks()); - } - - var mediaElement = createMediaElement(stream, session); - mediaElement.muted = true; - - stream.onended = function() { - self.onstreamended(streamedObject); - - // if user clicks "stop" button to close screen sharing - var _stream = self.streams[streamid]; - if (_stream && _stream.socket) { - _stream.socket.send({ - userid: self.userid, - streamid: _stream.streamid, - stopped: true - }); - } - - // to make sure same stream can be captured again! - if (currentUserMediaRequest.streams[idInstance]) { - delete currentUserMediaRequest.streams[idInstance]; - } - }; - - var streamid = getRandomString(); - - stream.streamid = streamid; - - var streamedObject = { - stream: stream, - streamid: streamid, - mediaElement: mediaElement, - blobURL: mediaElement.mozSrcObject || mediaElement.src, - type: 'local', - userid: self.userid || 'self', - extra: self.extra, - session: session - }; - - var sObject = { - stream: stream, - userid: self.userid || 'self', - streamid: streamid, - type: 'local', - streamObject: streamedObject, - mediaElement: mediaElement, - rtcMultiConnection: self - }; - - self.attachStreams.push(stream); - self.__attachStreams.push(sObject); - - self.streams[streamid] = self._getStream(sObject); - - self.onstream(streamedObject); - if (forcedCallback) forcedCallback(stream); - - if (self.onspeaking) { - var soundMeter = new SoundMeter({ - context: self._audioContext, - root: self, - event: streamedObject - }); - soundMeter.connectToSource(stream); - } - }, - onerror: function(e) { - self.onMediaError(toStr(e)); - - if (session.audio) { - self.onMediaError('Maybe microphone access is denied.'); - } - - if (session.video) { - self.onMediaError('Maybe webcam access is denied.'); - } - - if (session.screen) { - if (isFirefox) { - self.onMediaError('Firefox has not yet released their screen capturing modules. Still work in progress! Please try chrome for now!'); - } else if (location.protocol !== 'https:') { - self.onMediaError(' is mandatory to capture screen.'); - } else { - self.onMediaError('Unable to detect actual issue. Trying to check availability of screen sharing flag.'); - - self.caniuse.checkIfScreenSharingFlagEnabled(function(isFlagEnabled, warning) { - if (isFlagEnabled) { - if (chromeVersion < 31) { - self.onMediaError('Multi-capturing of screen is not allowed. Capturing process is denied. Try chrome >= M31.'); - } else { - self.onMediaError('Unknown screen capturing error.'); - } - } - - if (warning) self.onMediaError(warning); - - if (!warning) { - self.onMediaError('It seems that "chrome://flags/#enable-usermedia-screen-capture" flag is not enabled.'); - } - }); - } - } - }, - mediaConstraints: self.mediaConstraints || { } - }; - - mediaConfig.constraints = forcedConstraints || constraints; - mediaConfig.media = self.media; - getUserMedia(mediaConfig); - } - } - - // www.RTCMultiConnection.org/docs/captureUserMedia/ - this.captureUserMedia = captureUserMedia; - - // www.RTCMultiConnection.org/docs/leave/ - this.leave = function(userid) { - // eject a user; or leave the session - rtcSession.leave(userid); - - if (!userid) { - var streams = self.attachStreams; - for (var i = 0; i < streams.length; i++) { - stopTracks(streams[i]); - } - currentUserMediaRequest.streams = []; - self.attachStreams = []; - } - - // if firebase; remove data from firebase servers - if (self.isInitiator && !!self.socket && !!self.socket.remove) { - self.socket.remove(); - } - }; - - // www.RTCMultiConnection.org/docs/eject/ - this.eject = function(userid) { - if (!connection.isInitiator) throw 'Only session-initiator can eject a user.'; - this.leave(userid); - }; - - // www.RTCMultiConnection.org/docs/close/ - this.close = function() { - // close entire session - self.autoCloseEntireSession = true; - rtcSession.leave(); - }; - - // www.RTCMultiConnection.org/docs/renegotiate/ - this.renegotiate = function(stream) { - rtcSession.addStream({ - renegotiate: { - oneway: true, - audio: true, - video: true - }, - stream: stream - }); - }; - - // www.RTCMultiConnection.org/docs/addStream/ - this.addStream = function(session, socket) { - // www.RTCMultiConnection.org/docs/renegotiation/ - - // renegotiate new media stream - if (session) { - var isOneWayStreamFromParticipant; - if (!self.isInitiator && session.oneway) { - session.oneway = false; - isOneWayStreamFromParticipant = true; - } - - captureUserMedia(function(stream) { - if (isOneWayStreamFromParticipant) { - session.oneway = true; - } - addStream(stream); - }, session); - } else addStream(); - - function addStream(stream) { - rtcSession.addStream({ - stream: stream, - renegotiate: session || self.session, - socket: socket - }); - } - }; - - // www.RTCMultiConnection.org/docs/removeStream/ - this.removeStream = function(streamid) { - // detach pre-attached streams - if (!this.streams[streamid]) return warn('No such stream exists. Stream-id:', streamid); - - // www.RTCMultiConnection.org/docs/detachStreams/ - this.detachStreams.push(streamid); - }; - - // set RTCMultiConnection defaults on constructor invocation - setDefaults(this); - - this.__attachStreams = []; - }; - - function RTCMultiSession(root) { - var fileReceiver = new FileReceiver(root); - var textReceiver = new TextReceiver(root); - - function onDataChannelMessage(e) { - if (!e) return; - - e = JSON.parse(e); - - if (e.data.type === 'text') { - textReceiver.receive(e.data, e.userid, e.extra); - } else if (typeof e.data.maxChunks != 'undefined') { - fileReceiver.receive(e.data); - } else { - if (root.autoTranslateText) { - e.original = e.data; - root.Translator.TranslateText(e.data, function(translatedText) { - e.data = translatedText; - root.onmessage(e); - }); - } else root.onmessage(e); - } - } - - function onNewSession(session) { - if (root.onNewSession) { - session.join = function(forceSession) { - if (!forceSession) return root.join(session); - - for (var f in forceSession) { - session.session[f] = forceSession[f]; - } - - // keeping previous state - var isDontAttachStream = root.dontAttachStream; - - root.dontAttachStream = false; - root.captureUserMedia(function() { - root.dontAttachStream = true; - root.join(session); - - // returning back previous state - root.dontAttachStream = isDontAttachStream; - }, forceSession); - }; - return root.onNewSession(session); - } - - // user is already connected to a session - if (root.joinedARoom) return; - - root.joinedARoom = true; - - root.join(session); - } - - var self = { }; - var socketObjects = { }; - var sockets = []; - - self.userid = root.userid = root.userid || root.token(); - self.sessionid = root.channel; - - var participants = { }; - var isAcceptNewSession = true; - - function updateSocketForLocalStreams(socket) { - for (var i = 0; i < root.__attachStreams.length; i++) { - var streamid = root.__attachStreams[i].streamid; - if (root.streams[streamid]) root.streams[streamid].socket = socket; - } - root.__attachStreams = []; - } - - function newPrivateSocket(_config) { - var socketConfig = { - channel: _config.channel, - onmessage: socketResponse, - onopen: function(_socket) { - if (_socket) socket = _socket; - - if (isofferer && !peer) { - peerConfig.session = _config.session || root.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); - } - }; - - socketConfig.callback = function(_socket) { - socket = _socket; - socketConfig.onopen(); - }; - - var socket = root.openSignalingChannel(socketConfig), - isofferer = _config.isofferer, - peer; - - var peerConfig = { - onopen: onChannelOpened, - onicecandidate: function(candidate) { - if (!root.candidates) throw 'ICE candidates are mandatory.'; - if (!root.candidates.host && candidate.candidate.indexOf('typ host') != -1) return; - if (!root.candidates.relay && candidate.candidate.indexOf('relay') != -1) return; - if (!root.candidates.reflexive && candidate.candidate.indexOf('srflx') != -1) return; - - log(candidate.candidate); - - socket && socket.send({ - userid: self.userid, - candidate: { - sdpMLineIndex: candidate.sdpMLineIndex, - candidate: JSON.stringify(candidate.candidate) - } - }); - }, - onmessage: onDataChannelMessage, - onaddstream: function(stream) { - if (isData(root.session) && isFirefox) return; - - if (_config.streaminfo) { - var streaminfo = _config.streaminfo.split('----'); - if (streaminfo[0]) { - stream.streamid = streaminfo[0]; - delete streaminfo[0]; - _config.streaminfo = swap(streaminfo).join('----'); - } - } - - var __session = _config.session || root.session; - __session.remote = true; - - var mediaElement = createMediaElement(stream, __session); - - _config.stream = stream; - if (mediaElement.tagName.toLowerCase() == 'audio') - mediaElement.addEventListener('play', function() { - setTimeout(function() { - mediaElement.muted = false; - afterRemoteStreamStartedFlowing(mediaElement); - }, 3000); - }, false); - else - waitUntilRemoteStreamStartsFlowing(mediaElement); - }, - - onremovestream: function(event) { - warn('onremovestream', event); - }, - - onclose: function(e) { - e.extra = _config.extra; - e.userid = _config.userid; - root.onclose(e); - - // suggested in #71 by "efaj" - if (root.channels[e.userid]) - delete root.channels[e.userid]; - }, - onerror: function(e) { - e.extra = _config.extra; - e.userid = _config.userid; - root.onerror(e); - }, - - oniceconnectionstatechange: function(event) { - return log('oniceconnectionstatechange', toStr(event)); - - // auto redial feature is temporarily disabled because it affects renegotiation process - // when we renegotiate streams; it fires ice-connection-state == 'disconnected' - // so it is not easy to distinguish between peers failure or renegotiation. - - // if you want to enable auto-redial feature; then omit word "return" from above statement. - - if (!root.peers[_config.userid]) return; - - if (root.peers[_config.userid].peer.connection.iceConnectionState != 'disconnected') { - self.redialing = false; - } - - if (root.peers[_config.userid].peer.connection.iceConnectionState == 'disconnected' && !self.redialing) { - self.redialing = true; - - error('Peer connection is closed.', toStr(root.peers[_config.userid].peer.connection)); - root.peers[_config.userid].redial(); - } - }, - - onsignalingstatechange: function(event) { - log('onsignalingstatechange', toStr(event)); - }, - - attachStreams: root.attachStreams, - iceServers: root.iceServers, - bandwidth: root.bandwidth, - sdpConstraints: root.sdpConstraints, - optionalArgument: root.optionalArgument, - disableDtlsSrtp: root.disableDtlsSrtp, - dataChannelDict: root.dataChannelDict, - preferSCTP: root.preferSCTP, - - onSessionDescription: function(sessionDescription, streaminfo) { - sendsdp({ - sdp: sessionDescription, - socket: socket, - streaminfo: streaminfo - }); - }, - - socket: socket, - selfUserid: root.userid - }; - - function waitUntilRemoteStreamStartsFlowing(mediaElement) { - if (!(mediaElement.readyState <= HTMLMediaElement.HAVE_CURRENT_DATA || mediaElement.paused || mediaElement.currentTime <= 0)) { - afterRemoteStreamStartedFlowing(mediaElement); - } else - setTimeout(function() { - waitUntilRemoteStreamStartsFlowing(mediaElement); - }, 50); - } - - function initFakeChannel() { - if (!root.fakeDataChannels || root.channels[_config.userid]) return; - - // for non-data connections; allow fake data sender! - if (!root.session.data) { - var fakeChannel = { - send: function(data) { - socket.send({ - fakeData: data - }); - }, - readyState: 'open' - }; - // connection.channels['user-id'].send(data); - root.channels[_config.userid] = { - channel: fakeChannel, - send: function(data) { - this.channel.send(data); - } - }; - peerConfig.onopen(fakeChannel); - } - } - - function afterRemoteStreamStartedFlowing(mediaElement) { - var stream = _config.stream; - - stream.onended = function() { - root.onstreamended(streamedObject); - }; - - var streamedObject = { - mediaElement: mediaElement, - - stream: stream, - streamid: stream.streamid, - session: root.session, - - blobURL: mediaElement.mozSrcObject || mediaElement.src, - type: 'remote', - - extra: _config.extra, - userid: _config.userid - }; - - // connection.streams['stream-id'].mute({audio:true}) - root.streams[stream.streamid] = root._getStream({ - stream: stream, - userid: _config.userid, - streamid: stream.streamid, - socket: socket, - type: 'remote', - streamObject: streamedObject, - mediaElement: mediaElement, - rtcMultiConnection: root - }); - - root.onstream(streamedObject); - - onSessionOpened(); - - if (root.onspeaking) { - var soundMeter = new SoundMeter({ - context: root._audioContext, - root: root, - event: streamedObject - }); - soundMeter.connectToSource(stream); - } - } - - function onChannelOpened(channel) { - _config.channel = channel; - - // connection.channels['user-id'].send(data); - root.channels[_config.userid] = { - channel: _config.channel, - send: function(data) { - root.send(data, this.channel); - } - }; - - root.onopen({ - extra: _config.extra, - userid: _config.userid - }); - - // fetch files from file-queue - for (var q in root.fileQueue) { - root.send(root.fileQueue[q], channel); - } - - if (isData(root.session)) onSessionOpened(); - } - - 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.peers['user-id'].addStream({audio:true}) - root.peers[_config.userid] = { - socket: socket, - peer: peer, - userid: _config.userid, - extra: _config.extra, - addStream: function(session00) { - // connection.peers['user-id'].addStream({audio: true, video: true); - - root.addStream(session00, this.socket); - }, - renegotiate: function(stream) { - // connection.peers['user-id'].renegotiate(); - - root.renegotiate(stream); - }, - changeBandwidth: function(bandwidth) { - // connection.peers['user-id'].changeBandwidth(); - - if (!bandwidth) throw 'You MUST pass bandwidth object.'; - if (typeof bandwidth == 'string') 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({ - userid: root.userid, - extra: root.extra || { }, - changeBandwidth: true, - bandwidth: bandwidth - }); - }, - sendCustomMessage: function(message) { - // connection.peers['user-id'].sendCustomMessage(); - - this.socket.send({ - userid: root.userid, - extra: root.extra || { }, - customMessage: true, - message: message - }); - }, - onCustomMessage: function(message) { - log('Received "private" message from', this.userid, - typeof message == 'string' ? message : toStr(message)); - }, - drop: function(dontSendMessage) { - // connection.peers['user-id'].drop(); - - for (var stream in root.streams) { - if (root._skip.indexOf(stream) == -1) { - stream = root.streams[stream]; - - if (stream.userid == root.userid && stream.type == 'local') { - this.peer.connection.removeStream(stream.stream); - root.onstreamended(stream.streamObject); - } - - if (stream.type == 'remote' && stream.userid == this.userid) { - root.onstreamended(stream.streamObject); - } - } - } - - !dontSendMessage && this.socket.send({ - userid: root.userid, - extra: root.extra || { }, - drop: true - }); - }, - hold: function() { - // connection.peers['user-id'].hold(); - - this.peer.hold = true; - this.socket.send({ - userid: root.userid, - extra: root.extra || { }, - hold: true - }); - }, - unhold: function() { - // connection.peers['user-id'].unhold(); - - this.peer.hold = false; - this.socket.send({ - userid: root.userid, - extra: root.extra || { }, - unhold: true - }); - }, - fireHoldUnHoldEvents: function(hold) { - for (var stream in root.streams) { - if (root._skip.indexOf(stream) == -1) { - stream = root.streams[stream]; - - if (stream.userid == root.userid && stream.type == 'local') { - this.peer.connection.removeStream(stream.stream); - } - - // www.RTCMultiConnection.org/docs/onhold/ - if (hold && root.onhold) root.onhold(stream.streamObject); - - // www.RTCMultiConnection.org/docs/onunhold/ - if (hold && root.onunhold) root.onunhold(stream.streamObject); - } - } - }, - redial: function() { - // connection.peers['user-id'].redial(); - - // 1st of all; remove all relevant remote media streams - for (var stream in root.streams) { - if (root._skip.indexOf(stream) == -1) { - stream = root.streams[stream]; - - if (stream.userid == this.userid && stream.type == 'remote') { - root.onstreamended(stream.streamObject); - } - } - } - - log('ReDialing...'); - - socket.send({ - userid: root.userid, - extra: root.extra, - recreatePeer: true - }); - - peer = new PeerConnection(); - peer.create('offer', peerConfig); - } - }; - } - - function onSessionOpened() { - // admin/guest is one-to-one relationship - if (root.userType && root.direction !== 'many-to-many') return; - - // original conferencing infrastructure! - if (!root.session.oneway && !root.session.broadcast && root.isInitiator && getLength(participants) > 1 && getLength(participants) <= root.maxParticipantsAllowed) { - defaultSocket.send({ - newParticipant: _config.userid || socket.channel, - userid: self.userid, - extra: _config.extra || { } - }); - } - } - - function socketResponse(response) { - if (response.userid == root.userid) - return; - - if (response.sdp) { - _config.userid = response.userid; - _config.extra = response.extra || { }; - _config.renegotiate = response.renegotiate; - _config.streaminfo = response.streaminfo; - - var sdp = JSON.parse(response.sdp); - - if (sdp.type == 'offer') { - // to synchronize SCTP or RTP - peerConfig.preferSCTP = !!response.preferSCTP; - root.fakeDataChannels = !!response.fakeDataChannels; - } - - // initializing fake channel - initFakeChannel(); - - sdpInvoker(sdp, response.labels); - } - - if (response.candidate) { - peer && peer.addIceCandidate({ - sdpMLineIndex: response.candidate.sdpMLineIndex, - candidate: JSON.parse(response.candidate.candidate) - }); - } - - if (response.mute || response.unmute) { - if (response.promptMuteUnmute) { - if (response.mute) root.streams[response.streamid].mute(response.session); - if (response.unmute) root.streams[response.streamid].unmute(response.session); - } else { - if (root.streams[response.streamid]) - response.mediaElement = root.streams[response.streamid].mediaElement; - - if (response.mute) root.onmute(response); - if (response.unmute) root.onunmute(response); - } - } - - // to stop local stream - if (response.stopped) { - if (root.streams[response.streamid]) - response.mediaElement = root.streams[response.streamid].mediaElement; - - root.onstreamended(response); - } - - // to stop remote stream - if (response.promptStreamStop && !root.isInitiator) { - warn('What if initiator invoked stream.stop for remote user?'); - // todo: need to stop stream; however "close" or "leave" method MUST not affect it. - // if (root.streams[response.streamid]) root.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 root.streams) { - stream = root.streams[stream]; - if (stream.userid == userLeft) { - stopTracks(stream); - stream.stream.onended(stream.streamObject); - } - } - } - - if (peer && peer.connection) { - peer.connection.close(); - peer.connection = null; - } - - if (response.closeEntireSession) { - root.close(); - root.refresh(); - } else if (socket && response.ejected) { - // if user is ejected; his stream MUST be removed - // from all other users' side - socket.send({ - left: true, - extra: root.extra, - userid: self.userid - }); - - if (sockets[_config.socketIndex]) - delete sockets[_config.socketIndex]; - if (socketObjects[socket.channel]) - delete socketObjects[socket.channel]; - - socket = null; - } - - root.remove(response.userid); - - if (participants[response.userid]) delete participants[response.userid]; - - root.onleave({ - userid: response.userid, - extra: response.extra - }); - - if (root.userType) root.busy = false; - } - - // keeping session active even if initiator leaves - if (response.playRoleOfBroadcaster) { - if (response.extra) { - root.extra = merge(root.extra, response.extra); - } - setTimeout(root.playRoleOfInitiator, 2000); - } - - if (response.isCreateDataChannel) { - if (isFirefox) { - peer.createDataChannel(); - } - } - - if (response.changeBandwidth) { - if (!root.peers[response.userid]) throw 'No such peer exists.'; - - // synchronize bandwidth - root.peers[response.userid].peer.bandwidth = response.bandwidth; - - // renegotiate to apply bandwidth - root.peers[response.userid].renegotiate(); - } - - if (response.customMessage) { - if (!root.peers[response.userid]) throw 'No such peer exists.'; - root.peers[response.userid].onCustomMessage(response.message); - } - - if (response.drop) { - if (!root.peers[response.userid]) throw 'No such peer exists.'; - root.peers[response.userid].drop(true); - root.peers[response.userid].renegotiate(); - - root.ondrop(response.userid); - } - - if (response.hold) { - if (!root.peers[response.userid]) throw 'No such peer exists.'; - root.peers[response.userid].peer.hold = true; - root.peers[response.userid].renegotiate(); - } - - if (response.unhold) { - if (!root.peers[response.userid]) throw 'No such peer exists.'; - root.peers[response.userid].peer.hold = false; - root.peers[response.userid].renegotiate(); - } - - // fake data channels! - if (response.fakeData) { - peerConfig.onmessage(response.fakeData); - } - - if (response.recreatePeer) { - peer = new PeerConnection(); - } - } - - root.playRoleOfInitiator = function() { - root.dontAttachStream = true; - root.open(); - sockets = swap(sockets); - root.dontAttachStream = false; - }; - - function sdpInvoker(sdp, labels) { - log(sdp.type, sdp.sdp); - - if (sdp.type == 'answer') { - peer.setRemoteDescription(sdp); - updateSocket(); - return; - } - if (!_config.renegotiate && sdp.type == 'offer') { - peerConfig.offerDescription = sdp; - peerConfig.session = _config.session || root.session; - if (!peer) peer = new PeerConnection(); - peer.create('answer', peerConfig); - - updateSocket(); - return; - } - - root.session = _config.renegotiate; - // detach streams - detachMediaStream(labels, peer.connection); - - if (root.session.oneway || isData(root.session)) { - createAnswer(); - } else { - if (_config.capturing) - return; - - _config.capturing = true; - - root.captureUserMedia(function(stream) { - _config.capturing = false; - - peer.connection.addStream(stream); - createAnswer(); - }, _config.renegotiate); - } - - delete _config.renegotiate; - - function createAnswer() { - peer.recreateAnswer(sdp, root.session, function(_sdp, streaminfo) { - sendsdp({ - sdp: _sdp, - socket: socket, - streaminfo: streaminfo - }); - }); - } - } - } - - function detachMediaStream(labels, peer) { - if (!labels) return; - for (var i = 0; i < labels.length; i++) { - var label = labels[i]; - if (root.streams[label]) { - peer.removeStream(root.streams[label].stream); - } - } - } - - function sendsdp(e) { - e.socket.send({ - userid: self.userid, - sdp: JSON.stringify(e.sdp), - extra: root.extra, - renegotiate: !!e.renegotiate ? e.renegotiate : false, - streaminfo: e.streaminfo || '', - labels: e.labels || [], - preferSCTP: !!root.preferSCTP, - fakeDataChannels: !!root.fakeDataChannels - }); - } - - // sharing new user with existing participants - - function onNewParticipant(response) { - var channel = response.newParticipant; - - if (!channel || !!participants[channel] || channel == self.userid) - return; - - participants[channel] = channel; - - var new_channel = root.token(); - newPrivateSocket({ - channel: new_channel, - extra: response.extra || { }, - userid: response.userid - }); - - defaultSocket.send({ - participant: true, - userid: self.userid, - targetUser: channel, - channel: new_channel, - extra: root.extra - }); - } - - // if a user leaves - - function clearSession(channel) { - var alert = { - left: true, - extra: root.extra, - userid: self.userid, - sessionid: self.sessionid - }; - - if (root.isInitiator) { - if (root.autoCloseEntireSession) { - alert.closeEntireSession = true; - } else if (sockets[0]) { - sockets[0].send({ - playRoleOfBroadcaster: true, - userid: self.userid - }); - } - } - - if (!channel) { - var length = sockets.length; - for (var i = 0; i < length; i++) { - socket = sockets[i]; - if (socket) { - socket.send(alert); - - if (socketObjects[socket.channel]) - delete socketObjects[socket.channel]; - - delete sockets[i]; - } - } - } - - // eject a specific user! - if (channel) { - socket = socketObjects[channel]; - if (socket) { - alert.ejected = true; - socket.send(alert); - - if (sockets[socket.index]) - delete sockets[socket.index]; - - delete socketObjects[channel]; - } - } - - sockets = swap(sockets); - } - - // www.RTCMultiConnection.org/docs/remove/ - root.remove = function(userid) { - if (self.requestsFrom && self.requestsFrom[userid]) delete self.requestsFrom[userid]; - - if (root.peers[userid]) { - if (root.peers[userid].peer && root.peers[userid].peer.connection) { - root.peers[userid].peer.connection.close(); - root.peers[userid].peer.connection = null; - } - delete root.peers[userid]; - } - if (participants[userid]) { - delete participants[userid]; - } - for (var stream in root.streams) { - stream = root.streams[stream]; - if (stream.userid == userid) { - root.onstreamended(stream.streamObject); - if (stream.stop) stream.stop(); - delete root.streams[stream]; - } - } - if (socketObjects[userid]) { - delete socketObjects[userid]; - } - }; - - // www.RTCMultiConnection.org/docs/refresh/ - root.refresh = function() { - participants = []; - root.joinedARoom = self.joinedARoom = false; - isAcceptNewSession = true; - root.busy = false; - - // to stop/remove self streams - for (var i = 0; i < root.attachStreams.length; i++) { - stopTracks(root.attachStreams[i]); - } - root.attachStreams = []; - - // to allow capturing of identical streams - currentUserMediaRequest = { - streams: [], - mutex: false, - queueRequests: [] - }; - that.isOwnerLeaving = true; - root.isInitiator = false; - }; - - // www.RTCMultiConnection.org/docs/reject/ - root.reject = function(userid) { - if (typeof userid != 'string') userid = userid.userid; - defaultSocket.send({ - rejectedRequestOf: userid, - userid: root.userid, - extra: root.extra || { } - }); - }; - - window.addEventListener('beforeunload', function() { - clearSession(); - }, false); - - window.addEventListener('keydown', function(e) { - if (e.keyCode == 116) - clearSession(); - }, false); - - function initDefaultSocket() { - defaultSocket = root.openSignalingChannel({ - onmessage: function(response) { - if (response.userid == self.userid) return; - - if (isAcceptNewSession && response.sessionid && response.userid) { - root.session = response.session; - onNewSession(response); - } - - if (response.newParticipant && self.joinedARoom && self.broadcasterid === response.userid) { - onNewParticipant(response); - } - - if (getLength(participants) < root.maxParticipantsAllowed && response.userid && response.targetUser == self.userid && response.participant && !participants[response.userid]) { - acceptRequest(response); - } - - if (response.userType && response.userType != root.userType) { - if (!root.busy) { - if (response.userType == 'admin') { - if (root.onAdmin) root.onAdmin(response); - else root.accept(response.userid); - } - if (response.userType == 'guest') { - if (root.onGuest) root.onGuest(response); - else root.accept(response.userid); - } - } else { - if (response.userType != root.userType) { - root.reject(response.userid); - } - } - } - - if (response.acceptedRequestOf == self.userid) { - if (root.onstats) root.onstats('accepted', response); - } - - if (response.rejectedRequestOf == self.userid) { - if (root.onstats) root.onstats(root.userType ? 'busy' : 'rejected', response); - sendRequest(); - } - - if (response.customMessage) { - if (response.message.drop) { - root.ondrop(response.userid); - - root.attachStreams = []; - // "drop" should detach all local streams - for (var stream in root.streams) { - if (root._skip.indexOf(stream) == -1) { - stream = root.streams[stream]; - if (stream.type == 'local') { - root.detachStreams.push(stream.streamid); - root.onstreamended(stream.streamObject); - } else root.onstreamended(stream.streamObject); - } - } - - if (response.message.renegotiate) { - // renegotiate; so "peer.removeStream" happens. - root.addStream(); - } - } else if (root.onCustomMessage) { - root.onCustomMessage(response.message); - } - } - }, - callback: function(socket) { - if (socket) defaultSocket = socket; - if (root.userType) sendRequest(socket || defaultSocket); - }, - onopen: function(socket) { - if (socket) defaultSocket = socket; - if (root.userType) sendRequest(socket || defaultSocket); - } - }); - } - - var that = this, - defaultSocket; - - initDefaultSocket(); - - function sendRequest(socket) { - if (!socket) { - return setTimeout(function() { - sendRequest(defaultSocket); - }, 1000); - } - - socket.send({ - userType: root.userType, - userid: root.userid, - extra: root.extra || { } - }); - } - - function setDirections() { - var userMaxParticipantsAllowed; - // if user has set a custom max participant setting, remember it - if( root.maxParticipantsAllowed != 256 ) - userMaxParticipantsAllowed = root.maxParticipantsAllowed; - - if (root.direction == 'one-way') root.session.oneway = true; - if (root.direction == 'one-to-one') root.maxParticipantsAllowed = 1; - if (root.direction == 'one-to-many') root.session.broadcast = true; - if (root.direction == 'many-to-many') { - if (!root.maxParticipantsAllowed || root.maxParticipantsAllowed == 1) { - root.maxParticipantsAllowed = 256; - } - } - // if user has set a custom max participant setting, set it back - if( userMaxParticipantsAllowed && root.maxParticipantsAllowed != 1) - root.maxParticipantsAllowed= userMaxParticipantsAllowed; - } - - // open new session - this.initSession = function() { - that.isOwnerLeaving = false; - root.isInitiator = true; - - setDirections(); - participants = { }; - - self.sessionid = root.sessionid || root.channel; - - this.isOwnerLeaving = isAcceptNewSession = false; - self.joinedARoom = true; - - (function transmit() { - if (getLength(participants) < root.maxParticipantsAllowed && !that.isOwnerLeaving) { - defaultSocket && defaultSocket.send({ - sessionid: self.sessionid, - userid: root.userid, - session: root.session, - extra: root.extra - }); - } - - if (!root.transmitRoomOnce && !that.isOwnerLeaving) - setTimeout(transmit, root.interval || 3000); - })(); - }; - - // join existing session - this.joinSession = function(_config) { - _config = _config || { }; - participants = { }; - root.session = _config.session || { }; - self.broadcasterid = _config.userid; - - if (_config.sessionid) - self.sessionid = _config.sessionid; - - isAcceptNewSession = false; - self.joinedARoom = true; - - var channel = getRandomString(); - newPrivateSocket({ - channel: channel, - extra: _config.extra || { }, - userid: _config.userid - }); - - defaultSocket.send({ - participant: true, - userid: self.userid, - channel: channel, - targetUser: _config.userid, - extra: root.extra, - session: root.session - }); - }; - - // send file/data or text message - this.send = function(message, _channel) { - message = JSON.stringify({ - extra: root.extra, - userid: root.userid, - data: message - }); - - if (_channel) { - if (_channel.readyState == 'open') { - _channel.send(message); - } - return; - } - - for (var dataChannel in root.channels) { - var channel = root.channels[dataChannel].channel; - if (channel.readyState == 'open') { - channel.send(message); - } - } - }; - - // leave session - this.leave = function(userid) { - clearSession(userid); - - if (root.isInitiator) { - that.isOwnerLeaving = true; - root.isInitiator = false; - } - - // to stop/remove self streams - for (var i = 0; i < root.attachStreams.length; i++) { - stopTracks(root.attachStreams[i]); - } - root.attachStreams = []; - - // to allow capturing of identical streams - currentUserMediaRequest = { - streams: [], - mutex: false, - queueRequests: [] - }; - - if (!userid) { - root.joinedARoom = self.joinedARoom = false; - isAcceptNewSession = true; - } - - root.busy = false; - }; - - // renegotiate new stream - this.addStream = function(e) { - root.session = e.renegotiate; - - if (e.socket) { - addStream(root.peers[e.socket.userid]); - } else { - for (var peer in root.peers) { - addStream(root.peers[peer]); - } - } - - function addStream(peer00) { - var socket = peer00.socket; - if (!socket) { - warn(peer00, 'doesn\'t has socket.'); - return; - } - - updateSocketForLocalStreams(socket); - - if (!peer00 || !peer00.peer) { - throw 'No peer to renegotiate.'; - } - - peer00 = peer00.peer; - - // detaching old streams - detachMediaStream(root.detachStreams, peer00.connection); - - if (e.stream && (root.session.audio || root.session.video || root.session.screen)) { - peer00.connection.addStream(e.stream); - } - - peer00.recreateOffer(root.session, function(sdp, streaminfo) { - sendsdp({ - sdp: sdp, - socket: socket, - renegotiate: root.session, - labels: root.detachStreams, - streaminfo: streaminfo - }); - root.detachStreams = []; - }); - } - }; - - // www.RTCMultiConnection.org/docs/request/ - root.request = function(userid, extra) { - if (root.direction === 'many-to-many') root.busy = true; - - root.captureUserMedia(function() { - // open private socket that will be used to receive offer-sdp - newPrivateSocket({ - channel: root.userid, - extra: extra || { }, - userid: userid - }); - - // ask other user to create offer-sdp - defaultSocket.send({ - participant: true, - userid: root.userid, - extra: root.extra || { }, - targetUser: userid - }); - }); - }; - - function acceptRequest(response) { - if (!self.requestsFrom) self.requestsFrom = { }; - if (root.busy || self.requestsFrom[response.userid]) return; - - var obj = { - userid: response.userid, - extra: response.extra, - channel: response.channel || response.userid, - session: response.session || root.session - }; - - self.requestsFrom[response.userid] = obj; - - // www.RTCMultiConnection.org/docs/onRequest/ - if (root.onRequest) { - root.onRequest(obj); - } else _accept(obj); - } - - function _accept(e) { - if (root.userType) { - if (root.direction === 'many-to-many') root.busy = true; - defaultSocket.send({ - acceptedRequestOf: e.userid, - userid: self.userid, - extra: root.extra || { } - }); - } - - participants[e.userid] = e.userid; - newPrivateSocket({ - isofferer: true, - userid: e.userid, - channel: e.channel, - extra: e.extra || { }, - session: e.session || root.session - }); - } - - // www.RTCMultiConnection.org/docs/sendMessage/ - root.sendCustomMessage = function(message) { - if (!defaultSocket) { - return setTimeout(function() { - root.sendMessage(message); - }, 1000); - } - - defaultSocket.send({ - userid: root.userid, - customMessage: true, - message: message - }); - }; - - // www.RTCMultiConnection.org/docs/accept/ - root.accept = function(e) { - // for backward compatibility - if (arguments.length > 1 && typeof arguments[0] == 'string') { - e = { }; - if (arguments[0]) e.userid = arguments[0]; - if (arguments[1]) e.extra = arguments[1]; - if (arguments[2]) e.channel = arguments[2]; - } - - root.captureUserMedia(function() { - _accept(e); - }); - }; - } - - var RTCPeerConnection = window.mozRTCPeerConnection || window.webkitRTCPeerConnection; - var RTCSessionDescription = window.mozRTCSessionDescription || window.RTCSessionDescription; - var RTCIceCandidate = window.mozRTCIceCandidate || window.RTCIceCandidate; - - function PeerConnection() { - return { - create: function(type, options) { - merge(this, options); - - var self = this; - - this.type = type; - this.init(); - this.attachMediaStreams(); - - if (isData(this.session) && isFirefox) { - navigator.mozGetUserMedia({ - audio: true, - fake: true - }, function(stream) { - self.connection.addStream(stream); - - if (type == 'offer') { - self.createDataChannel(); - } - - self.getLocalDescription(type); - - if (type == 'answer') { - self.createDataChannel(); - } - }, this.onMediaError); - } - - if (!isData(this.session) && isFirefox) { - if (this.session.data && type == 'offer') { - this.createDataChannel(); - } - - this.getLocalDescription(type); - - if (this.session.data && type == 'answer') { - this.createDataChannel(); - } - } - - isChrome && self.getLocalDescription(type); - return this; - }, - getLocalDescription: function(type) { - log('peer type is', type); - - if (type == 'answer') { - this.setRemoteDescription(this.offerDescription); - } - - var self = this; - this.connection[type == 'offer' ? 'createOffer' : 'createAnswer'](function(sessionDescription) { - sessionDescription.sdp = self.serializeSdp(sessionDescription.sdp); - self.connection.setLocalDescription(sessionDescription); - self.onSessionDescription(sessionDescription, self.streaminfo); - }, this.onSdpError, this.constraints); - }, - serializeSdp: function(sdp) { - sdp = this.setBandwidth(sdp); - if (this.hold) { - sdp = sdp.replace( /sendonly|recvonly|sendrecv/g , 'inactive'); - } else if (this.prevSDP) { - sdp = sdp.replace( /inactive/g , 'sendrecv'); - } - return sdp; - }, - init: function() { - this.setConstraints(); - this.connection = new RTCPeerConnection(this.iceServers, this.optionalArgument); - - if (this.session.data && isChrome) { - this.createDataChannel(); - } - - this.connection.onicecandidate = function(event) { - if (event.candidate) { - self.onicecandidate(event.candidate); - } - }; - - this.connection.onaddstream = function(e) { - log('onaddstream', e.stream); - - self.onaddstream(e.stream); - }; - - 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() { - self.connection && self.oniceconnectionstatechange({ - iceConnectionState: self.connection.iceConnectionState, - iceGatheringState: self.connection.iceGatheringState, - signalingState: self.connection.signalingState - }); - }; - var self = this; - }, - setBandwidth: function(sdp) { - // sdp.replace( /a=sendrecv\r\n/g , 'a=sendrecv\r\nb=AS:50\r\n'); - - if (isMobileDevice || isFirefox || !this.bandwidth) return sdp; - - var bandwidth = this.bandwidth; - - // if screen; must use at least 300kbs - if (this.session.screen && isEmpty(bandwidth)) { - 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:300\r\n'); - } - - // remove existing bandwidth lines - if (bandwidth.audio || bandwidth.video || bandwidth.data) { - sdp = sdp.replace( /b=AS([^\r\n]+\r\n)/g , ''); - } - - if (bandwidth.audio) { - sdp = sdp.replace( /a=mid:audio\r\n/g , 'a=mid:audio\r\nb=AS:' + bandwidth.audio + '\r\n'); - } - - if (bandwidth.video) { - sdp = sdp.replace( /a=mid:video\r\n/g , 'a=mid:video\r\nb=AS:' + (this.session.screen ? '300' : bandwidth.video) + '\r\n'); - } - - if (bandwidth.data && !this.preferSCTP) { - sdp = sdp.replace( /a=mid:data\r\n/g , 'a=mid:data\r\nb=AS:' + bandwidth.data + '\r\n'); - } - - return sdp; - }, - setConstraints: function() { - this.constraints = { - optional: this.sdpConstraints.optional || [], - mandatory: this.sdpConstraints.mandatory || { - OfferToReceiveAudio: !!this.session.audio, - OfferToReceiveVideo: !!this.session.video || !!this.session.screen - } - }; - - // workaround for older firefox - if (this.session.data && isFirefox && this.constraints.mandatory) { - this.constraints.mandatory.OfferToReceiveAudio = true; - } - - log('sdp-constraints', toStr(this.constraints.mandatory)); - - this.optionalArgument = { - optional: this.optionalArgument.optional || [{ - DtlsSrtpKeyAgreement: true - }], - mandatory: this.optionalArgument.mandatory || { } - }; - - if (isChrome && chromeVersion >= 32) { - this.optionalArgument.optional.push({ - googIPv6: true - }); - // this.optionalArgument.optional.push({ googDscp: true }); - } - - if (!this.preferSCTP) { - this.optionalArgument.optional.push({ - RtpDataChannels: true - }); - } - - log('optional-argument', toStr(this.optionalArgument.optional)); - - this.iceServers = { - iceServers: this.iceServers - }; - - log('ice-servers', toStr(this.iceServers.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); - }, - onMediaError: function(err) { - error(toStr(err)); - }, - setRemoteDescription: function(sessionDescription) { - if (!sessionDescription) throw 'Remote session description should NOT be NULL.'; - - log('setting remote description', sessionDescription.type, sessionDescription.sdp); - this.connection.setRemoteDescription( - new RTCSessionDescription(sessionDescription) - ); - }, - addIceCandidate: function(candidate) { - this.connection.addIceCandidate(new RTCIceCandidate({ - sdpMLineIndex: candidate.sdpMLineIndex, - candidate: candidate.candidate - })); - }, - createDataChannel: function(channelIdentifier) { - if (!this.channels) this.channels = []; - - // protocol: 'text/chat', preset: true, stream: 16 - // maxRetransmits:0 && ordered:false - var dataChannelDict = { }; - - if (this.dataChannelDict) dataChannelDict = this.dataChannelDict; - - if (isChrome && !this.preferSCTP) { - dataChannelDict.reliable = false; // Deprecated! - } - - log('dataChannelDict', toStr(dataChannelDict)); - - if (isFirefox) { - this.connection.onconnection = function() { - self.socket.send({ - userid: self.selfUserid, - isCreateDataChannel: true - }); - }; - } - - 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) - ); - } - - var self = this; - }, - setChannelEvents: function(channel) { - var self = this; - channel.onmessage = function(event) { - self.onmessage(event.data); - }; - channel.onopen = function() { - channel.push = channel.send; - channel.send = function(data) { - if (channel.readyState != 'open') { - return setTimeout(function() { - channel.send(data); - }, 1000); - } - try { - channel.push(data); - } catch(e) { - warn('Data transmission failed. Re-transmitting..', toStr(e)); - return 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); - }, - attachMediaStreams: function() { - var streams = this.attachStreams; - for (var i = 0; i < streams.length; i++) { - log('attaching stream:', streams[i].streamid); - this.connection.addStream(streams[i]); - } - this.attachStreams = []; - this.getStreamInfo(); - }, - getStreamInfo: function() { - this.streaminfo = ''; - var streams = this.connection.getLocalStreams(); - for (var i = 0; i < streams.length; i++) { - if (i == 0) { - this.streaminfo = streams[i].streamid; - } else { - this.streaminfo += '----' + streams[i].streamid; - } - } - }, - recreateOffer: function(renegotiate, callback) { - // if(isFirefox) this.create(this.type, this); - - log('recreating offer'); - - this.type = 'offer'; - this.renegotiate = true; - this.session = renegotiate; - this.setConstraints(); - - this.onSessionDescription = callback; - this.getStreamInfo(); - - // one can renegotiate data connection in existing audio/video/screen connection! - if (this.session.data && isChrome) { - this.createDataChannel(); - } - - this.getLocalDescription('offer'); - }, - recreateAnswer: function(sdp, session, callback) { - // if(isFirefox) this.create(this.type, this); - - log('recreating answer'); - - this.type = 'answer'; - this.renegotiate = true; - this.session = session; - 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 && isChrome) { - this.createDataChannel(); - } - - this.getLocalDescription('answer'); - } - }; - } - - var video_constraints = { - mandatory: { }, - optional: [] - }; - - /* by @FreCap pull request #41 */ - var currentUserMediaRequest = { - streams: [], - mutex: false, - queueRequests: [] - }; - - function getUserMedia(options) { - if (currentUserMediaRequest.mutex === true) { - currentUserMediaRequest.queueRequests.push(options); - return; - } - currentUserMediaRequest.mutex = true; - - // tools.ietf.org/html/draft-alvestrand-constraints-resolution-00 - var mediaConstraints = options.mediaConstraints || { }; - var n = navigator, - hints = options.constraints || { - audio: true, - video: video_constraints - }; - - if (hints.video == true) hints.video = video_constraints; - - // connection.mediaConstraints.audio = false; - if (typeof mediaConstraints.audio != 'undefined') { - hints.audio = mediaConstraints.audio; - } - - // connection.media.min(320,180); - // connection.media.max(1920,1080); - var media = options.media; - if (isChrome) { - var mandatory = { - minWidth: media.minWidth, - minHeight: media.minHeight, - maxWidth: media.maxWidth, - maxHeight: media.maxHeight, - minAspectRatio: media.minAspectRatio - }; - - // code.google.com/p/chromium/issues/detail?id=143631#c9 - 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 (mediaConstraints.mandatory) - hints.video.mandatory = merge(hints.video.mandatory, mediaConstraints.mandatory); - - // mediaConstraints.optional.bandwidth = 1638400; - if (mediaConstraints.optional) - hints.video.optional[0] = merge({ }, mediaConstraints.optional); - - log('media hints:', toStr(hints)); - - // easy way to match - var idInstance = JSON.stringify(hints); - - function streaming(stream, returnBack) { - var video = options.video; - if (video) { - video[isFirefox ? 'mozSrcObject' : 'src'] = isFirefox ? stream : window.webkitURL.createObjectURL(stream); - video.play(); - } - - options.onsuccess(stream, returnBack, idInstance); - currentUserMediaRequest.streams[idInstance] = stream; - currentUserMediaRequest.mutex = false; - if (currentUserMediaRequest.queueRequests.length) - getUserMedia(currentUserMediaRequest.queueRequests.shift()); - } - - if (currentUserMediaRequest.streams[idInstance]) { - streaming(currentUserMediaRequest.streams[idInstance], true); - } else { - n.getMedia = n.webkitGetUserMedia || n.mozGetUserMedia; - n.getMedia(hints, streaming, options.onerror || function(e) { - error(toStr(e)); - }); - } - } - - var FileSender = { - send: function(config) { - var root = config.root; - var channel = config.channel; - var privateChannel = config._channel; - var file = config.file; - - if (!config.file) { - console.error('You must attach/select a file.'); - return; - } - - // max chunk sending limit on chrome is 64k - // max chunk receiving limit on firefox is 16k - var packetSize = (!!navigator.mozGetUserMedia || root.preferSCTP) ? 15 * 1000 : 1 * 1000; - - if (root.chunkSize) { - packetSize = root.chunkSize; - } - - var textToTransfer = ''; - var numberOfPackets = 0; - var packets = 0; - - file.uuid = getRandomString(); - - function processInWebWorker() { - var blob = URL.createObjectURL(new Blob(['function readFile(_file) {postMessage(new FileReaderSync().readAsDataURL(_file));};this.onmessage = function (e) {readFile(e.data);}'], { - type: 'application/javascript' - })); - - var worker = new Worker(blob); - URL.revokeObjectURL(blob); - return worker; - } - - if (!!window.Worker && !isMobileDevice) { - var webWorker = processInWebWorker(); - - webWorker.onmessage = function(event) { - onReadAsDataURL(event.data); - }; - - webWorker.postMessage(file); - } else { - var reader = new FileReader(); - reader.onload = function(e) { - onReadAsDataURL(e.target.result); - }; - reader.readAsDataURL(file); - } - - function onReadAsDataURL(dataURL, text) { - var data = { - type: 'file', - uuid: file.uuid, - maxChunks: numberOfPackets, - currentPosition: numberOfPackets - packets, - name: file.name, - fileType: file.type, - size: file.size - }; - - if (dataURL) { - text = dataURL; - numberOfPackets = packets = data.packets = parseInt(text.length / packetSize); - - file.maxChunks = data.maxChunks = numberOfPackets; - data.currentPosition = numberOfPackets - packets; - - root.onFileStart(file); - } - - root.onFileProgress({ - remaining: packets--, - length: numberOfPackets, - sent: numberOfPackets - packets, - - maxChunks: numberOfPackets, - uuid: file.uuid, - currentPosition: numberOfPackets - packets - }, file.uuid); - - if (text.length > packetSize) data.message = text.slice(0, packetSize); - else { - data.message = text; - data.last = true; - data.name = file.name; - data.extra = root.extra || { }; - - file.url = URL.createObjectURL(file); - root.onFileEnd(file); - } - - channel.send(data, privateChannel); - - textToTransfer = text.slice(data.message.length); - if (textToTransfer.length) { - setTimeout(function() { - onReadAsDataURL(null, textToTransfer); - }, root.chunkInterval || 100); - } - } - } - }; - - function FileReceiver(root) { - var content = { }, - packets = { }, - numberOfPackets = { }; - - function receive(data) { - var uuid = data.uuid; - - if (typeof data.packets !== 'undefined') { - numberOfPackets[uuid] = packets[uuid] = parseInt(data.packets); - root.onFileStart(data); - } - - root.onFileProgress({ - remaining: packets[uuid]--, - length: numberOfPackets[uuid], - received: numberOfPackets[uuid] - packets[uuid], - - maxChunks: numberOfPackets[uuid], - uuid: uuid, - currentPosition: numberOfPackets[uuid] - packets[uuid] - }, uuid); - - if (!content[uuid]) content[uuid] = []; - - content[uuid].push(data.message); - - if (data.last) { - var dataURL = content[uuid].join(''); - - FileConverter.DataURLToBlob(dataURL, data.fileType, function(blob) { - blob.uuid = uuid; - blob.name = data.name; - blob.type = data.fileType; - blob.extra = data.extra || { }; - - blob.url = (window.URL || window.webkitURL).createObjectURL(blob); - - if (root.autoSaveToDisk) { - FileSaver.SaveToDisk(blob.url, data.name); - } - - root.onFileEnd(blob); - - delete content[uuid]; - }); - } - } - - return { - receive: receive - }; - } - - var FileSaver = { - SaveToDisk: function(fileUrl, fileName) { - var hyperlink = document.createElement('a'); - hyperlink.href = fileUrl; - hyperlink.target = '_blank'; - hyperlink.download = fileName || fileUrl; - - var mouseEvent = new MouseEvent('click', { - view: window, - bubbles: true, - cancelable: true - }); - - hyperlink.dispatchEvent(mouseEvent); - (window.URL || window.webkitURL).revokeObjectURL(hyperlink.href); - } - }; - - var FileConverter = { - DataURLToBlob: function(dataURL, fileType, callback) { - - function processInWebWorker() { - var blob = URL.createObjectURL(new Blob(['function getBlob(_dataURL, _fileType) {var binary = atob(_dataURL.substr(_dataURL.indexOf(",") + 1)),i = binary.length,view = new Uint8Array(i);while (i--) {view[i] = binary.charCodeAt(i);};postMessage(new Blob([view], {type: _fileType}));};this.onmessage = function (e) {var data = JSON.parse(e.data); getBlob(data.dataURL, data.fileType);}'], { - type: 'application/javascript' - })); - - var worker = new Worker(blob); - URL.revokeObjectURL(blob); - return worker; - } - - if (!!window.Worker && !isMobileDevice) { - var webWorker = processInWebWorker(); - - webWorker.onmessage = function(event) { - callback(event.data); - }; - - webWorker.postMessage(JSON.stringify({ - dataURL: dataURL, - fileType: fileType - })); - } else { - var binary = atob(dataURL.substr(dataURL.indexOf(',') + 1)), - i = binary.length, - view = new Uint8Array(i); - - while (i--) { - view[i] = binary.charCodeAt(i); - } - - callback(new Blob([view])); - } - } - }; - - var TextSender = { - send: function(config) { - var root = config.root; - - var channel = config.channel, - _channel = config._channel, - initialText = config.text, - packetSize = root.chunkSize || 1000, - textToTransfer = '', - isobject = false; - - if (typeof initialText !== 'string') { - 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); - }, root.chunkInterval || 100); - } - } - } - }; - - // _______________ - // TextReceiver.js - - function TextReceiver(root) { - 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 (root.autoTranslateText) { - e.original = e.data; - root.Translator.TranslateText(e.data, function(translatedText) { - e.data = translatedText; - root.onmessage(e); - }); - } else root.onmessage(e); - - delete content[uuid]; - } - } - - return { - receive: receive - }; - } - - // Sound meter is used to detect speaker - // SoundMeter.js copyright goes to someone else! - - function SoundMeter(config) { - var root = config.root; - var context = config.context; - this.context = context; - this.volume = 0.0; - this.slow_volume = 0.0; - this.clip = 0.0; - - // Legal values are (256, 512, 1024, 2048, 4096, 8192, 16384) - this.script = context.createScriptProcessor(256, 1, 1); - that = this; - - this.script.onaudioprocess = function(event) { - var input = event.inputBuffer.getChannelData(0); - var i; - var sum = 0.0; - var clipcount = 0; - for (i = 0; i < input.length; ++i) { - sum += input[i] * input[i]; - if (Math.abs(input[i]) > 0.99) { - clipcount += 1; - } - } - that.volume = Math.sqrt(sum / input.length); - - var volume = that.volume.toFixed(2); - - if (volume >= .1 && root.onspeaking) { - root.onspeaking(config.event); - } - - if (volume < .1 && root.onsilence) { - root.onsilence(config.event); - } - }; - } - - SoundMeter.prototype.connectToSource = function(stream) { - this.mic = this.context.createMediaStreamSource(stream); - this.mic.connect(this.script); - this.script.connect(this.context.destination); - }; - - SoundMeter.prototype.stop = function() { - this.mic.disconnect(); - this.script.disconnect(); - }; - - - var isChrome = !!navigator.webkitGetUserMedia; - var isFirefox = !!navigator.mozGetUserMedia; - var isMobileDevice = navigator.userAgent.match( /Android|iPhone|iPad|iPod|BlackBerry|IEMobile/i ); - - window.MediaStream = window.MediaStream || window.webkitMediaStream; - window.AudioContext = window.AudioContext || window.webkitAudioContext; - - function getRandomString() { - return (Math.random() * new Date().getTime()).toString(36).replace( /\./g , ''); - } - - var chromeVersion = !!navigator.mozGetUserMedia ? 0 : parseInt(navigator.userAgent.match( /Chrom(e|ium)\/([0-9]+)\./ )[2]); - - function isData(session) { - return !session.audio && !session.video && !session.screen && session.data; - } - - function isEmpty(session) { - var length = 0; - for (var s in session) { - length++; - } - return length == 0; - } - - 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 log() { - if (window.skipRTCMultiConnectionLogs) return; - console.log(Array.prototype.slice.call(arguments).join('\n')); - } - - function error() { - console.error(Array.prototype.slice.call(arguments).join('\n')); - } - - function warn() { - if (window.skipRTCMultiConnectionLogs) return; - console.warn(Array.prototype.slice.call(arguments).join('\n')); - } - - function toStr(obj) { - return JSON.stringify(obj, function(key, value) { - if (value && value.sdp) { - console.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 isAudio = session.audio && !session.video && !session.screen; - if (isChrome && stream.getAudioTracks && stream.getVideoTracks) { - isAudio = stream.getAudioTracks().length && !stream.getVideoTracks().length; - } - - var mediaElement = document.createElement(isAudio ? 'audio' : 'video'); - - // "mozSrcObject" is always preferred over "src"!! - mediaElement[isFirefox ? 'mozSrcObject' : 'src'] = isFirefox ? stream : window.webkitURL.createObjectURL(stream); - - mediaElement.controls = true; - mediaElement.autoplay = !!session.remote; - mediaElement.volume = session.remote ? 1 : 0; - - if (!session.remote) { - mediaElement.muted = true; - } - - mediaElement.play(); - - return mediaElement; - } - - 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; - if (onload) script.onload = onload; - document.documentElement.appendChild(script); - } - - function muteOrUnmute(e) { - var stream = e.stream, - root = e.root, - session = e.session || { }, - enabled = e.enabled; - - if (!session.audio && !session.video) { - session = merge(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('session', JSON.stringify(session, null, '\t')); - - // enable/disable audio/video tracks - - if (session.audio) { - var audioTracks = stream.getAudioTracks()[0]; - if (audioTracks) - audioTracks.enabled = !enabled; - } - - if (session.video) { - var videoTracks = stream.getVideoTracks()[0]; - if (videoTracks) - videoTracks.enabled = !enabled; - } - - // socket message to change media element look - if (root.socket) { - if (root.type == 'local') - root.socket.send({ - userid: root.rtcMultiConnection.userid, - streamid: root.streamid, - mute: !!enabled, - unmute: !enabled, - session: session - }); - - if (root.type == 'remote') - root.socket.send({ - userid: root.rtcMultiConnection.userid, - promptMuteUnmute: true, - streamid: root.streamid, - mute: !!enabled, - unmute: !enabled, - session: session - }); - } - - // According to issue #135, onmute/onumute must be fired for self - root.streamObject.session = session; - if (!!enabled) { - root.rtcMultiConnection.onmute(root.streamObject); - } - - if (!enabled) { - root.rtcMultiConnection.onunmute(root.streamObject); - } - } - - function stopTracks(mediaStream) { - // if getAudioTracks is not implemented - if ((!mediaStream.getAudioTracks || !mediaStream.getVideoTracks) && mediaStream.stop) { - mediaStream.stop(); - return; - } - - var fallback = false, - i; - - // MediaStream.stop should be avoided. It still exist and works but - // it is removed from the spec and instead MediaStreamTrack.stop should be used - var audioTracks = mediaStream.getAudioTracks(); - var videoTracks = mediaStream.getVideoTracks(); - - for (i = 0; i < audioTracks.length; i++) { - if (audioTracks[i].stop) { - // for chrome canary; which has "stop" method; however not functional yet! - try { - audioTracks[i].stop(); - } catch(e) { - fallback = true; - continue; - } - } else { - fallback = true; - continue; - } - } - - for (i = 0; i < videoTracks.length; i++) { - if (videoTracks[i].stop) { - // for chrome canary; which has "stop" method; however not functional yet! - try { - videoTracks[i].stop(); - } catch(e) { - fallback = true; - continue; - } - } else { - fallback = true; - continue; - } - } - - if (fallback && mediaStream.stop) mediaStream.stop(); - } - - - function setDefaults(connection) { - // 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)); - }; - - var progressHelper = { }; - - // www.RTCMultiConnection.org/docs/body/ - connection.body = document.body; - - // www.RTCMultiConnection.org/docs/autoSaveToDisk/ - // to make sure file-saver dialog is not invoked. - connection.autoSaveToDisk = false; - - // 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) { - warn('Now, "autoSaveToDisk" is false. Read more here: http://www.RTCMultiConnection.org/docs/autoSaveToDisk/'); - 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/dontAttachStream/ - connection.dontAttachStream = false; - - // www.RTCMultiConnection.org/docs/onstream/ - connection.onstream = function(e) { - connection.body.insertBefore(e.mediaElement, connection.body.firstChild); - }; - - // www.RTCMultiConnection.org/docs/onstreamended/ - connection.onstreamended = function(e) { - if (e.mediaElement && e.mediaElement.parentNode) { - e.mediaElement.parentNode.removeChild(e.mediaElement); - } - }; - - // www.RTCMultiConnection.org/docs/onmute/ - connection.onmute = function(e) { - if (e.session.video) { - e.mediaElement.setAttribute('poster', '//www.webrtc-experiment.com/images/muted.png'); - } - }; - - // www.RTCMultiConnection.org/docs/onunmute/ - connection.onunmute = function(e) { - if (e.session.video && e.mediaElement) { - e.mediaElement.removeAttribute('poster'); - } - }; - - // www.RTCMultiConnection.org/docs/onleave/ - connection.onleave = function(e) { - log('onleave', toStr(e)); - }; - - connection.token = function() { - // suggested by @rvulpescu from #154 - if (window.crypto) { - 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 , ''); - } - }; - - // www.RTCMultiConnection.org/docs/userid/ - connection.userid = connection.token(); - - // www.RTCMultiConnection.org/docs/peers/ - connection.peers = { }; - connection.peers[connection.userid] = { - drop: function() { - connection.drop(); - } - }; - - connection._skip = ['stop', 'mute', 'unmute', '_private']; - - // 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) { - // implementation from #68 - for (var stream in this) { - if (connection._skip.indexOf(stream) == -1) { - this[stream]._private(session, enabled); - } - } - }, - stop: function(type) { - // connection.streams.stop('local'); - var _stream; - for (var stream in this) { - if (stream != 'stop' && stream != 'mute' && stream != 'unmute' && stream != '_private') { - _stream = this[stream]; - - if (!type) _stream.stop(); - - if (type == 'local' && _stream.type == 'local') - _stream.stop(); - - if (type == 'remote' && _stream.type == 'remote') - _stream.stop(); - } - } - } - }; - - // www.RTCMultiConnection.org/docs/channels/ - connection.channels = { }; - - // www.RTCMultiConnection.org/docs/extra/ - connection.extra = { }; - - // www.RTCMultiConnection.org/docs/session/ - connection.session = { - audio: true, - video: true - }; - - // www.RTCMultiConnection.org/docs/bandwidth/ - connection.bandwidth = { }; - - connection.sdpConstraints = { }; - connection.mediaConstraints = { }; - connection.optionalArgument = { }; - connection.dataChannelDict = { }; - - var iceServers = []; - - if (isFirefox) { - iceServers.push({ - url: 'stun:23.21.150.121' - }); - - iceServers.push({ - url: 'stun:stun.services.mozilla.com' - }); - } - - if (isChrome) { - iceServers.push({ - url: 'stun:stun.l.google.com:19302' - }); - - iceServers.push({ - url: 'stun:stun.anyfirewall.com:3478' - }); - } - - if (isChrome && chromeVersion < 28) { - iceServers.push({ - url: 'turn:homeo@turn.bistri.com:80?transport=udp', - credential: 'homeo' - }); - iceServers.push({ - url: 'turn:homeo@turn.bistri.com:80?transport=tcp', - credential: 'homeo' - }); - } - - if (isChrome && chromeVersion >= 28) { - iceServers.push({ - url: 'turn:turn.bistri.com:80?transport=udp', - credential: 'homeo', - username: 'homeo' - }); - iceServers.push({ - url: 'turn:turn.bistri.com:80?transport=tcp', - credential: 'homeo', - username: 'homeo' - }); - - iceServers.push({ - url: 'turn:turn.anyfirewall.com:443?transport=tcp', - credential: 'webrtc', - username: 'webrtc' - }); - } - connection.iceServers = iceServers; - - // www.RTCMultiConnection.org/docs/preferSCTP/ - connection.preferSCTP = chromeVersion >= 32 ? true : false; - connection.chunkInterval = chromeVersion >= 32 ? 100 : 500; // 500ms for RTP and 100ms for SCTP - connection.chunkSize = chromeVersion >= 32 ? 13 * 1000 : 1000; // 1000 chars for RTP and 13000 chars for SCTP - - if (isFirefox) { - connection.preferSCTP = true; // FF supports only SCTP! - } - - // www.RTCMultiConnection.org/docs/fakeDataChannels/ - connection.fakeDataChannels = false; - - // www.RTCMultiConnection.org/docs/UA/ - connection.UA = { - Firefox: isFirefox, - Chrome: isChrome, - Mobile: isMobileDevice, - Version: chromeVersion - }; - - // file queue: to store previous file objects in memory; - // and stream over newly connected peers - // www.RTCMultiConnection.org/docs/fileQueue/ - connection.fileQueue = { }; - - // www.RTCMultiConnection.org/docs/media/ - connection.media = { - min: function(width, height) { - this.minWidth = width; - this.minHeight = height; - }, - minWidth: 640, - minHeight: 360, - max: function(width, height) { - this.maxWidth = width; - this.maxHeight = height; - }, - maxWidth: 1280, - maxHeight: 720, - bandwidth: 256, - minFrameRate: 1, - maxFrameRate: 30, - minAspectRatio: 1.77 - }; - - // www.RTCMultiConnection.org/docs/candidates/ - connection.candidates = { - host: true, - relay: true, - reflexive: true - }; - - // www.RTCMultiConnection.org/docs/attachStreams/ - connection.attachStreams = []; - - // www.RTCMultiConnection.org/docs/detachStreams/ - connection.detachStreams = []; - - // 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'; - - connection._getStream = function(e) { - return { - rtcMultiConnection: e.rtcMultiConnection, - streamObject: e.streamObject, - stream: e.stream, - userid: e.userid, - streamid: e.streamid, - socket: e.socket, - type: e.type, - mediaElement: e.mediaElement, - stop: function() { - if (this.socket) { - if (this.type == 'local') - this.socket.send({ - userid: this.rtcMultiConnection.userid, - streamid: this.streamid, - stopped: true - }); - - if (this.type == 'remote') - this.socket.send({ - userid: this.rtcMultiConnection.userid, - promptStreamStop: true, - streamid: this.streamid - }); - } - - var stream = this.stream; - if (stream && stream.stop) { - stopTracks(stream); - } - }, - mute: function(session) { - this._private(session, true); - }, - unmute: function(session) { - this._private(session, false); - }, - _private: function(session, enabled) { - muteOrUnmute({ - root: this, - session: session, - enabled: enabled, - stream: this.stream - }); - }, - startRecording: function(session) { - if (!session) - session = { - audio: true, - video: true - }; - - if (isFirefox) { - // https://www.webrtc-experiment.com/RecordRTC/AudioVideo-on-Firefox.html - session = { audio: true }; - } - - if (!window.RecordRTC) { - var self = this; - return loadScript('//www.webrtc-experiment.com/RecordRTC.js', function() { - self.startRecording(session); - }); - } - - this.recorder = new MRecordRTC(); - this.recorder.mediaType = session; - this.recorder.addStream(this.stream); - this.recorder.startRecording(); - }, - stopRecording: function(callback) { - this.recorder.stopRecording(); - this.recorder.getBlob(function(blob) { - callback(blob.audio || blob.video, blob.video); - }); - } - }; - }; - - // new RTCMultiConnection().set({properties}).connect() - connection.set = function(properties) { - for (var property in properties) { - this[property] = properties[property]; - } - return this; - }; - - // www.RTCMultiConnection.org/docs/firebase/ - connection.firebase = 'rtcweb'; - - // www.RTCMultiConnection.org/docs/onMediaError/ - connection.onMediaError = function(_error) { - error(_error); - }; - - // www.RTCMultiConnection.org/docs/stats/ - connection.stats = { }; - - // www.RTCMultiConnection.org/docs/getStats/ - connection.getStats = function(callback) { - var numberOfConnectedUsers = 0; - for (var peer in this.peers) { - numberOfConnectedUsers++; - } - - this.stats.numberOfConnectedUsers = numberOfConnectedUsers; - - // numberOfSessions - - if (callback) callback(this.stats); - }; - - // www.RTCMultiConnection.org/docs/caniuse/ - connection.caniuse = { - RTCPeerConnection: !!RTCPeerConnection, - getUserMedia: !!getUserMedia, - AudioContext: !!AudioContext, - - // there is no way to check whether "getUserMedia" flag is enabled or not! - ScreenSharing: isChrome && chromeVersion >= 26 && location.protocol == 'https:', - checkIfScreenSharingFlagEnabled: function(callback) { - var warning; - if (isFirefox) { - warning = 'Screen sharing is NOT supported on Firefox.'; - error(warning); - if (callback) callback(false); - } - - if (location.protocol !== 'https:') { - warning = 'Screen sharing is NOT supported on ' + location.protocol + ' Try https!'; - error(warning); - if (callback) return callback(false); - } - - if (chromeVersion < 26) { - warning = 'Screen sharing support is suspicious!'; - warn(warning); - } - - var screen_constraints = { - video: { - mandatory: { - chromeMediaSource: 'screen' - } - } - }; - - var invocationInterval = 0, - stop; - (function selfInvoker() { - invocationInterval++; - if (!stop) setTimeout(selfInvoker, 10); - })(); - - navigator.webkitGetUserMedia(screen_constraints, onsuccess, onfailure); - - function onsuccess(stream) { - if (stream.stop) { - stream.stop(); - } - - if (callback) { - callback(true); - } - } - - function onfailure() { - stop = true; - if (callback) callback(invocationInterval > 5, warning); - } - }, - - RtpDataChannels: isChrome && chromeVersion >= 25, - SctpDataChannels: isChrome && chromeVersion >= 31 - }; - - // www.RTCMultiConnection.org/docs/snapshots/ - connection.snapshots = { }; - - // www.RTCMultiConnection.org/docs/takeSnapshot/ - connection.takeSnapshot = function(userid, callback) { - for (var stream in this.streams) { - stream = this.streams[stream]; - if (stream.userid == userid) { - var video = stream.streamObject.mediaElement; - 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); - - this.snapshots[userid] = canvas.toDataURL(); - callback && callback(this.snapshots[userid]); - continue; - } - } - }; - - 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.WebRTC-Experiment.com/demos/MediaStreamTrack.getSources.html - connection._mediaSources = { }; - - // 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/devices/ - connection.devices = { }; - - // www.RTCMultiConnection.org/docs/getDevices/ - connection.getDevices = function(callback) { - if (!!window.MediaStreamTrack && !!MediaStreamTrack.getSources) { - MediaStreamTrack.getSources(function(media_sources) { - var sources = []; - for (var i = 0; i < media_sources.length; i++) { - sources.push(media_sources[i]); - } - - getAllUserMedias(sources); - - if (callback) callback(connection.devices); - }); - - var index = 0; - - function getAllUserMedias(media_sources) { - var media_source = media_sources[index]; - if (!media_source) return; - - connection.devices[media_source.id] = media_source; - - index++; - getAllUserMedias(media_sources); - } - } - }; - - // 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 || { }; - this.attachStreams = []; - - // "drop" should detach all local streams - for (var stream in this.streams) { - if (this._skip.indexOf(stream) == -1) { - stream = this.streams[stream]; - if (stream.type == 'local') { - this.detachStreams.push(stream.streamid); - this.onstreamended(stream.streamObject); - } else this.onstreamended(stream.streamObject); - } - } - - // www.RTCMultiConnection.org/docs/sendCustomMessage/ - this.sendCustomMessage({ - drop: true, - dontRenegotiate: typeof config.renegotiate == 'undefined' ? true : config.renegotiate - }); - }; - - // used for SoundMeter - if (!!window.AudioContext) { - connection._audioContext = new AudioContext(); - } - - // 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 API key; if possible - connection.googKey = 'AIzaSyCUmCjvKRb-kOYrnoL2xaXb8I-_JJeKpf0'; - - // 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); - } - }; - - 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); - } - }; - } -})(); diff --git a/RTCMultiConnection/Library/RTCMultiConnection-v1.7.js b/RTCMultiConnection/Library/RTCMultiConnection-v1.7.js deleted file mode 100644 index 26462c65..00000000 --- a/RTCMultiConnection/Library/RTCMultiConnection-v1.7.js +++ /dev/null @@ -1,4449 +0,0 @@ -// Last time updated at May 12, 2014, 08:32:23 -// Latest file can be found here: https://www.webrtc-experiment.com/RTCMultiConnection-v1.7.js -// Muaz Khan - www.MuazKhan.com -// MIT License - www.WebRTC-Experiment.com/licence -// Documentation - www.RTCMultiConnection.org/docs -// FAQ - www.RTCMultiConnection.org/FAQ -// v1.7 changes log - www.RTCMultiConnection.org/changes-log/#v1.7 -// Demos - www.WebRTC-Experiment.com/RTCMultiConnection -// _______________________ -// RTCMultiConnection-v1.7 -/* issues/features need to be fixed & implemented: - --. "channel" object in the openSignalingChannel shouldn't be mandatory! --. JSON parse/stringify options for data transmitted using data-channels; e.g. connection.preferJSON = true; --. "onspeaking" and "onsilence" fires too often! --. removeTrack() and addTracks() instead of "stop" --. voice translation using Translator.js -*/ - -(function () { - // www.RTCMultiConnection.org/docs/constructor/ - window.RTCMultiConnection = function (channel) { - // a reference to your constructor! - var connection = this; - - // www.RTCMultiConnection.org/docs/channel-id/ - connection.channel = channel || location.href.replace(/\/|:|#|%|\.|\[|\]/g, ''); - - var rtcMultiSession; // a reference to backbone object i.e. RTCMultiSession! - - // 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 (typeof args == 'string') { - connection.sessionid = args; - } else { - if (typeof args.transmitRoomOnce != 'undefined') { - connection.transmitRoomOnce = args.transmitRoomOnce; - } - - if (typeof args.dontTransmit != 'undefined') { - dontTransmit = args.dontTransmit; - } - - if (typeof args.sessionid != 'undefined') { - 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.stats.sessions[connection.sessionDescription.sessionid]) { - connection.stats.numberOfSessions++; - connection.stats.sessions[connection.sessionDescription.sessionid] = connection.sessionDescription; - } - - // verify to see if "openSignalingChannel" exists! - prepareSignalingChannel(function () { - // connect with signaling channel - initRTCMultiSession(function () { - // for session-initiator, user-media is captured as soon as "open" is invoked. - captureUserMedia(function () { - rtcMultiSession.initSession({ - sessionDescription: connection.sessionDescription, - dontTransmit: dontTransmit - }); - }); - }); - }); - return connection.sessionDescription; - }; - - // www.RTCMultiConnection.org/docs/connect/ - this.connect = function (sessionid) { - // a channel can contain multiple rooms i.e. sessions - if (sessionid) { - connection.sessionid = sessionid; - } - - // verify to see if "openSignalingChannel" exists! - prepareSignalingChannel(function () { - // connect with signaling channel - initRTCMultiSession(function () { - log('Signaling channel is ready.'); - }); - }); - - return this; - }; - - // www.RTCMultiConnection.org/docs/join/ - this.join = joinSession; - - // www.RTCMultiConnection.org/docs/send/ - this.send = function (data, _channel) { - // 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.forEach) { - // 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++) { - connection.send(data[i], _channel); - } - return; - } - - // File or Blob object MUST have "type" and "size" properties - if (typeof data.size != 'undefined' && typeof data.type != 'undefined') { - // to send multiple files concurrently! - // file of any size; maximum length: 1GB - FileSender.send({ - file: data, - channel: rtcMultiSession, - _channel: _channel, - connection: connection - }); - } 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 - }); - } - }; - - // this method checks to verify "openSignalingChannel" method - // github.com/muaz-khan/WebRTC-Experiment/blob/master/Signaling.md - - function prepareSignalingChannel(callback) { - if (connection.openSignalingChannel) return callback(); - - // make sure firebase.js is loaded before using their JavaScript API - if (!window.Firebase) { - return loadScript('https://www.webrtc-experiment.com/firebase.js', function () { - prepareSignalingChannel(callback); - }); - } - - // Single socket is a preferred solution! - var socketCallbacks = {}; - var firebase = new Firebase('https://' + connection.firebase + '.firebaseio.com/' + connection.channel); - firebase.on('child_added', function (snap) { - var data = snap.val(); - if (data.sender == connection.userid) return; - - if (socketCallbacks[data.channel]) { - socketCallbacks[data.channel](data.message); - } - snap.ref().remove(); - }); - - // www.RTCMultiConnection.org/docs/openSignalingChannel/ - connection.openSignalingChannel = function (args) { - var callbackid = args.channel || connection.channel; - socketCallbacks[callbackid] = args.onmessage; - - if (args.onopen) setTimeout(args.onopen, 1000); - return { - send: function (message) { - firebase.push({ - sender: connection.userid, - channel: callbackid, - message: message - }); - }, - channel: channel // todo: remove this "channel" object - }; - }; - - callback(); - } - - function initRTCMultiSession(onSignalingReady) { - // 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); - } - - function joinSession(session) { - if (!rtcMultiSession) { - log('Signaling channel is not ready. Connecting...'); - // verify to see if "openSignalingChannel" exists! - prepareSignalingChannel(function () { - // connect with signaling channel - initRTCMultiSession(function () { - log('Signaling channel is connected. Joining the session again...'); - setTimeout(function () { - joinSession(session); - }, 1000); - }); - }); - return; - } - - // connection.join('sessionid'); - if (typeof session == 'string') { - if (connection.stats.sessions[session]) { - session = connection.stats.sessions[session]; - } else - return setTimeout(function () { - log('Session-Descriptions not found. Rechecking..'); - joinSession(session); - }, 1000); - } - - if (!session || !session.userid || !session.sessionid) - throw 'invalid data passed over "join" method'; - - if (!connection.dontOverrideSession) { - connection.session = session.session; - } - - 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) { - // capture user's media resources - var session = _session || connection.session; - - if (isEmpty(session)) { - if (callback) callback(); - return; - } - - // it is possible to check presence of the microphone before using it! - if (isChrome && session.audio && !DetectRTC.hasMicrophone) { - warn('It seems that you have no microphone attached to your device/system.'); - session.audio = connection.session.audio = false; - } - - // it is possible to check presence of the webcam before using it! - if (isChrome && session.video && !DetectRTC.hasWebcam) { - warn('It seems that you have no webcam attached to your device/system.'); - session.video = connection.session.video = false; - } - - // you can force to skip media capturing! - if (connection.dontAttachStream) - 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, - video: !!session.video - }; - - // if custom audio device is selected - if (connection._mediaSources.audio) { - constraints.audio = { - optional: [{ - sourceId: connection._mediaSources.audio - }] - }; - } - - // if custom video device is selected - if (connection._mediaSources.video) { - constraints.video = { - optional: [{ - sourceId: connection._mediaSources.video - }] - }; - } - - var screen_constraints = { - audio: false, - video: { - mandatory: { - chromeMediaSource: 'screen' - }, - optional: [] - } - }; - - // if screen is prompted - if (session.screen) { - 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) { - var mediaConfig = { - onsuccess: function (stream, returnBack, idInstance, streamid) { - if (isRemoveVideoTracks && isChrome) { - stream = new window.webkitMediaStream(stream.getAudioTracks()); - } - - // var streamid = getRandomString(); - connection.localStreamids.push(streamid); - stream.onended = function () { - connection.onstreamended(streamedObject); - - // 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, - userid: _stream.rtcMultiConnection.userid, - extra: _stream.rtcMultiConnection.extra, - stopped: true - }); - }); - } - - currentUserMediaRequest.mutex = false; - // to make sure same stream can be captured again! - if (currentUserMediaRequest.streams[idInstance]) { - delete currentUserMediaRequest.streams[idInstance]; - } - }; - - var mediaElement = createMediaElement(stream, session); - - mediaElement.muted = true; - - stream.streamid = streamid; - - var streamedObject = { - stream: stream, - streamid: streamid, - mediaElement: mediaElement, - blobURL: mediaElement.mozSrcObject || mediaElement.src, - type: 'local', - userid: connection.userid, - extra: connection.extra, - session: session, - isVideo: stream.getVideoTracks().length > 0, - isAudio: !stream.getVideoTracks().length && stream.getAudioTracks().length > 0, - isInitiator: !!connection.isInitiator - }; - - var sObject = { - stream: stream, - userid: connection.userid, - streamid: streamid, - session: session, - type: 'local', - streamObject: streamedObject, - mediaElement: mediaElement, - rtcMultiConnection: connection - }; - - if (isFirstSession) { - connection.attachStreams.push(stream); - } - isFirstSession = false; - - connection.streams[streamid] = connection._getStream(sObject); - - if (!returnBack) { - connection.onstream(streamedObject); - } - - if (connection.setDefaultEventsForMediaElement) { - connection.setDefaultEventsForMediaElement(mediaElement, streamid); - } - - if (forcedCallback) forcedCallback(stream, streamedObject); - - if (connection.onspeaking) { - var soundMeter = new SoundMeter({ - context: connection._audioContext, - connection: connection, - event: streamedObject - }); - soundMeter.connectToSource(stream); - } - }, - onerror: function (e, idInstance) { - connection.onMediaError(toStr(e)); - - if (session.audio) { - connection.onMediaError('Maybe microphone access is denied.'); - } - - if (session.video) { - connection.onMediaError('Maybe webcam access is denied.'); - } - - if (session.screen) { - if (isFirefox) { - connection.onMediaError('Firefox has not yet released their screen capturing modules. Still work in progress! Please try chrome for now!'); - } else if (location.protocol !== 'https:') { - connection.onMediaError(' is mandatory to capture screen.'); - } else { - connection.onMediaError('Unable to detect actual issue. Maybe "deprecated" screen capturing flag is not enabled or maybe you clicked "No" button.'); - } - - currentUserMediaRequest.mutex = false; - - // to make sure same stream can be captured again! - if (currentUserMediaRequest.streams[idInstance]) { - delete currentUserMediaRequest.streams[idInstance]; - } - } - }, - mediaConstraints: connection.mediaConstraints || {} - }; - - mediaConfig.constraints = forcedConstraints || constraints; - mediaConfig.media = connection.media; - mediaConfig.connection = connection; - getUserMedia(mediaConfig); - } - } - - // www.RTCMultiConnection.org/docs/captureUserMedia/ - this.captureUserMedia = captureUserMedia; - - // www.RTCMultiConnection.org/docs/leave/ - this.leave = function (userid) { - isFirstSession = true; - - // eject a user; or leave the session - rtcMultiSession.leave(userid); - }; - - // www.RTCMultiConnection.org/docs/eject/ - this.eject = function (userid) { - if (!connection.isInitiator) throw 'Only session-initiator can eject a user.'; - connection.leave(userid); - }; - - // www.RTCMultiConnection.org/docs/close/ - this.close = function () { - // close entire session - connection.autoCloseEntireSession = true; - connection.leave(); - }; - - // www.RTCMultiConnection.org/docs/renegotiate/ - this.renegotiate = function (stream, session) { - rtcMultiSession.addStream({ - renegotiate: session || { - oneway: true, - audio: true, - video: true - }, - stream: stream - }); - }; - - // www.RTCMultiConnection.org/docs/addStream/ - this.addStream = function (session, socket) { - // www.RTCMultiConnection.org/docs/renegotiation/ - - // 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/ - this.removeStream = function (streamid) { - // detach pre-attached streams - if (!this.streams[streamid]) return warn('No such stream exists. Stream-id:', streamid); - - // www.RTCMultiConnection.org/docs/detachStreams/ - this.detachStreams.push(streamid); - this.renegotiate(); - }; - - // set RTCMultiConnection defaults on constructor invocation - setDefaults(this); - }; - - function RTCMultiSession(connection, callbackForSignalingReady) { - var fileReceiver = new FileReceiver(connection); - var textReceiver = new TextReceiver(connection); - - function onDataChannelMessage(e) { - if (!e) return; - - e = JSON.parse(e); - - if (e.data.type === 'text') { - textReceiver.receive(e.data, e.userid, e.extra); - } else if (typeof e.data.maxChunks != 'undefined') { - fileReceiver.receive(e.data); - } 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) { - // 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 isDontAttachStream = connection.dontAttachStream; - - connection.dontAttachStream = false; - connection.captureUserMedia(function () { - connection.dontAttachStream = true; - connection.join(session); - - // returning back previous state - connection.dontAttachStream = isDontAttachStream; - }, forceSession); - }; - if (!session.extra) session.extra = {}; - - return connection.onNewSession(session); - } - - connection.join(session); - } - - var socketObjects = {}; - var sockets = []; - - var rtcMultiSession = this; - - var participants = {}; - - 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 "onstreamended" 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); - } - }; - - socketConfig.callback = function (_socket) { - socket = _socket; - socketConfig.onopen(); - }; - - var socket = connection.openSignalingChannel(socketConfig), - isofferer = _config.isofferer, - peer; - - var peerConfig = { - onopen: onChannelOpened, - onicecandidate: function (candidate) { - if (!connection.candidates) throw 'ICE candidates are mandatory.'; - if (!connection.candidates.host && candidate.candidate.indexOf('typ host ') != -1) return; - if (!connection.candidates.relay && candidate.candidate.indexOf('typ relay ') != -1) return; - if (!connection.candidates.reflexive && candidate.candidate.indexOf('typ srflx ') != -1) return; - - log(candidate.candidate); - - socket && socket.send({ - userid: connection.userid, - candidate: { - sdpMLineIndex: candidate.sdpMLineIndex, - candidate: JSON.stringify(candidate.candidate) - } - }); - }, - onmessage: onDataChannelMessage, - onaddstream: function (stream, session) { - session = session || _config.renegotiate || connection.session; - - // if it is Firefox; then return. - if (isData(session)) return; - - if (_config.streaminfo) { - var streaminfo = _config.streaminfo.split('----'); - for (var i = 0; i < streaminfo.length; i++) { - stream.streamid = streaminfo[i]; - } - - _config.streaminfo = swap(streaminfo.pop()).join('----'); - } - - var mediaElement = createMediaElement(stream, merge({ - remote: true - }, session)); - _config.stream = stream; - - if (!stream.getVideoTracks().length) - mediaElement.addEventListener('play', function () { - setTimeout(function () { - mediaElement.muted = false; - afterRemoteStreamStartedFlowing(mediaElement, session); - }, 3000); - }, false); - else - waitUntilRemoteStreamStartsFlowing(mediaElement, session); - - if (connection.setDefaultEventsForMediaElement) { - connection.setDefaultEventsForMediaElement(mediaElement, stream.streamid); - } - - // to allow this user join all existing users! - if (connection.isInitiator && getLength(participants) > 1 && getLength(participants) <= connection.maxParticipantsAllowed) { - if (!connection.session.oneway && !connection.session.broadcast) { - defaultSocket.send({ - joinUsers: participants, - userid: connection.userid, - extra: connection.extra - }); - } - } - }, - - onremovestream: function (event) { - warn('onremovestream', event); - }, - - 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 (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') { - if(isFirefox || _config.targetBrowser == 'gecko') { - warn('ICE connectivity check is failed. Re-establishing peer connection.'); - connection.peers[_config.userid].redial(); - } - else { - warn('ICE connectivity check is failed. Renegotiating peer connection.'); - connection.peers[_config.userid].renegotiate(); - } - } - - if (connection.peers[_config.userid] && connection.peers[_config.userid].peer.connection.iceConnectionState == 'disconnected') { - // to make sure this user's all remote streams are removed. - for (var stream in connection.streams) { - stream = connection.streams[stream]; - if (stream.userid == _config.userid && stream.type == 'remote') { - connection.onstreamended(stream.streamObject); - } - } - - connection.remove(_config.userid); - } - - 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({ - userid: connection.userid, - extra: connection.extra || {}, - redial: true - }); - - // to make sure all old "remote" streams are also removed! - for (var stream in connection.streams) { - stream = connection.streams[stream]; - if (stream.userid == _config.userid && stream.type == 'remote') { - connection.onstreamended(stream.streamObject); - } - } - } - } - }, - - onsignalingstatechange: function (event) { - log('onsignalingstatechange', toStr(event)); - }, - - attachStreams: connection.attachStreams, - iceServers: connection.iceServers, - 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 - }); - }, - - socket: socket, - selfUserid: connection.userid, - trickleIce: connection.trickleIce - }; - - function waitUntilRemoteStreamStartsFlowing(mediaElement, session, numberOfTimes) { - if (!numberOfTimes) numberOfTimes = 0; - numberOfTimes++; - - if (!(mediaElement.readyState <= HTMLMediaElement.HAVE_CURRENT_DATA || mediaElement.paused || mediaElement.currentTime <= 0)) { - afterRemoteStreamStartedFlowing(mediaElement, session); - } else { - if (numberOfTimes >= 60) { // wait 60 seconds while video is delivered! - socket.send({ - userid: connection.userid, - extra: connection.extra, - failedToReceiveRemoteVideo: true, - streamid: _config.stream.streamid - }); - } else - setTimeout(function () { - log('Waiting for incoming remote stream to be started flowing: ' + numberOfTimes + ' seconds.'); - waitUntilRemoteStreamStartsFlowing(mediaElement, session, numberOfTimes); - }, 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(mediaElement, session) { - var stream = _config.stream; - - stream.onended = function () { - connection.onstreamended(streamedObject); - }; - - var streamedObject = { - mediaElement: mediaElement, - - stream: stream, - streamid: stream.streamid, - session: session || connection.session, - - blobURL: mediaElement.mozSrcObject || mediaElement.src, - type: 'remote', - - extra: _config.extra, - userid: _config.userid, - - isVideo: stream.getVideoTracks().length > 0, - isAudio: !stream.getVideoTracks().length && stream.getAudioTracks().length > 0, - isInitiator: !!_config.isInitiator - }; - - // connection.streams['stream-id'].mute({audio:true}) - connection.streams[stream.streamid] = connection._getStream({ - stream: stream, - userid: _config.userid, - streamid: stream.streamid, - socket: socket, - type: 'remote', - streamObject: streamedObject, - mediaElement: mediaElement, - rtcMultiConnection: connection, - session: session || connection.session - }); - - connection.onstream(streamedObject); - - onSessionOpened(); - - if (connection.onspeaking) { - var soundMeter = new SoundMeter({ - context: connection._audioContext, - connection: connection, - event: streamedObject - }); - soundMeter.connectToSource(stream); - } - } - - 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 - }); - - // fetch files from file-queue - for (var q in connection.fileQueue) { - connection.send(connection.fileQueue[q], channel); - } - - if (isData(connection.session)) onSessionOpened(); - } - - 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.stats.numberOfConnectedUsers++; - // connection.peers['user-id'].addStream({audio:true}) - connection.peers[_config.userid] = { - socket: socket, - peer: peer, - userid: _config.userid, - extra: _config.extra, - targetBrowser: _config.targetBrowser, - 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 (typeof bandwidth == 'string') 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({ - userid: connection.userid, - extra: connection.extra || {}, - changeBandwidth: true, - bandwidth: bandwidth - }); - }, - sendCustomMessage: function (message) { - // connection.peers['user-id'].sendCustomMessage(); - - this.socket.send({ - userid: connection.userid, - extra: connection.extra || {}, - customMessage: true, - message: message - }); - }, - onCustomMessage: function (message) { - log('Received "private" message from', this.userid, - typeof message == 'string' ? 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); - connection.onstreamended(stream.streamObject); - } - - if (stream.type == 'remote' && stream.userid == this.userid) { - connection.onstreamended(stream.streamObject); - } - } - } - - !dontSendMessage && this.socket.send({ - userid: connection.userid, - extra: connection.extra || {}, - drop: true - }); - }, - hold: function (holdMLine) { - // connection.peers['user-id'].hold(); - - this.socket.send({ - userid: connection.userid, - extra: connection.extra || {}, - 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(); - - this.socket.send({ - userid: connection.userid, - extra: connection.extra || {}, - 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.streamObject)); - - // www.RTCMultiConnection.org/docs/onunhold/ - if (!isHold) - connection.onunhold(merge({ - kind: kind - }, stream.streamObject)); - } - } - } - }, - 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') { - connection.onstreamended(stream.streamObject); - } - } - } - - log('ReDialing...'); - - socket.send({ - userid: connection.userid, - extra: connection.extra, - recreatePeer: true - }); - - peer = new PeerConnection(); - peer.create('offer', peerConfig); - }, - sharePartOfScreen: function (args) { - // www.RTCMultiConnection.org/docs/onpartofscreen/ - - var element = args.element; - var that = this; - - if (!window.html2canvas) { - return loadScript('https://www.webrtc-experiment.com/screenshot.js', function () { - that.sharePartOfScreen(args); - }); - } - - if (typeof element == 'string') { - element = document.querySelector(element); - if (!element) element = document.getElementById(element); - } - if (!element) throw 'HTML Element is inaccessible!'; - - 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); - } - - // html2canvas.js is used to take screenshots - html2canvas(element, { - onrendered: function (canvas) { - var screenshot = canvas.toDataURL(); - - 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({ - userid: connection.userid, - extra: connection.extra, - screenshot: screenshot, - isPartOfScreen: true - }); - } - - // "once" can be used to share single screenshot - !args.once && setTimeout(partOfScreenCapturer, args.interval || 200); - } - }); - } - - partOfScreenCapturer(); - } - }; - } - - 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, - userid: connection.userid, - extra: connection.extra, - 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.targetBrowser == 'chromium' && !_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 (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.targetBrowser = response.targetBrowser; - - 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({ - sdpMLineIndex: response.candidate.sdpMLineIndex, - candidate: JSON.parse(response.candidate.candidate) - }); - } - - if (response.mute || response.unmute) { - if (response.promptMuteUnmute) { - 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].streamObject; - } - - var session = response.session; - var fakeObject = merge({}, streamObject); - fakeObject.session = session; - fakeObject.isAudio = session.audio && !session.video; - fakeObject.isVideo = (!session.audio && session.video) || (session.audio && session.video); - - 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]) { - connection.onstreamended(connection.streams[response.streamid].streamObject); - } - } - - // to stop remote stream - if (response.promptStreamStop /* && !connection.isInitiator */ ) { - // var forceToStopRemoteStream = true; - // connection.streams['remote-stream-id'].stop( forceToStopRemoteStream ); - 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) { - stopTracks(stream); - stream.stream.onended(stream.streamObject); - } - } - } - - if (peer && peer.connection) { - if (peer.connection.signalingState != 'closed') { - peer.connection.close(); - } - peer.connection = null; - } - - if (response.closeEntireSession) { - connection.close(); - connection.refresh(); - } else if (socket && response.ejected) { - // if user is ejected; his stream MUST be removed - // from all other users' side - socket.send({ - left: true, - extra: connection.extra, - userid: connection.userid - }); - - if (sockets[_config.socketIndex]) - delete sockets[_config.socketIndex]; - if (socketObjects[socket.channel]) - delete socketObjects[socket.channel]; - - socket = null; - } - - connection.remove(response.userid); - - if (participants[response.userid]) delete participants[response.userid]; - - connection.onleave({ - userid: response.userid, - extra: response.extra, - entireSessionClosed: !!response.closeEntireSession - }); - } - - // keeping session active even if initiator leaves - if (response.playRoleOfBroadcaster) { - if (response.extra) { - connection.extra = merge(connection.extra, response.extra); - } - setTimeout(connection.playRoleOfInitiator, 2000); - } - - if (response.isCreateDataChannel) { - if (isFirefox) { - peer.createDataChannel(); - } - } - - 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.'; - 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) { - if (!connection.peers[response.userid]) throw 'No such peer exists.'; - connection.peers[response.userid].peer.hold = true; - connection.peers[response.userid].peer.holdMLine = response.holdMLine; - connection.peers[response.userid].renegotiate(); - - connection.peers[response.userid].fireHoldUnHoldEvents({ - kind: response.holdMLine, - isHold: true, - userid: response.userid - }); - } - - if (response.unhold) { - if (!connection.peers[response.userid]) throw 'No such peer exists.'; - connection.peers[response.userid].peer.hold = false; - connection.peers[response.userid].peer.holdMLine = response.holdMLine; - connection.peers[response.userid].renegotiate(); - - connection.peers[response.userid].fireHoldUnHoldEvents({ - kind: response.holdMLine, - isHold: false, - userid: response.userid - }); - } - - // 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.dontAttachStream = true; - connection.open(); - sockets = swap(sockets); - connection.dontAttachStream = false; - }; - - connection.askToShareParticipants = function () { - defaultSocket && defaultSocket.send({ - userid: connection.userid, - extra: connection.extra, - 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) { - log(sdp.type, sdp.sdp); - - 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; - - if (isChrome || (isFirefox && !peer.connection.getLocalStreams().length)) { - peer.connection.addStream(stream); - } - - connection.renegotiatedSessions[JSON.stringify(_config.renegotiate)] = { - session: _config.renegotiate, - stream: stream - }; - - delete _config.renegotiate; - - createAnswer(); - }, _config.renegotiate); - } - - function createAnswer() { - // because gecko has no support of renegotiation yet; - // so both chrome and firefox should redial instead of renegotiate! - if (isFirefox || _config.targetBrowser == 'gecko') { - if (connection.peers[_config.userid]) { - connection.peers[_config.userid].redial(); - } - return; - } - - peer.recreateAnswer(sdp, session, function (_sdp, streaminfo) { - sendsdp({ - sdp: _sdp, - socket: socket, - streaminfo: streaminfo - }); - }); - } - } - } - - 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({ - userid: connection.userid, - sdp: JSON.stringify(e.sdp), - extra: connection.extra, - renegotiate: !!e.renegotiate ? e.renegotiate : false, - streaminfo: e.streaminfo || '', - labels: e.labels || [], - preferSCTP: !!connection.preferSCTP, - fakeDataChannels: !!connection.fakeDataChannels, - isInitiator: !!connection.isInitiator, - targetBrowser: isFirefox ? 'gecko' : 'chromium' - }); - } - - // sharing new user with existing participants - - function onNewParticipant(response) { - // todo: make sure this works as expected. - // if(connection.sessionid && response.sessionid != connection.sessionid) return; - - var channel = response.newParticipant; - - if (!channel || !!participants[channel] || channel == connection.userid) - return; - - participants[channel] = channel; - - 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, - userid: connection.userid, - targetUser: channel, - channel: new_channel, - extra: connection.extra - }); - } - - // if a user leaves - - function clearSession(channel) { - connection.stats.numberOfConnectedUsers--; - - var alert = { - left: true, - extra: connection.extra, - userid: connection.userid, - sessionid: connection.sessionid - }; - - if (connection.isInitiator) { - if (connection.autoCloseEntireSession) { - alert.closeEntireSession = true; - } else if (sockets[0]) { - sockets[0].send({ - playRoleOfBroadcaster: true, - userid: connection.userid - }); - } - } - - if (!channel) { - var length = sockets.length; - for (var i = 0; i < length; i++) { - socket = sockets[i]; - if (socket) { - socket.send(alert); - - if (socketObjects[socket.channel]) - delete socketObjects[socket.channel]; - - delete sockets[i]; - } - } - } - - // eject a specific user! - if (channel) { - socket = socketObjects[channel]; - if (socket) { - alert.ejected = true; - socket.send(alert); - - if (sockets[socket.index]) - delete sockets[socket.index]; - - delete socketObjects[channel]; - } - } - - sockets = swap(sockets); - } - - // 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) { - connection.onstreamended(stream.streamObject); - if (stream.stop) stream.stop(); - 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 = []; - connection.isAcceptNewSession = true; - - // to stop/remove self streams - for (var i = 0; i < connection.attachStreams.length; i++) { - stopTracks(connection.attachStreams[i]); - } - connection.attachStreams = []; - - // to allow capturing of identical streams - currentUserMediaRequest = { - streams: [], - mutex: false, - queueRequests: [] - }; - - // to make sure remote streams are also removed! - for (var stream in connection.streams) { - if (connection._skip.indexOf(stream) == -1) { - connection.onstreamended(connection.streams[stream].streamObject); - delete connection.streams[stream]; - } - } - - rtcMultiSession.isOwnerLeaving = true; - connection.isInitiator = false; - }; - - // www.RTCMultiConnection.org/docs/reject/ - connection.reject = function (userid) { - if (typeof userid != 'string') userid = userid.userid; - defaultSocket.send({ - rejectedRequestOf: userid, - userid: connection.userid, - extra: connection.extra || {} - }); - }; - - window.addEventListener('beforeunload', function () { - clearSession(); - }, false); - - window.addEventListener('keyup', function (e) { - if (e.keyCode == 116) - clearSession(); - }, false); - - 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.send({ - userid: connection.userid, - extra: connection.extra, - 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 - }); - } - } - } - - // 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 = connection.openSignalingChannel({ - onmessage: function (response) { - if (response.userid == connection.userid) return; - - if (response.sessionid && response.userid) { - if (!connection.stats.sessions[response.sessionid]) { - connection.stats.numberOfSessions++; - connection.stats.sessions[response.sessionid] = response; - } - } - - if (connection.isAcceptNewSession && response.sessionid && response.userid) { - if (!connection.dontOverrideSession) { - connection.session = response.session; - } - - onNewSession(response); - } - - if (response.newParticipant && !connection.isAcceptNewSession && rtcMultiSession.broadcasterid === response.userid) { - onNewParticipant(response); - } - - if (getLength(participants) < connection.maxParticipantsAllowed && response.userid && response.targetUser == connection.userid && response.participant && !participants[response.userid]) { - // because broadcaster already have anonymous user in "participants" array - // that's why this code isn't executed! - acceptRequest(response); - } - - if (response.acceptedRequestOf == connection.userid) { - if (connection.onstats) connection.onstats('accepted', response); - } - - if (response.rejectedRequestOf == connection.userid) { - if (connection.onstats) connection.onstats('rejected', response); - } - - 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); - connection.onstreamended(stream.streamObject); - } else connection.onstreamended(stream.streamObject); - } - } - - if (response.message.renegotiate) { - // renegotiate; so "peer.removeStream" happens. - connection.addStream(); - } - } else if (connection.onCustomMessage) { - connection.onCustomMessage(response.message); - } - } - - if (connection.isInitiator && response.searchingForRooms) { - defaultSocket.send({ - userid: connection.userid, - extra: connection.extra, - sessionDescription: connection.sessionDescription, - responseFor: response.userid - }); - } - - if (response.sessionDescription && response.responseFor == connection.userid) { - var sessionDescription = response.sessionDescription; - if (!connection.stats.sessions[sessionDescription.sessionid]) { - connection.stats.numberOfSessions++; - connection.stats.sessions[sessionDescription.sessionid] = sessionDescription; - } - } - - if (connection.isInitiator && response.askToShareParticipants) { - 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 && connection.userid != response.dontShareWith) { - joinParticipants(response.joinUsers); - } - } - }, - callback: function (socket) { - if (socket) defaultSocket = socket; - if (onSignalingReady) onSignalingReady(); - }, - onopen: function (socket) { - if (socket) defaultSocket = socket; - if (onSignalingReady) onSignalingReady(); - } - }); - - if (defaultSocket && onSignalingReady) setTimeout(onSignalingReady, 2000); - - 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 (typeof args.transmitRoomOnce != 'undefined') { - 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(); - }; - - // 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 = {}; - - // dont-override-session allows you force RTCMultiConnection - // to not override default session of participants; - // by default, session is always overridden and set to the session coming from initiator! - if (!connection.dontOverrideSession) { - connection.session = _config.session || {}; - } - - 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 - }); - - defaultSocket.send({ - participant: true, - userid: connection.userid, - channel: channel, - targetUser: _config.userid, - extra: connection.extra, - session: connection.session - }); - }; - - // send file/data or text message - this.send = function (message, _channel) { - message = JSON.stringify({ - 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 (userid) { - clearSession(userid); - connection.refresh(); - }; - - // 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) { - addStream(connection.peers[e.socket.userid]); - } else { - for (var peer in connection.peers) { - 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) { - peer.attachStreams = [e.stream]; - } - - // detaching old streams - detachMediaStream(connection.detachStreams, peer.connection); - - if (e.stream && (session.audio || session.video || session.screen)) { - // removeStream is not yet implemented in Firefox - // if(isFirefox) peer.connection.removeStream(e.stream); - - if (isChrome || (isFirefox && !peer.connection.getLocalStreams().length)) { - peer.connection.addStream(e.stream); - } - } - - // because gecko has no support of renegotiation yet; - // so both chrome and firefox should redial instead of renegotiate! - if (isFirefox || _peer.targetBrowser == 'gecko') { - return _peer.redial(); - } - - 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, - userid: connection.userid, - extra: connection.extra || {}, - 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 - }; - - rtcMultiSession.requestsFrom[response.userid] = obj; - - // www.RTCMultiConnection.org/docs/onRequest/ - if (connection.onRequest && connection.isInitiator) { - connection.onRequest(obj); - } else _accept(obj); - } - - function _accept(e) { - 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/sendMessage/ - connection.sendCustomMessage = function (message) { - if (!defaultSocket) { - return setTimeout(function () { - connection.sendMessage(message); - }, 1000); - } - - defaultSocket.send({ - userid: connection.userid, - customMessage: true, - message: message - }); - }; - - // www.RTCMultiConnection.org/docs/accept/ - connection.accept = function (e) { - // for backward compatibility - if (arguments.length > 1 && typeof arguments[0] == 'string') { - 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 RTCPeerConnection = window.mozRTCPeerConnection || window.webkitRTCPeerConnection; - var RTCSessionDescription = window.mozRTCSessionDescription || window.RTCSessionDescription; - var RTCIceCandidate = window.mozRTCIceCandidate || window.RTCIceCandidate; - - function PeerConnection() { - return { - create: function (type, options) { - merge(this, options); - - var self = this; - - this.type = type; - this.init(); - this.attachMediaStreams(); - - if (isData(this.session) && isFirefox) { - navigator.mozGetUserMedia({ - audio: true, - fake: true - }, function (stream) { - self.connection.addStream(stream); - - if (type == 'offer') { - self.createDataChannel(); - } - - self.getLocalDescription(type); - - if (type == 'answer') { - self.createDataChannel(); - } - }, this.onMediaError); - } - - if (!isData(this.session) && isFirefox) { - if (this.session.data && type == 'offer') { - this.createDataChannel(); - } - - this.getLocalDescription(type); - - if (this.session.data && type == 'answer') { - this.createDataChannel(); - } - } - - isChrome && self.getLocalDescription(type); - return this; - }, - getLocalDescription: function (type) { - log('peer type is', type); - - if (type == 'answer') { - this.setRemoteDescription(this.offerDescription); - } - - var self = this; - this.connection[type == 'offer' ? 'createOffer' : 'createAnswer'](function (sessionDescription) { - sessionDescription.sdp = self.serializeSdp(sessionDescription.sdp); - self.connection.setLocalDescription(sessionDescription); - - if (self.trickleIce) { - self.onSessionDescription(sessionDescription, self.streaminfo); - } - }, this.onSdpError, this.constraints); - }, - serializeSdp: function (sdp) { - sdp = this.setBandwidth(sdp); - if (this.holdMLine == 'both') { - if (this.hold) { - this.prevSDP = sdp; - sdp = sdp.replace(/sendonly|recvonly|sendrecv/g, 'inactive'); - } else if (this.prevSDP) { - sdp = this.prevSDP; - } - } else if (this.holdMLine == 'audio' || this.holdMLine == 'video') { - sdp = sdp.split('m='); - - var audio = ''; - var video = ''; - - if (sdp[1] && sdp[1].indexOf('audio') == 0) { - audio = 'm=' + sdp[1]; - } - if (sdp[2] && sdp[2].indexOf('audio') == 0) { - audio = 'm=' + sdp[2]; - } - - if (sdp[1] && sdp[1].indexOf('video') == 0) { - video = 'm=' + sdp[1]; - } - if (sdp[2] && sdp[2].indexOf('video') == 0) { - video = 'm=' + sdp[2]; - } - - if (this.holdMLine == 'audio') { - if (this.hold) { - this.prevSDP = sdp[0] + audio + video; - sdp = sdp[0] + audio.replace(/sendonly|recvonly|sendrecv/g, 'inactive') + video; - } else if (this.prevSDP) { - sdp = this.prevSDP; - } - } - - if (this.holdMLine == 'video') { - if (this.hold) { - this.prevSDP = sdp[0] + audio + video; - sdp = sdp[0] + audio + video.replace(/sendonly|recvonly|sendrecv/g, 'inactive'); - } else if (this.prevSDP) { - sdp = this.prevSDP; - } - } - } - return sdp; - }, - init: function () { - this.setConstraints(); - this.connection = new RTCPeerConnection(this.iceServers, this.optionalArgument); - - if (this.session.data && isChrome) { - this.createDataChannel(); - } - - this.connection.onicecandidate = function (event) { - if (!event.candidate) { - if (!self.trickleIce) { - returnSDP(); - } - - return; - } - - if (!self.trickleIce) return; - self.onicecandidate(event.candidate); - }; - - this.connection.ongatheringchange = function () { - // this method is usually not fired. - // todo: need to fix event listners - log('iceGatheringState', self.connection.iceGatheringState); - - if (self.trickleIce) return; - if (self.connection.iceGatheringState == 'complete') { - returnSDP(); - } - }; - - function returnSDP() { - self.onSessionDescription(self.connection.localDescription, self.streaminfo); - } - - this.connection.onaddstream = function (e) { - self.onaddstream(e.stream, self.session); - - log('onaddstream', toStr(e.stream)); - }; - - 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 () { - self.connection && self.oniceconnectionstatechange({ - iceConnectionState: self.connection.iceConnectionState, - iceGatheringState: self.connection.iceGatheringState, - signalingState: self.connection.signalingState - }); - }; - var self = this; - }, - setBandwidth: function (sdp) { - if (isMobileDevice || isFirefox || !this.bandwidth) return sdp; - - var bandwidth = this.bandwidth; - - if (this.session.screen) { - if (!bandwidth.screen) { - warn('It seems that you are not using bandwidth for screen. Screen sharing is expected to fail.'); - } else if (bandwidth.screen < 300) { - 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 && this.session.screen) { - 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 || bandwidth.data) { - sdp = sdp.replace(/b=AS([^\r\n]+\r\n)/g, ''); - } - - if (bandwidth.audio) { - sdp = sdp.replace(/a=mid:audio\r\n/g, 'a=mid:audio\r\nb=AS:' + bandwidth.audio + '\r\n'); - } - - if (bandwidth.video) { - sdp = sdp.replace(/a=mid:video\r\n/g, 'a=mid:video\r\nb=AS:' + (bandwidth.screen || bandwidth.video) + '\r\n'); - } - - if (bandwidth.data && !this.preferSCTP) { - sdp = sdp.replace(/a=mid:data\r\n/g, 'a=mid:data\r\nb=AS:' + bandwidth.data + '\r\n'); - } - - return sdp; - }, - setConstraints: function () { - this.constraints = { - optional: this.sdpConstraints.optional || [{ - VoiceActivityDetection: false - }], - mandatory: this.sdpConstraints.mandatory || { - OfferToReceiveAudio: !!this.session.audio, - OfferToReceiveVideo: !!this.session.video || !!this.session.screen - } - }; - - // workaround for older firefox - if (this.session.data && isFirefox && this.constraints.mandatory) { - this.constraints.mandatory.OfferToReceiveAudio = true; - } - - if(this.constraints.mandatory) { - log('sdp-mandatory-constraints', toStr(this.constraints.mandatory)); - } - - if(this.constraints.optional) { - log('sdp-optional-constraints', toStr(this.constraints.optional)); - } - - this.optionalArgument = { - optional: this.optionalArgument.optional || [{ - DtlsSrtpKeyAgreement: true - }], - mandatory: this.optionalArgument.mandatory || {} - }; - - if (isChrome && chromeVersion >= 32 && !isNodeWebkit) { - this.optionalArgument.optional.push({ - googIPv6: true - }); - this.optionalArgument.optional.push({ - googDscp: true - }); - this.optionalArgument.optional.push({ - googImprovedWifiBwe: true - }); - } - - if (!this.preferSCTP) { - this.optionalArgument.optional.push({ - RtpDataChannels: true - }); - } - - log('optional-argument', toStr(this.optionalArgument.optional)); - - if (typeof this.iceServers != 'undefined') { - this.iceServers = { - iceServers: this.iceServers - }; - } else this.iceServers = null; - - log('ice-servers', toStr(this.iceServers.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); - }, - onMediaError: function (err) { - error(toStr(err)); - }, - setRemoteDescription: function (sessionDescription) { - if (!sessionDescription) throw 'Remote session description should NOT be NULL.'; - - log('setting remote description', sessionDescription.type, sessionDescription.sdp); - this.connection.setRemoteDescription( - new RTCSessionDescription(sessionDescription) - ); - }, - addIceCandidate: function (candidate) { - var iceCandidate = new RTCIceCandidate({ - sdpMLineIndex: candidate.sdpMLineIndex, - candidate: candidate.candidate - }); - - this.connection.addIceCandidate(iceCandidate); - }, - createDataChannel: function (channelIdentifier) { - if (!this.channels) this.channels = []; - - // protocol: 'text/chat', preset: true, stream: 16 - // maxRetransmits:0 && ordered:false - var dataChannelDict = {}; - - if (this.dataChannelDict) dataChannelDict = this.dataChannelDict; - - if (isChrome && !this.preferSCTP) { - dataChannelDict.reliable = false; // Deprecated! - } - - log('dataChannelDict', toStr(dataChannelDict)); - - if (isFirefox) { - this.connection.onconnection = function () { - self.socket && self.socket.send({ - userid: self.selfUserid, - isCreateDataChannel: true - }); - }; - } - - 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) - ); - } - - var self = this; - }, - setChannelEvents: function (channel) { - var self = this; - channel.onmessage = function (event) { - self.onmessage(event.data); - }; - - var numberOfTimes = 0; - channel.onopen = function () { - channel.push = channel.send; - channel.send = function (data) { - if (channel.readyState != 'open') { - 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); - }, - attachMediaStreams: function () { - var streams = this.attachStreams; - for (var i = 0; i < streams.length; i++) { - log('attaching stream:', streams[i].streamid); - this.connection.addStream(streams[i]); - } - this.getStreamInfo(); - }, - getStreamInfo: function () { - this.streaminfo = ''; - var streams = this.attachStreams; - for (var i = 0; i < streams.length; i++) { - if (i == 0) { - this.streaminfo = streams[i].streamid; - } else { - this.streaminfo += '----' + streams[i].streamid; - } - } - this.attachStreams = []; - }, - recreateOffer: function (renegotiate, callback) { - // if(isFirefox) this.create(this.type, this); - - log('recreating offer'); - - this.type = 'offer'; - this.renegotiate = true; - this.session = renegotiate; - this.setConstraints(); - - this.onSessionDescription = callback; - this.getStreamInfo(); - - // one can renegotiate data connection in existing audio/video/screen connection! - if (this.session.data && isChrome) { - this.createDataChannel(); - } - - this.getLocalDescription('offer'); - }, - recreateAnswer: function (sdp, session, callback) { - // if(isFirefox) this.create(this.type, this); - - log('recreating answer'); - - this.type = 'answer'; - this.renegotiate = true; - this.session = session; - 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 && isChrome) { - this.createDataChannel(); - } - - this.getLocalDescription('answer'); - } - }; - } - - var video_constraints = { - mandatory: {}, - optional: [] - }; - - /* by @FreCap pull request #41 */ - var currentUserMediaRequest = { - streams: [], - mutex: false, - queueRequests: [] - }; - - function getUserMedia(options) { - 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 n = navigator, - hints = options.constraints || { - audio: true, - video: video_constraints - }; - - if (hints.video == true) hints.video = video_constraints; - - // connection.mediaConstraints.audio = false; - if (typeof mediaConstraints.audio != 'undefined') { - hints.audio = mediaConstraints.audio; - } - - // connection.mediaConstraints.video = false; - if (typeof mediaConstraints.video != 'undefined' && hints.video) { - hints.video = merge(hints.video, mediaConstraints.video); - } - - // connection.media.min(320,180); - // connection.media.max(1920,1080); - var media = options.media; - if (isChrome) { - var mandatory = {}; - - if (media.minWidth) { - mandatory.minWidth = media.minWidth; - } - - if (media.minHeight) { - mandatory.minHeight = media.minHeight; - } - - if (media.maxWidth) { - mandatory.maxWidth = media.maxWidth; - } - - if (media.maxHeight) { - mandatory.maxHeight = media.maxHeight; - } - - if (media.minAspectRatio) { - mandatory.minAspectRatio = media.minAspectRatio; - } - - if (mandatory.minWidth && mandatory.minHeight) { - // code.google.com/p/chromium/issues/detail?id=143631#c9 - 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 (mediaConstraints.mandatory) { - hints.video.mandatory = merge(hints.video.mandatory, mediaConstraints.mandatory); - } - - // mediaConstraints.optional.bandwidth = 1638400; - if (mediaConstraints.optional) - hints.video.optional[0] = merge({}, mediaConstraints.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.optional && hints.video.mandatory) { - if (!hints.video.optional.length && isEmpty(hints.video.mandatory)) { - hints.video = true; - } - } - - log('media hints:', toStr(hints)); - - // easy way to match - var idInstance = JSON.stringify(hints); - - function streaming(stream, returnBack, streamid) { - if (!streamid) streamid = getRandomString(); - - var video = options.video; - if (video) { - video[isFirefox ? 'mozSrcObject' : 'src'] = isFirefox ? stream : 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; - n.getMedia(hints, streaming, function (err) { - if (options.onerror) options.onerror(err, idInstance); - else error(toStr(err)); - }); - } - } - - var FileSender = { - send: function (config) { - var connection = config.connection; - var channel = config.channel; - var privateChannel = config._channel; - var file = config.file; - - if (!config.file) { - error('You must select a file or pass Blob.'); - return; - } - - // max chunk sending limit on chrome is 64k - // max chunk receiving limit on firefox is 16k - var packetSize = (!!navigator.mozGetUserMedia || connection.preferSCTP) ? 15 * 1000 : 1 * 1000; - - if (connection.chunkSize) { - packetSize = connection.chunkSize; - } - - var textToTransfer = ''; - var numberOfPackets = 0; - var packets = 0; - - file.uuid = getRandomString(); - - function processInWebWorker() { - var blob = URL.createObjectURL(new Blob(['function readFile(_file) {postMessage(new FileReaderSync().readAsDataURL(_file));};this.onmessage = function (e) {readFile(e.data);}'], { - type: 'application/javascript' - })); - - var worker = new Worker(blob); - URL.revokeObjectURL(blob); - return worker; - } - - if (!!window.Worker && !isMobileDevice) { - var webWorker = processInWebWorker(); - - webWorker.onmessage = function (event) { - onReadAsDataURL(event.data); - }; - - webWorker.postMessage(file); - } else { - var reader = new FileReader(); - reader.onload = function (e) { - onReadAsDataURL(e.target.result); - }; - reader.readAsDataURL(file); - } - - function onReadAsDataURL(dataURL, text) { - var data = { - type: 'file', - uuid: file.uuid, - maxChunks: numberOfPackets, - currentPosition: numberOfPackets - packets, - name: file.name, - fileType: file.type, - size: file.size, - - userid: connection.userid, - extra: connection.extra - }; - - if (dataURL) { - text = dataURL; - numberOfPackets = packets = data.packets = parseInt(text.length / packetSize); - - file.maxChunks = data.maxChunks = numberOfPackets; - data.currentPosition = numberOfPackets - packets; - - file.userid = connection.userid; - file.extra = connection.extra; - file.sending = true; - connection.onFileStart(file); - } - - connection.onFileProgress({ - remaining: packets--, - length: numberOfPackets, - sent: numberOfPackets - packets, - - maxChunks: numberOfPackets, - uuid: file.uuid, - currentPosition: numberOfPackets - packets, - - sending: true - }, file.uuid); - - if (text.length > packetSize) data.message = text.slice(0, packetSize); - else { - data.message = text; - data.last = true; - data.name = file.name; - - file.url = URL.createObjectURL(file); - file.userid = connection.userid; - file.extra = connection.extra; - file.sending = true; - connection.onFileEnd(file); - } - - channel.send(data, privateChannel); - - textToTransfer = text.slice(data.message.length); - if (textToTransfer.length) { - setTimeout(function () { - onReadAsDataURL(null, textToTransfer); - }, connection.chunkInterval || 100); - } - } - } - }; - - function FileReceiver(connection) { - var content = {}, - packets = {}, - numberOfPackets = {}; - - function receive(data) { - var uuid = data.uuid; - - if (typeof data.packets !== 'undefined') { - numberOfPackets[uuid] = packets[uuid] = parseInt(data.packets); - data.sending = false; - connection.onFileStart(data); - } - - connection.onFileProgress({ - remaining: packets[uuid]--, - length: numberOfPackets[uuid], - received: numberOfPackets[uuid] - packets[uuid], - - maxChunks: numberOfPackets[uuid], - uuid: uuid, - currentPosition: numberOfPackets[uuid] - packets[uuid], - - sending: false - }, uuid); - - if (!content[uuid]) content[uuid] = []; - - content[uuid].push(data.message); - - if (data.last) { - var dataURL = content[uuid].join(''); - - FileConverter.DataURLToBlob(dataURL, data.fileType, function (blob) { - blob.uuid = uuid; - blob.name = data.name; - blob.type = data.fileType; - - blob.url = (window.URL || window.webkitURL).createObjectURL(blob); - - blob.sending = false; - blob.userid = data.userid || connection.userid; - blob.extra = data.extra || connection.extra; - connection.onFileEnd(blob); - - if (connection.autoSaveToDisk) { - FileSaver.SaveToDisk(blob.url, data.name); - } - - delete content[uuid]; - }); - } - } - - return { - receive: receive - }; - } - - var FileSaver = { - SaveToDisk: function (fileUrl, fileName) { - var hyperlink = document.createElement('a'); - hyperlink.href = fileUrl; - hyperlink.target = '_blank'; - hyperlink.download = fileName || fileUrl; - - var mouseEvent = new MouseEvent('click', { - view: window, - bubbles: true, - cancelable: true - }); - - hyperlink.dispatchEvent(mouseEvent); - - // (window.URL || window.webkitURL).revokeObjectURL(hyperlink.href); - } - }; - - var FileConverter = { - DataURLToBlob: function (dataURL, fileType, callback) { - - function processInWebWorker() { - var blob = URL.createObjectURL(new Blob(['function getBlob(_dataURL, _fileType) {var binary = atob(_dataURL.substr(_dataURL.indexOf(",") + 1)),i = binary.length,view = new Uint8Array(i);while (i--) {view[i] = binary.charCodeAt(i);};postMessage(new Blob([view], {type: _fileType}));};this.onmessage = function (e) {var data = JSON.parse(e.data); getBlob(data.dataURL, data.fileType);}'], { - type: 'application/javascript' - })); - - var worker = new Worker(blob); - URL.revokeObjectURL(blob); - return worker; - } - - if (!!window.Worker && !isMobileDevice) { - var webWorker = processInWebWorker(); - - webWorker.onmessage = function (event) { - callback(event.data); - }; - - webWorker.postMessage(JSON.stringify({ - dataURL: dataURL, - fileType: fileType - })); - } else { - var binary = atob(dataURL.substr(dataURL.indexOf(',') + 1)), - i = binary.length, - view = new Uint8Array(i); - - while (i--) { - view[i] = binary.charCodeAt(i); - } - - callback(new Blob([view])); - } - } - }; - - var TextSender = { - send: function (config) { - var connection = config.connection; - - var channel = config.channel, - _channel = config._channel, - initialText = config.text, - packetSize = connection.chunkSize || 1000, - textToTransfer = '', - isobject = false; - - if (typeof initialText !== 'string') { - 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); - } - } - } - }; - - // _______________ - // TextReceiver.js - - 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 - }; - } - - // Sound meter is used to detect speaker - // SoundMeter.js copyright goes to someone else! - - function SoundMeter(config) { - var connection = config.connection; - var context = config.context; - this.context = context; - this.volume = 0.0; - this.slow_volume = 0.0; - this.clip = 0.0; - - // Legal values are (256, 512, 1024, 2048, 4096, 8192, 16384) - this.script = context.createScriptProcessor(256, 1, 1); - that = this; - - this.script.onaudioprocess = function (event) { - var input = event.inputBuffer.getChannelData(0); - var i; - var sum = 0.0; - var clipcount = 0; - for (i = 0; i < input.length; ++i) { - sum += input[i] * input[i]; - if (Math.abs(input[i]) > 0.99) { - clipcount += 1; - } - } - that.volume = Math.sqrt(sum / input.length); - - var volume = that.volume.toFixed(2); - - if (volume >= .1 && connection.onspeaking) { - connection.onspeaking(config.event); - } - - if (volume < .1 && connection.onsilence) { - connection.onsilence(config.event); - } - }; - } - - SoundMeter.prototype.connectToSource = function (stream) { - this.mic = this.context.createMediaStreamSource(stream); - this.mic.connect(this.script); - this.script.connect(this.context.destination); - }; - - SoundMeter.prototype.stop = function () { - this.mic.disconnect(); - this.script.disconnect(); - }; - - - var isChrome = !!navigator.webkitGetUserMedia; - var isFirefox = !!navigator.mozGetUserMedia; - 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() { - return (Math.random() * new Date().getTime()).toString(36).replace(/\./g, ''); - } - - var chromeVersion = !!navigator.mozGetUserMedia ? 0 : parseInt(navigator.userAgent.match(/Chrom(e|ium)\/([0-9]+)\./)[2]); - - function isData(session) { - return !session.audio && !session.video && !session.screen && session.data; - } - - function isEmpty(session) { - var length = 0; - for (var s in session) { - length++; - } - return length == 0; - } - - 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; - } - - 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 isAudio = session.audio && !session.video && !session.screen; - if (isChrome && stream.getAudioTracks && stream.getVideoTracks) { - isAudio = stream.getAudioTracks().length && !stream.getVideoTracks().length; - } - - var mediaElement = document.createElement(isAudio ? 'audio' : 'video'); - - // "mozSrcObject" is always preferred over "src"!! - mediaElement[isFirefox ? 'mozSrcObject' : 'src'] = isFirefox ? stream : window.webkitURL.createObjectURL(stream); - - mediaElement.controls = true; - mediaElement.autoplay = !!session.remote; - mediaElement.muted = session.remote ? false : true; - - mediaElement.play(); - - return mediaElement; - } - - 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; - if (onload) script.onload = onload; - document.documentElement.appendChild(script); - } - - function muteOrUnmute(e) { - var stream = e.stream, - root = e.root, - session = e.session || {}, - enabled = e.enabled; - - if (!session.audio && !session.video) { - if (typeof session != 'string') { - 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 ? 'mute' : 'unmute', 'session', session); - - // enable/disable audio/video tracks - - if (session.audio) { - var audioTracks = stream.getAudioTracks()[0]; - if (audioTracks) - audioTracks.enabled = !enabled; - } - - if (session.video) { - var videoTracks = stream.getVideoTracks()[0]; - if (videoTracks) - videoTracks.enabled = !enabled; - } - - root.sockets.forEach(function (socket) { - if (root.type == 'local') - socket.send({ - userid: root.rtcMultiConnection.userid, - streamid: root.streamid, - mute: !!enabled, - unmute: !enabled, - session: session - }); - - if (root.type == 'remote') - socket.send({ - userid: root.rtcMultiConnection.userid, - promptMuteUnmute: true, - streamid: root.streamid, - mute: !!enabled, - unmute: !enabled, - session: session - }); - }); - - // 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 accurate session over "onstreamended" event. - var fakeObject = merge({}, root.streamObject); - fakeObject.session = session; - fakeObject.isAudio = session.audio && !session.video; - fakeObject.isVideo = (!session.audio && session.video) || (session.audio && session.video); - if (!!enabled) { - root.rtcMultiConnection.onmute(fakeObject); - } - - if (!enabled) { - root.rtcMultiConnection.onunmute(fakeObject); - } - } - - function stopTracks(mediaStream) { - // if getAudioTracks is not implemented - if ((!mediaStream.getAudioTracks || !mediaStream.getVideoTracks) && mediaStream.stop) { - mediaStream.stop(); - return; - } - - var fallback = false, - i; - - // MediaStream.stop should be avoided. It still exist and works but - // it is removed from the spec and instead MediaStreamTrack.stop should be used - var audioTracks = mediaStream.getAudioTracks(); - var videoTracks = mediaStream.getVideoTracks(); - - for (i = 0; i < audioTracks.length; i++) { - if (audioTracks[i].stop) { - // for chrome canary; which has "stop" method; however not functional yet! - try { - audioTracks[i].stop(); - } catch (e) { - fallback = true; - continue; - } - } else { - fallback = true; - continue; - } - } - - for (i = 0; i < videoTracks.length; i++) { - if (videoTracks[i].stop) { - // for chrome canary; which has "stop" method; however not functional yet! - try { - videoTracks[i].stop(); - } catch (e) { - fallback = true; - continue; - } - } else { - fallback = true; - continue; - } - } - - if (fallback && mediaStream.stop) mediaStream.stop(); - } - - // this object is used for pre-recorded media streaming! - - function Streamer(connection) { - var prefix = !!navigator.webkitGetUserMedia ? '' : 'moz'; - var self = this; - - self.stream = streamPreRecordedMedia; - - window.MediaSource = window.MediaSource || window.WebKitMediaSource; - if (!window.MediaSource) throw 'Chrome >=M28 (or Firefox with flag "media.mediasource.enabled=true") is mandatory to test this experiment.'; - - function streamPreRecordedMedia(file) { - if (!self.push) throw ' method is mandatory.'; - - var reader = new window.FileReader(); - reader.readAsArrayBuffer(file); - reader.onload = function (e) { - startStreaming(new window.Blob([new window.Uint8Array(e.target.result)])); - }; - - var sourceBuffer, mediaSource = new MediaSource(); - mediaSource.addEventListener(prefix + 'sourceopen', function () { - sourceBuffer = mediaSource.addSourceBuffer('video/webm; codecs="vorbis,vp8"'); - log('MediaSource readyState: <', this.readyState, '>'); - }, false); - - mediaSource.addEventListener(prefix + 'sourceended', function () { - log('MediaSource readyState: <', this.readyState, '>'); - }, false); - - function startStreaming(blob) { - if (!blob) return; - var size = blob.size, - startIndex = 0, - plus = 3000; - - log('one chunk size: <', plus, '>'); - - function inner_streamer() { - reader = new window.FileReader(); - reader.onload = function (e) { - self.push(new window.Uint8Array(e.target.result)); - - startIndex += plus; - if (startIndex <= size) { - setTimeout(inner_streamer, connection.chunkInterval || 100); - } else { - self.push({ - end: true - }); - } - }; - reader.readAsArrayBuffer(blob.slice(startIndex, startIndex + plus)); - } - - inner_streamer(); - } - - startStreaming(); - } - - self.receive = receive; - - function receive() { - var mediaSource = new MediaSource(); - - self.video.src = window.URL.createObjectURL(mediaSource); - mediaSource.addEventListener(prefix + 'sourceopen', function () { - self.receiver = mediaSource.addSourceBuffer('video/webm; codecs="vorbis,vp8"'); - self.mediaSource = mediaSource; - - log('MediaSource readyState: <', this.readyState, '>'); - }, false); - - - mediaSource.addEventListener(prefix + 'sourceended', function () { - warn('MediaSource readyState: <', this.readyState, '>'); - }, false); - } - - this.append = function (data) { - var that = this; - if (!self.receiver) - return setTimeout(function () { - that.append(data); - }); - - try { - var uint8array = new window.Uint8Array(data); - self.receiver.appendBuffer(uint8array); - } catch (e) { - error('Pre-recorded media streaming:', e); - } - }; - - this.end = function () { - self.mediaSource.endOfStream(); - }; - } - - // github.com/muaz-khan/WebRTC-Experiment/tree/master/DetectRTC - var DetectRTC = {}; - - (function () { - - function CheckDeviceSupport() { - // This method is useful only for Chrome! - - // 1st step: verify "MediaStreamTrack" support. - if (!window.MediaStreamTrack && !navigator.getMediaDevices) { - return; - } - - if (!window.MediaStreamTrack && navigator.getMediaDevices) { - window.MediaStreamTrack = {}; - } - - // 2nd step: verify "getSources" support which is planned to be removed soon! - // "getSources" will be replaced with "getMediaDevices" - if (!MediaStreamTrack.getSources) { - MediaStreamTrack.getSources = MediaStreamTrack.getMediaDevices; - } - - // todo: need to verify if this trick works - // via: https://code.google.com/p/chromium/issues/detail?id=338511 - if (!MediaStreamTrack.getSources && navigator.getMediaDevices) { - MediaStreamTrack.getSources = navigator.getMediaDevices.bind(navigator); - } - - // if still no "getSources"; it MUST be firefox! - if (!MediaStreamTrack.getSources) { - // assuming that it is older chrome or chromium implementation - if (isChrome) { - DetectRTC.hasMicrophone = true; - DetectRTC.hasWebcam = true; - } - - return; - } - - // loop over all audio/video input/output devices - MediaStreamTrack.getSources(function (sources) { - var result = {}; - - for (var i = 0; i < sources.length; i++) { - result[sources[i].kind] = true; - } - - DetectRTC.hasMicrophone = result.audio; - DetectRTC.hasWebcam = result.video; - }); - } - - DetectRTC.isWebRTCSupported = !!window.webkitRTCPeerConnection || !!window.mozRTCPeerConnection; - DetectRTC.isAudioContextSupported = !!window.AudioContext || !!window.webkitAudioContext; - DetectRTC.isScreenCapturingSupported = isChrome && chromeVersion >= 26 && (isNodeWebkit ? true : location.protocol == 'https:'); - DetectRTC.isSctpDataChannelsSupported = !!navigator.mozGetUserMedia || (isChrome && chromeVersion >= 25); - DetectRTC.isRtpDataChannelsSupported = isChrome && chromeVersion >= 31; - - // check for microphone/webcam support! - CheckDeviceSupport(); - })(); - - - function setDefaults(connection) { - // 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)); - }; - - var progressHelper = {}; - - // www.RTCMultiConnection.org/docs/body/ - connection.body = document.body || document.documentElement; - - // www.RTCMultiConnection.org/docs/autoSaveToDisk/ - // to make sure file-saver dialog is not invoked. - connection.autoSaveToDisk = false; - - // 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) { - warn('Now, "autoSaveToDisk" is false. Read more here: http://www.RTCMultiConnection.org/docs/autoSaveToDisk/'); - 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/dontAttachStream/ - connection.dontAttachStream = false; - - // www.RTCMultiConnection.org/docs/onstream/ - connection.onstream = function (e) { - connection.body.insertBefore(e.mediaElement, connection.body.firstChild); - }; - - // www.RTCMultiConnection.org/docs/onstreamended/ - connection.onstreamended = function (e) { - if (e.mediaElement && e.mediaElement.parentNode) { - e.mediaElement.parentNode.removeChild(e.mediaElement); - } - }; - - // www.RTCMultiConnection.org/docs/onmute/ - connection.onmute = function (e) { - log('onmute', e); - if (e.isVideo && e.mediaElement) { - e.mediaElement.pause(); - e.mediaElement.setAttribute('poster', e.snapshot || 'https://www.webrtc-experiment.com/images/muted.png'); - } - if (e.isAudio && e.mediaElement) { - e.mediaElement.muted = true; - } - }; - - // www.RTCMultiConnection.org/docs/onunmute/ - connection.onunmute = function (e) { - log('onunmute', 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 = function () { - // suggested by @rvulpescu from #154 - if (window.crypto) { - 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, ''); - } - }; - - // www.RTCMultiConnection.org/docs/userid/ - connection.userid = connection.token(); - - // www.RTCMultiConnection.org/docs/peers/ - connection.peers = {}; - 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']; - - // 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) { - // implementation from #68 - for (var stream in this) { - if (connection._skip.indexOf(stream) == -1) { - this[stream]._private(session, enabled); - } - } - }, - stop: function (type) { - // connection.streams.stop('local'); - var _stream; - for (var stream in this) { - if (stream != 'stop' && stream != 'mute' && stream != 'unmute' && stream != '_private') { - _stream = this[stream]; - - if (!type) _stream.stop(); - - if (type == 'local' && _stream.type == 'local') - _stream.stop(); - - if (type == 'remote' && _stream.type == 'remote') - _stream.stop(); - } - } - } - }; - - // 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/session/ - connection.session = { - audio: true, - video: true - }; - - // www.RTCMultiConnection.org/docs/bandwidth/ - connection.bandwidth = { - screen: 300 // 300kbps (old workaround!) - }; - - connection.sdpConstraints = {}; - connection.mediaConstraints = {}; - connection.optionalArgument = {}; - connection.dataChannelDict = {}; - - var iceServers = []; - - if (isFirefox) { - iceServers.push({ - url: 'stun:23.21.150.121' - }); - - iceServers.push({ - url: 'stun:stun.services.mozilla.com' - }); - } - - if (isChrome) { - iceServers.push({ - url: 'stun:stun.l.google.com:19302' - }); - - iceServers.push({ - url: 'stun:stun.anyfirewall.com:3478' - }); - } - - if (isChrome && chromeVersion < 28) { - iceServers.push({ - url: 'turn:homeo@turn.bistri.com:80?transport=udp', - credential: 'homeo' - }); - - iceServers.push({ - url: 'turn:homeo@turn.bistri.com:80?transport=tcp', - credential: 'homeo' - }); - } - - if (isChrome && chromeVersion >= 28) { - iceServers.push({ - url: 'turn:turn.bistri.com:80?transport=udp', - credential: 'homeo', - username: 'homeo' - }); - - iceServers.push({ - url: 'turn:turn.bistri.com:80?transport=tcp', - credential: 'homeo', - username: 'homeo' - }); - - iceServers.push({ - url: 'turn:turn.anyfirewall.com:443?transport=tcp', - credential: 'webrtc', - username: 'webrtc' - }); - } - connection.iceServers = iceServers; - - // 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 - - if (isFirefox) { - connection.preferSCTP = true; // FF supports only SCTP! - } - - // www.RTCMultiConnection.org/docs/fakeDataChannels/ - connection.fakeDataChannels = false; - - // www.RTCMultiConnection.org/docs/UA/ - connection.UA = { - Firefox: isFirefox, - Chrome: isChrome, - Mobile: isMobileDevice, - Version: chromeVersion, - NodeWebkit: isNodeWebkit - }; - - // file queue: to store previous file objects in memory; - // and stream over newly connected peers - // www.RTCMultiConnection.org/docs/fileQueue/ - connection.fileQueue = {}; - - // www.RTCMultiConnection.org/docs/media/ - connection.media = { - min: function (width, height) { - this.minWidth = width; - this.minHeight = height; - }, - max: function (width, height) { - this.maxWidth = width; - this.maxHeight = height; - } - }; - - // www.RTCMultiConnection.org/docs/candidates/ - connection.candidates = { - host: true, - relay: true, - reflexive: true - }; - - // www.RTCMultiConnection.org/docs/attachStreams/ - connection.attachStreams = []; - - // www.RTCMultiConnection.org/docs/detachStreams/ - connection.detachStreams = []; - - // 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'; - - connection._getStream = function (e) { - return { - rtcMultiConnection: e.rtcMultiConnection, - streamObject: e.streamObject, - stream: e.stream, - session: e.session, - userid: e.userid, - streamid: e.streamid, - sockets: e.socket ? [e.socket] : [], - type: e.type, - mediaElement: e.mediaElement, - stop: function (forceToStopRemoteStream) { - this.sockets.forEach(function (socket) { - if (this.type == 'local') { - socket.send({ - userid: this.rtcMultiConnection.userid, - extra: this.rtcMultiConnection.extra, - streamid: this.streamid, - stopped: true - }); - } - - if (this.type == 'remote' && !!forceToStopRemoteStream) { - socket.send({ - userid: this.rtcMultiConnection.userid, - promptStreamStop: true, - streamid: this.streamid - }); - } - }); - - var stream = this.stream; - if (stream && stream.stop) { - stopTracks(stream); - } - }, - mute: function (session) { - this.muted = true; - this._private(session, true); - }, - unmute: function (session) { - this.muted = false; - this._private(session, false); - }, - _private: function (session, enabled) { - muteOrUnmute({ - root: this, - session: session, - enabled: enabled, - stream: this.stream - }); - }, - startRecording: function (session) { - if (!session) - session = { - audio: true, - video: true - }; - - if (isFirefox) { - // https://www.webrtc-experiment.com/RecordRTC/AudioVideo-on-Firefox.html - session = { - audio: true - }; - } - - if (!window.RecordRTC) { - var self = this; - return loadScript('https://www.webrtc-experiment.com/RecordRTC.js', function () { - self.startRecording(session); - }); - } - - this.recorder = new MRecordRTC(); - this.recorder.mediaType = session; - this.recorder.addStream(this.stream); - this.recorder.startRecording(); - }, - stopRecording: function (callback) { - this.recorder.stopRecording(); - this.recorder.getBlob(function (blob) { - callback(blob.audio || blob.video, blob.video); - }); - } - }; - }; - - // new RTCMultiConnection().set({properties}).connect() - connection.set = function (properties) { - for (var property in properties) { - this[property] = properties[property]; - } - return this; - }; - - // www.RTCMultiConnection.org/docs/firebase/ - connection.firebase = 'chat'; - - // www.RTCMultiConnection.org/docs/onMediaError/ - connection.onMediaError = function (_error) { - error(_error); - }; - - // www.RTCMultiConnection.org/docs/stats/ - connection.stats = { - numberOfConnectedUsers: 0, - numberOfSessions: 0, - sessions: {} - }; - - // www.RTCMultiConnection.org/docs/getStats/ - connection.getStats = function (callback) { - var numberOfConnectedUsers = 0; - for (var peer in connection.peers) { - numberOfConnectedUsers++; - } - - connection.stats.numberOfConnectedUsers = numberOfConnectedUsers; - - if (callback) callback(connection.stats); - }; - - // 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, - checkIfScreenSharingFlagEnabled: function (callback) { - var warning; - if (isFirefox) { - warning = 'Screen sharing is NOT supported on Firefox.'; - error(warning); - if (callback) callback(false); - } - - if (location.protocol !== 'https:') { - warning = 'Screen sharing is NOT supported on ' + location.protocol + ' Try https!'; - error(warning); - if (callback) return callback(false); - } - - if (chromeVersion < 26) { - warning = 'Screen sharing support is suspicious!'; - warn(warning); - } - - var screen_constraints = { - video: { - mandatory: { - chromeMediaSource: 'screen' - } - } - }; - - var invocationInterval = 0, - stop; - (function selfInvoker() { - invocationInterval++; - if (!stop) setTimeout(selfInvoker, 10); - })(); - - navigator.webkitGetUserMedia(screen_constraints, onsuccess, onfailure); - - function onsuccess(stream) { - if (stream.stop) { - stream.stop(); - } - - if (callback) { - callback(true); - } - } - - function onfailure() { - stop = true; - if (callback) callback(invocationInterval > 5, warning); - } - }, - - RtpDataChannels: DetectRTC.isRtpDataChannelsSupported, - SctpDataChannels: DetectRTC.isSctpDataChannelsSupported - }; - - // www.RTCMultiConnection.org/docs/snapshots/ - connection.snapshots = {}; - - // www.RTCMultiConnection.org/docs/takeSnapshot/ - connection.takeSnapshot = function (userid, callback) { - for (var stream in connection.streams) { - stream = connection.streams[stream]; - if (stream.userid == userid && stream.stream && stream.stream.getVideoTracks && stream.stream.getVideoTracks().length) { - var video = stream.streamObject.mediaElement; - 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(); - callback && callback(connection.snapshots[userid]); - continue; - } - } - }; - - 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.WebRTC-Experiment.com/demos/MediaStreamTrack.getSources.html - connection._mediaSources = {}; - - // 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/devices/ - connection.devices = {}; - - // www.RTCMultiConnection.org/docs/getDevices/ - connection.getDevices = function (callback) { - // This method is useful only for Chrome! - - // 1st step: verify "MediaStreamTrack" support. - if (!window.MediaStreamTrack && !navigator.getMediaDevices) { - return callback(connection.devices); - } - - if (!window.MediaStreamTrack && navigator.getMediaDevices) { - window.MediaStreamTrack = {}; - } - - // 2nd step: verify "getSources" supported which is planned to be removed soon! - // "getSources" will be replaced with "getMediaDevices" - if (!MediaStreamTrack.getSources) { - MediaStreamTrack.getSources = MediaStreamTrack.getMediaDevices; - } - - // todo: need to verify if this trick works - // via: https://code.google.com/p/chromium/issues/detail?id=338511 - if (!MediaStreamTrack.getSources && navigator.getMediaDevices) { - MediaStreamTrack.getSources = navigator.getMediaDevices.bind(navigator); - } - - // if still no "getSources"; it MUST be firefox! - // or otherwise, it will be older chrome - if (!MediaStreamTrack.getSources) { - return callback(connection.devices); - } - - // loop over all audio/video input/output devices - MediaStreamTrack.getSources(function (media_sources) { - var sources = []; - for (var i = 0; i < media_sources.length; i++) { - sources.push(media_sources[i]); - } - - getAllUserMedias(sources); - - if (callback) callback(connection.devices); - }); - - var index = 0; - - var devicesFetched = {}; - - function getAllUserMedias(media_sources) { - var media_source = media_sources[index]; - if (!media_source) return; - - // to prevent duplicated devices to be fetched. - if (devicesFetched[media_source.id]) { - index++; - return getAllUserMedias(media_sources); - } - devicesFetched[media_source.id] = media_source; - - connection.devices[media_source.id] = media_source; - - index++; - getAllUserMedias(media_sources); - } - }; - - // 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 || {}; - this.attachStreams = []; - - // "drop" should detach all local streams - for (var stream in this.streams) { - if (this._skip.indexOf(stream) == -1) { - stream = this.streams[stream]; - if (stream.type == 'local') { - this.detachStreams.push(stream.streamid); - this.onstreamended(stream.streamObject); - } else this.onstreamended(stream.streamObject); - } - } - - // www.RTCMultiConnection.org/docs/sendCustomMessage/ - this.sendCustomMessage({ - drop: true, - dontRenegotiate: typeof config.renegotiate == 'undefined' ? true : config.renegotiate - }); - }; - - // used for SoundMeter - if (!!window.AudioContext) { - connection._audioContext = new AudioContext(); - } - - // 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 API key; if possible - connection.googKey = 'AIzaSyCUmCjvKRb-kOYrnoL2xaXb8I-_JJeKpf0'; - - // 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); - } - }; - - 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; - setTimeout(function () { - var root = connection.streams[streamid]; - connection.streams[streamid].sockets.forEach(function (socket) { - socket.send({ - userid: connection.userid, - streamid: root.streamid, - isVolumeChanged: true, - volume: mediaElement.volume - }); - }); - volumeChangeEventFired = false; - }, 2000); - } - }; - }; - - connection.localStreamids = []; - - // www.RTCMultiConnection.org/docs/onMediaFile/ - connection.onMediaFile = function (e) { - log('onMediaFile', e); - connection.body.appendChild(e.mediaElement); - }; - - // this object stores pre-recorded media streaming uids - // multiple pre-recorded media files can be streamed concurrently. - connection.preRecordedMedias = {}; - - // www.RTCMultiConnection.org/docs/shareMediaFile/ - // this method handles pre-recorded media streaming - connection.shareMediaFile = function (file, video, streamerid) { - if (file && (typeof file.size == 'undefined' || typeof file.type == 'undefined')) throw 'You MUST attach file using input[type=file] or pass a Blob.'; - - warn('Pre-recorded media streaming is added as experimental feature.'); - - video = video || document.createElement('video'); - - video.autoplay = true; - video.controls = true; - - streamerid = streamerid || connection.token(); - - var streamer = new Streamer(this); - - streamer.push = function (chunk) { - connection.send({ - preRecordedMediaChunk: true, - chunk: chunk, - streamerid: streamerid - }); - }; - - if (file) { - streamer.stream(file); - } - - streamer.video = video; - - streamer.receive(); - - connection.preRecordedMedias[streamerid] = { - video: video, - streamer: streamer, - onData: function (data) { - if (data.end) this.streamer.end(); - else this.streamer.append(data); - } - }; - - connection.onMediaFile({ - mediaElement: video, - userid: connection.userid, - extra: connection.extra - }); - - return streamerid; - }; - - // 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 || 'https://www.webrtc-experiment.com/images/muted.png'); - } - 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) { - for (var peer in connection.peers) { - connection.peers[peer].sharePartOfScreen(args); - } - }; - - connection.pausePartOfScreenSharing = function () { - for (var peer in connection.peers) { - connection.peers[peer].pausePartOfScreenSharing = true; - } - }; - - connection.stopPartOfScreenSharing = function () { - for (var peer in connection.peers) { - connection.peers[peer].stopPartOfScreenSharing = true; - } - }; - - connection.takeScreenshot = function (element, callback) { - if(!element || !callback) throw 'Invalid number of arguments.'; - - if (!window.html2canvas) { - return loadScript('https://www.webrtc-experiment.com/screenshot.js', function () { - connection.takeScreenshot(element); - }); - } - - if (typeof element == 'string') { - 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()); - } - }); - }; - - // it is false because workaround that is used to capture connections' failures - // affects renegotiation scenarios! - // todo: fix it! - connection.autoReDialOnFailure = false; - - 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 - connection.trickleIce = true; - } -})(); diff --git a/RTCMultiConnection/Library/RTCMultiConnection-v1.8.js b/RTCMultiConnection/Library/RTCMultiConnection-v1.8.js deleted file mode 100644 index 73ab40da..00000000 --- a/RTCMultiConnection/Library/RTCMultiConnection-v1.8.js +++ /dev/null @@ -1,4935 +0,0 @@ -// Last time updated at June 28, 2014, 08:32:23 - -// Latest file can be found here: https://www.rtcmulticonnection.org/latest.js - -// Muaz Khan - www.MuazKhan.com -// MIT License - www.WebRTC-Experiment.com/licence -// Documentation - www.RTCMultiConnection.org/docs -// FAQ - www.RTCMultiConnection.org/FAQ -// v1.8 changes log - www.RTCMultiConnection.org/changes-log/#v1.8 -// Demos - www.WebRTC-Experiment.com/RTCMultiConnection - -// _______________________ -// RTCMultiConnection-v1.8 - -/* issues/features need to be fixed & implemented: - --. (to fix canary ipv6 candidates issues): disabled "googIPv6", "googDscp" and "googImprovedWifiBwe" - --. "groupId" returned by MediaDeviceInfo object refers to single device with multiple tracks. --. need to provide API like this: -connection.DetectRTC.MediaDevices.forEach(function(device) { ---- device.audioinput (headset's microphone) --------- device.audioinput.deviceid - ---- device.audiooutput (headset's speakers); --------- device.audioinput.audiooutput - ---- device.videoinput (webcam) --------- device.videoinput.audiooutput -}); - --. renegotiation scenarios that fails: --. 1) if chrome starts video-only session and firefox joins with only audio --. 2) if chrome starts with audio-only session and firefox joins with only video --. 3) if chrome starts only audio and firefox joins with audio+video --. renegotiation scenarios that works: --. 1) if chrome starts audio+video and firefox joins with only audio or audio+video --. 2) if both browsers has similar streams - --. todo: add mp3-live streaming support --. todo: add mozCaptureStreamUntilEnded streaming support. - --. todo: Fix "disconnected" which happens often. Need to use WebRTC data channels for dirty workaround whenever possible; currently we're relying on signaling medium. - --. todo: check if stream.onended is fired on Firefox. --. todo: add removeStream workaround for Firefox. - --. todo: add connection.keepStreamsOpened --. todo: auto fallback to part-of-screen option for Firefox. - --. todo-fix: trickleIce & renegotiation fails. - --. "channel" object in the openSignalingChannel shouldn't be mandatory! --. JSON parse/stringify options for data transmitted using data-channels; e.g. connection.preferJSON = true; --. removeTrack() and addTracks() instead of "stop" --. voice translation using Translator.js -*/ - -(function () { - // www.RTCMultiConnection.org/docs/constructor/ - window.RTCMultiConnection = function (channel) { - // a reference to your constructor! - var connection = this; - - // www.RTCMultiConnection.org/docs/channel-id/ - connection.channel = channel || location.href.replace(/\/|:|#|%|\.|\[|\]/g, ''); - - var rtcMultiSession; // a reference to backbone object i.e. RTCMultiSession! - - // 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 (typeof args == 'string') { - connection.sessionid = args; - } else { - if (typeof args.transmitRoomOnce != 'undefined') { - connection.transmitRoomOnce = args.transmitRoomOnce; - } - - if (typeof args.dontTransmit != 'undefined') { - dontTransmit = args.dontTransmit; - } - - if (typeof args.sessionid != 'undefined') { - 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.stats.sessions[connection.sessionDescription.sessionid]) { - connection.stats.numberOfSessions++; - connection.stats.sessions[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) { - rtcMultiSession.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 - }); - - // to let user know that media resource has been captured - // now, he can share "sessionDescription" using sockets - if (rtcMultiSession.onMediaCaptured) rtcMultiSession.onMediaCaptured(); - }); - - if (rtcMultiSession.captureUserMediaOnDemand) { - rtcMultiSession.initSession({ - sessionDescription: connection.sessionDescription, - dontTransmit: dontTransmit - }); - } - }); - return connection.sessionDescription; - }; - - // www.RTCMultiConnection.org/docs/connect/ - this.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/ - this.join = joinSession; - - // www.RTCMultiConnection.org/docs/send/ - this.send = function (data, _channel) { - // 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.forEach) { - // 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++) { - connection.send(data[i], _channel); - } - return; - } - - // File or Blob object MUST have "type" and "size" properties - if (typeof data.size != 'undefined' && typeof data.type != 'undefined') { - // to send multiple files concurrently! - // file of any size; maximum length: 1GB - FileSender.send({ - file: data, - channel: rtcMultiSession, - _channel: _channel, - connection: connection - }); - } 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) { - // 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); - } - - function joinSession(session, joinAs) { - if (typeof session == 'string') { - 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 (typeof session == 'string') { - if (connection.stats.sessions[session]) { - session = connection.stats.sessions[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 "join" method'; - connection.onstatechange('failed', 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.dontAttachStream) - 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, - video: !!session.video - }; - - // if custom audio device is selected - if (connection._mediaSources.audio) { - constraints.audio = { - optional: [{ - sourceId: connection._mediaSources.audio - }] - }; - } - - // if custom video device is selected - if (connection._mediaSources.video) { - constraints.video = { - optional: [{ - sourceId: connection._mediaSources.video - }] - }; - } - - var screen_constraints = { - audio: false, - video: { - mandatory: { - chromeMediaSource: DetectRTC.screen.chromeMediaSource, - maxWidth: 1920, - maxHeight: 1080, - minAspectRatio: 1.77 - }, - optional: [] - } - }; - - // if screen is prompted - if (session.screen) { - // check if screen capturing extension is installed. - if(!dontCheckChromExtension && DetectRTC.screen.chromeMediaSource == 'screen' && DetectRTC.screen.extensionid) { - log('checking if chrome extension is installed.'); - DetectRTC.screen.getChromeExtensionStatus(DetectRTC.screen.extensionid, 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 (DetectRTC.screen.chromeMediaSource == 'desktop' && !DetectRTC.screen.sourceId) { - DetectRTC.screen.getSourceId(function (error) { - if (error && error == '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); - } - - captureUserMedia(callback, _session); - }); - return; - } - - if (DetectRTC.screen.chromeMediaSource == 'desktop') { - screen_constraints.video.mandatory.chromeMediaSourceId = DetectRTC.screen.sourceId; - } else log('You can install screen capturing chrome extension from this link: https://chrome.google.com/webstore/detail/screen-capturing/ajhifddimkapgcifgcodmmfdlknahffk'); - - 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); - - connection.onstatechange('fetching-usermedia'); - - function _captureUserMedia(forcedConstraints, forcedCallback, isRemoveVideoTracks, dontPreventSSLAutoAllowed) { - - 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) { - connection.onstatechange('usermedia-fetched'); - - if (isRemoveVideoTracks) { - stream = convertToAudioStream(stream); - } - - connection.localStreamids.push(streamid); - stream.onended = function () { - connection.onstreamended(streamedObject); - - // 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, - userid: _stream.rtcMultiConnection.userid, - extra: _stream.rtcMultiConnection.extra, - 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; - }; - - var mediaElement = createMediaElement(stream, session); - - mediaElement.muted = true; - - stream.streamid = streamid; - - 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.getVideoTracks().length > 0, - isAudio: !stream.getVideoTracks().length && stream.getAudioTracks().length > 0, - isInitiator: !!connection.isInitiator - }; - - var sObject = { - stream: stream, - blobURL: streamedObject.blobURL, - userid: connection.userid, - streamid: streamid, - session: session, - type: 'local', - streamObject: streamedObject, - mediaElement: mediaElement, - rtcMultiConnection: connection - }; - - if (isFirstSession) { - connection.attachStreams.push(stream); - } - isFirstSession = false; - - connection.streams[streamid] = connection._getStream(sObject); - - 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 - }); - } - }, - onerror: function (e, constraintUsed) { - // http://dev.w3.org/2011/webrtc/editor/getusermedia.html#h2_error-handling - if (isFirefox) { - if (e == 'PERMISSION_DENIED') { - e = { - message: '', - name: 'PermissionDeniedError', - constraintName: constraintUsed, - session: session - }; - } - } - - if (typeof e == 'string') { - 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 has not yet released their screen capturing modules. Still work in progress! Please try chrome for now!'); - } 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".'); - } - } - - 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.media = connection.media; - mediaConfig.connection = connection; - getUserMedia(mediaConfig); - } - } - - // www.RTCMultiConnection.org/docs/captureUserMedia/ - this.captureUserMedia = captureUserMedia; - - // www.RTCMultiConnection.org/docs/leave/ - this.leave = function (userid) { - isFirstSession = true; - - // eject a user; or leave the session - rtcMultiSession.leave(userid); - }; - - // www.RTCMultiConnection.org/docs/eject/ - this.eject = function (userid) { - if (!connection.isInitiator) throw 'Only session-initiator can eject a user.'; - connection.leave(userid); - }; - - // www.RTCMultiConnection.org/docs/close/ - this.close = function () { - // close entire session - connection.autoCloseEntireSession = true; - connection.leave(); - }; - - // www.RTCMultiConnection.org/docs/renegotiate/ - this.renegotiate = function (stream, session) { - rtcMultiSession.addStream({ - renegotiate: session || { - oneway: true, - audio: true, - video: true - }, - stream: stream - }); - }; - - // www.RTCMultiConnection.org/docs/addStream/ - this.addStream = function (session, socket) { - // www.RTCMultiConnection.org/docs/renegotiation/ - - // 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/ - this.removeStream = function (streamid) { - // detach pre-attached streams - if (!this.streams[streamid]) return warn('No such stream exists. Stream-id:', streamid); - - // www.RTCMultiConnection.org/docs/detachStreams/ - this.detachStreams.push(streamid); - this.renegotiate(); - }; - - // set RTCMultiConnection defaults on constructor invocation - setDefaults(this); - }; - - function RTCMultiSession(connection, callbackForSignalingReady) { - var fileReceiver = new FileReceiver(connection); - var textReceiver = new TextReceiver(connection); - - function onDataChannelMessage(e) { - if (!e) return; - - e = JSON.parse(e); - - if (e.data.type === 'text') { - textReceiver.receive(e.data, e.userid, e.extra); - } else if (typeof e.data.maxChunks != 'undefined') { - fileReceiver.receive(e.data); - } 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; - - // 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 isDontAttachStream = connection.dontAttachStream; - - connection.dontAttachStream = false; - connection.captureUserMedia(function () { - connection.dontAttachStream = true; - connection.join(session); - - // returning back previous state - connection.dontAttachStream = isDontAttachStream; - }, forceSession); - }; - if (!session.extra) session.extra = {}; - - return connection.onNewSession(session); - } - - connection.join(session); - } - - var socketObjects = {}; - var sockets = []; - - var rtcMultiSession = this; - - var participants = {}; - - 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 "onstreamended" 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); - } - }; - - socketConfig.callback = function (_socket) { - socket = _socket; - socketConfig.onopen(); - }; - - var socket = connection.openSignalingChannel(socketConfig), - isofferer = _config.isofferer, - peer; - - var peerConfig = { - onopen: onChannelOpened, - onicecandidate: function (candidate) { - if (!connection.candidates) throw 'ICE candidates are mandatory.'; - if (!connection.candidates.host && candidate.candidate.indexOf('typ host') != -1) return; - if (!connection.candidates.relay && candidate.candidate.indexOf('typ relay') != -1) return; - if (!connection.candidates.reflexive && candidate.candidate.indexOf('typ srflx') != -1) return; - - log(candidate.candidate); - - socket && socket.send({ - userid: connection.userid, - candidate: { - sdpMLineIndex: candidate.sdpMLineIndex, - candidate: JSON.stringify(candidate.candidate) - } - }); - }, - onmessage: onDataChannelMessage, - onaddstream: function (stream, session) { - session = session || _config.renegotiate || connection.session; - - // if it is Firefox; then return. - if (isData(session)) return; - - if (_config.streaminfo) { - var streaminfo = _config.streaminfo.split('----'); - for (var i = 0; i < streaminfo.length; i++) { - stream.streamid = streaminfo[i]; - } - - _config.streaminfo = swap(streaminfo.pop()).join('----'); - } - - var mediaElement = createMediaElement(stream, merge({ - remote: true - }, session)); - _config.stream = stream; - - if (!stream.getVideoTracks().length) - mediaElement.addEventListener('play', function () { - setTimeout(function () { - mediaElement.muted = false; - afterRemoteStreamStartedFlowing(mediaElement, session); - }, 3000); - }, false); - else - waitUntilRemoteStreamStartsFlowing(mediaElement, session); - - if (connection.setDefaultEventsForMediaElement) { - connection.setDefaultEventsForMediaElement(mediaElement, stream.streamid); - } - }, - - onremovestream: function (event) { - warn('onremovestream', event); - }, - - 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(!connection.isInitiator && peer.connection && peer.connection.iceConnectionState == 'connected' && peer.connection.iceGatheringState == 'complete' && peer.connection.signalingState == 'stable' && connection.stats.numberOfConnectedUsers == 1) { - connection.onstatechange('connected-with-initiator'); - } - - 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') { - if (isFirefox || _config.userinfo.browser == 'firefox') { - warn('ICE connectivity check is failed. Re-establishing peer connection.'); - connection.peers[_config.userid].redial(); - } else { - warn('ICE connectivity check is failed. Renegotiating peer connection.'); - connection.peers[_config.userid].renegotiate(); - } - } - - if (connection.peers[_config.userid] && connection.peers[_config.userid].peer.connection.iceConnectionState == 'disconnected') { - connection.peers[_config.userid].connected = false; - - socket.send({ - userid: connection.userid, - extra: connection.extra, - question: 'are-you-there' - }); - - // 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[_config.userid].connected) return; - - // to make sure this user's all remote streams are removed. - for (var stream in connection.streams) { - stream = connection.streams[stream]; - if (stream.userid == _config.userid && stream.type == 'remote') { - connection.onstreamended(stream.streamObject); - } - } - - connection.remove(_config.userid); - }, 5000); - } - - 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({ - userid: connection.userid, - extra: connection.extra || {}, - redial: true - }); - - // to make sure all old "remote" streams are also removed! - for (var stream in connection.streams) { - stream = connection.streams[stream]; - if (stream.userid == _config.userid && stream.type == 'remote') { - connection.onstreamended(stream.streamObject); - } - } - } - } - }, - - onsignalingstatechange: function (event) { - log('onsignalingstatechange', toStr(event)); - }, - - attachStreams: connection.attachStreams, - iceServers: connection.iceServers, - 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 - }; - - function waitUntilRemoteStreamStartsFlowing(mediaElement, session, numberOfTimes) { - // chrome for android may have some features missing - if (isMobileDevice) { - return afterRemoteStreamStartedFlowing(mediaElement, session); - } - - if (!numberOfTimes) numberOfTimes = 0; - numberOfTimes++; - - if (!(mediaElement.readyState <= HTMLMediaElement.HAVE_CURRENT_DATA || mediaElement.paused || mediaElement.currentTime <= 0)) { - afterRemoteStreamStartedFlowing(mediaElement, session); - } else { - if (numberOfTimes >= 60) { // wait 60 seconds while video is delivered! - socket.send({ - userid: connection.userid, - extra: connection.extra, - failedToReceiveRemoteVideo: true, - streamid: _config.stream.streamid - }); - } else - setTimeout(function () { - log('Waiting for incoming remote stream to be started flowing: ' + numberOfTimes + ' seconds.'); - waitUntilRemoteStreamStartsFlowing(mediaElement, session, numberOfTimes); - }, 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(mediaElement, session) { - var stream = _config.stream; - - stream.onended = function () { - connection.onstreamended(streamedObject); - }; - - var streamedObject = { - mediaElement: mediaElement, - - stream: stream, - streamid: stream.streamid, - session: session || connection.session, - - blobURL: mediaElement.mozSrcObject ? URL.createObjectURL(stream) : mediaElement.src, - type: 'remote', - - extra: _config.extra, - userid: _config.userid, - - isVideo: stream.getVideoTracks().length > 0, - isAudio: !stream.getVideoTracks().length && stream.getAudioTracks().length > 0, - isInitiator: !!_config.isInitiator - }; - - // connection.streams['stream-id'].mute({audio:true}) - connection.streams[stream.streamid] = connection._getStream({ - stream: stream, - blobURL: streamedObject.blobURL, - userid: _config.userid, - streamid: stream.streamid, - socket: socket, - type: 'remote', - streamObject: streamedObject, - mediaElement: mediaElement, - rtcMultiConnection: connection, - session: session || connection.session - }); - - connection.onstream(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 - }); - - // 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.stats.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 (typeof bandwidth == 'string') 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({ - userid: connection.userid, - extra: connection.extra || {}, - changeBandwidth: true, - bandwidth: bandwidth - }); - }, - sendCustomMessage: function (message) { - // connection.peers['user-id'].sendCustomMessage(); - - this.socket.send({ - userid: connection.userid, - extra: connection.extra || {}, - customMessage: true, - message: message - }); - }, - onCustomMessage: function (message) { - log('Received "private" message from', this.userid, - typeof message == 'string' ? 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); - connection.onstreamended(stream.streamObject); - } - - if (stream.type == 'remote' && stream.userid == this.userid) { - connection.onstreamended(stream.streamObject); - } - } - } - - !dontSendMessage && this.socket.send({ - userid: connection.userid, - extra: connection.extra || {}, - drop: true - }); - }, - hold: function (holdMLine) { - // connection.peers['user-id'].hold(); - - this.socket.send({ - userid: connection.userid, - extra: connection.extra || {}, - 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(); - - this.socket.send({ - userid: connection.userid, - extra: connection.extra || {}, - 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.streamObject)); - - // www.RTCMultiConnection.org/docs/onunhold/ - if (!isHold) - connection.onunhold(merge({ - kind: kind - }, stream.streamObject)); - } - } - } - }, - 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') { - connection.onstreamended(stream.streamObject); - } - } - } - - log('ReDialing...'); - - socket.send({ - userid: connection.userid, - extra: connection.extra, - recreatePeer: true - }); - - peer = new PeerConnection(); - peer.create('offer', peerConfig); - }, - sharePartOfScreen: function (args) { - // www.RTCMultiConnection.org/docs/onpartofscreen/ - - var element = args.element; - var that = this; - - if (!window.html2canvas) { - return loadScript(connection.resources.html2canvas, function () { - that.sharePartOfScreen(args); - }); - } - - if (typeof element == 'string') { - element = document.querySelector(element); - if (!element) element = document.getElementById(element); - } - if (!element) throw 'HTML Element is inaccessible!'; - - 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); - } - - // html2canvas.js is used to take screenshots - html2canvas(element, { - onrendered: function (canvas) { - var screenshot = canvas.toDataURL(); - - 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({ - userid: connection.userid, - extra: connection.extra, - screenshot: screenshot, - isPartOfScreen: true - }); - } - - // "once" can be used to share single screenshot - !args.once && setTimeout(partOfScreenCapturer, args.interval || 200); - } - }); - } - - partOfScreenCapturer(); - } - }; - } - - function onSessionOpened() { - // original conferencing infrastructure! - if (connection.isInitiator && getLength(participants) && getLength(participants) <= connection.maxParticipantsAllowed) { - log('syncing participants', participants); - log('inner-config-object', _config); - if (!connection.session.oneway && !connection.session.broadcast) { - defaultSocket.send({ - sessionid: connection.sessionid, - newParticipant: _config.userid || socket.channel, - userid: connection.userid, - extra: connection.extra, - 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 (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({ - sdpMLineIndex: response.candidate.sdpMLineIndex, - candidate: JSON.parse(response.candidate.candidate) - }); - } - - if (response.mute || response.unmute) { - if (response.promptMuteUnmute) { - 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].streamObject; - } - - var session = response.session; - var fakeObject = merge({}, streamObject); - fakeObject.session = session; - fakeObject.isAudio = session.audio && !session.video; - fakeObject.isVideo = (!session.audio && session.video) || (session.audio && session.video); - - 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]) { - connection.onstreamended(connection.streams[response.streamid].streamObject); - } - } - - // to stop remote stream - if (response.promptStreamStop /* && !connection.isInitiator */ ) { - // var forceToStopRemoteStream = true; - // connection.streams['remote-stream-id'].stop( forceToStopRemoteStream ); - 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) { - stopTracks(stream); - stream.stream.onended(stream.streamObject); - } - } - } - - if (peer && peer.connection) { - if (peer.connection.signalingState != 'closed') { - peer.connection.close(); - } - peer.connection = null; - } - - if (response.closeEntireSession) { - connection.close(); - connection.refresh(); - } else if (socket && response.ejected) { - // if user is ejected; his stream MUST be removed - // from all other users' side - socket.send({ - left: true, - extra: connection.extra, - userid: connection.userid - }); - - if (sockets[_config.socketIndex]) - delete sockets[_config.socketIndex]; - if (socketObjects[socket.channel]) - delete socketObjects[socket.channel]; - - socket = null; - } - - connection.remove(response.userid); - - if (participants[response.userid]) delete participants[response.userid]; - - connection.onleave({ - userid: response.userid, - extra: response.extra, - entireSessionClosed: !!response.closeEntireSession - }); - } - - // keeping session active even if initiator leaves - if (response.playRoleOfBroadcaster) { - if (response.extra) { - connection.extra = merge(connection.extra, response.extra); - } - 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.'; - 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) { - if (!connection.peers[response.userid]) throw 'No such peer exists.'; - connection.peers[response.userid].peer.hold = true; - connection.peers[response.userid].peer.holdMLine = response.holdMLine; - connection.peers[response.userid].renegotiate(); - - connection.peers[response.userid].fireHoldUnHoldEvents({ - kind: response.holdMLine, - isHold: true, - userid: response.userid - }); - } - - if (response.unhold) { - if (!connection.peers[response.userid]) throw 'No such peer exists.'; - connection.peers[response.userid].peer.hold = false; - connection.peers[response.userid].peer.holdMLine = response.holdMLine; - connection.peers[response.userid].renegotiate(); - - connection.peers[response.userid].fireHoldUnHoldEvents({ - kind: response.holdMLine, - isHold: false, - userid: response.userid - }); - } - - // 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(); - } - } - } - - if (response.question == 'are-you-there') { - socket.send({ - userid: connection.userid, - extra: connection.extra, - answer: 'yes-iam-connected' - }); - } - - if (response.answer == 'yes-iam-connected') { - connection.peers[_config.userid].connected = true; - } - } - - connection.playRoleOfInitiator = function () { - connection.dontAttachStream = true; - connection.open(); - sockets = swap(sockets); - connection.dontAttachStream = false; - }; - - connection.askToShareParticipants = function () { - defaultSocket && defaultSocket.send({ - userid: connection.userid, - extra: connection.extra, - 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; - - if (isChrome || (isFirefox && !peer.connection.getLocalStreams().length)) { - peer.connection.addStream(stream); - } - - connection.renegotiatedSessions[JSON.stringify(_config.renegotiate)] = { - session: _config.renegotiate, - stream: stream - }; - - delete _config.renegotiate; - - createAnswer(); - }, _config.renegotiate); - } - - function createAnswer() { - // because Firefox has no support of renegotiation yet; - // so both chrome and firefox should redial instead of renegotiate! - if (isFirefox || _config.userinfo.browser == 'firefox') { - if (connection.peers[_config.userid]) { - connection.peers[_config.userid].redial(); - } - return; - } - - peer.recreateAnswer(sdp, session, function (_sdp, streaminfo) { - sendsdp({ - sdp: _sdp, - socket: socket, - streaminfo: streaminfo - }); - }); - } - } - } - - 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({ - userid: connection.userid, - sdp: JSON.stringify(e.sdp), - extra: connection.extra, - 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, - userid: connection.userid, - targetUser: channel, - channel: new_channel, - extra: connection.extra - }); - } - - // if a user leaves - - function clearSession(channel) { - connection.stats.numberOfConnectedUsers--; - - var alert = { - left: true, - extra: connection.extra, - userid: connection.userid, - sessionid: connection.sessionid - }; - - if (connection.isInitiator) { - if (connection.autoCloseEntireSession) { - alert.closeEntireSession = true; - } else if (sockets[0]) { - sockets[0].send({ - playRoleOfBroadcaster: true, - userid: connection.userid - }); - } - } - - if (!channel) { - var length = sockets.length; - for (var i = 0; i < length; i++) { - var socket = sockets[i]; - if (socket) { - socket.send(alert); - - if (socketObjects[socket.channel]) - delete socketObjects[socket.channel]; - - delete sockets[i]; - } - } - } - - // eject a specific user! - if (channel) { - socket = socketObjects[channel]; - if (socket) { - alert.ejected = true; - socket.send(alert); - - if (sockets[socket.index]) - delete sockets[socket.index]; - - delete socketObjects[channel]; - } - } - - sockets = swap(sockets); - } - - // 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) { - connection.onstreamended(stream.streamObject); - if (stream.stop) stream.stop(); - 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 = {}; - connection.isAcceptNewSession = true; - - // to stop/remove self streams - for (var i = 0; i < connection.attachStreams.length; i++) { - stopTracks(connection.attachStreams[i]); - } - connection.attachStreams = []; - - // to allow capturing of identical streams - currentUserMediaRequest = { - streams: [], - mutex: false, - queueRequests: [] - }; - - // to make sure remote streams are also removed! - for (var stream in connection.streams) { - if (connection._skip.indexOf(stream) == -1) { - connection.onstreamended(connection.streams[stream].streamObject); - delete connection.streams[stream]; - } - } - - rtcMultiSession.isOwnerLeaving = true; - connection.isInitiator = false; - - connection.sessionDescriptions = { }; - connection.stats.sessions = { }; - }; - - // www.RTCMultiConnection.org/docs/reject/ - connection.reject = function (userid) { - if (typeof userid != 'string') userid = userid.userid; - defaultSocket.send({ - rejectedRequestOf: userid, - userid: connection.userid, - extra: connection.extra || {} - }); - - // remove relevant data to allow him join again - connection.remove(userid); - }; - - function fireOnSessionRemoved() { - if (!connection.isInitiator || !connection.autoCloseEntireSession) return; - defaultSocket.send({ - isSessionClosed: true, - session: connection.sessionDescription, - userid: connection.userid, - extra: connection.extra - }); - } - - window.addEventListener('beforeunload', function () { - if(!connection.leaveOnPageUnload) return; - - fireOnSessionRemoved(); - clearSession(); - }, false); - - window.addEventListener('keyup', function (e) { - if(!connection.leaveOnPageUnload) return; - - if (e.keyCode == 116) { - fireOnSessionRemoved(); - clearSession(); - } - }, false); - - 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.send({ - userid: connection.userid, - extra: connection.extra, - 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 - }); - } - } - } - - // 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 = connection.openSignalingChannel({ - onmessage: function (response) { - if (response.userid == connection.userid) return; - - if (response.sessionid && response.userid) { - if (!connection.stats.sessions[response.sessionid]) { - connection.stats.numberOfSessions++; - connection.stats.sessions[response.sessionid] = response; - } - } - - if (connection.isAcceptNewSession && response.sessionid && response.userid && !connection.sessionDescriptions[response.sessionid]) { - connection.sessionDescriptions[response.sessionid] = response; - - if (!connection.dontOverrideSession) { - connection.session = response.session; - } - - onNewSession(response); - } - - if (response.isSessionClosed) { - connection.onSessionClosed(response.session); - } - - 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 && !participants[response.userid]) { - acceptRequest(response); - } - - if (response.acceptedRequestOf == connection.userid) { - connection.onstatechange('request-accepted'); - } - - if (response.rejectedRequestOf == connection.userid) { - connection.onstatechange('request-rejected'); - } - - 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); - connection.onstreamended(stream.streamObject); - } else connection.onstreamended(stream.streamObject); - } - } - - if (response.message.renegotiate) { - // renegotiate; so "peer.removeStream" happens. - connection.addStream(); - } - } else if (connection.onCustomMessage) { - connection.onCustomMessage(response.message); - } - } - - if (connection.isInitiator && response.searchingForRooms) { - defaultSocket && defaultSocket.send({ - userid: connection.userid, - extra: connection.extra, - sessionDescription: connection.sessionDescription, - responseFor: response.userid - }); - } - - if (response.sessionDescription && response.responseFor == connection.userid) { - var sessionDescription = response.sessionDescription; - if (!connection.stats.sessions[sessionDescription.sessionid]) { - connection.stats.numberOfSessions++; - connection.stats.sessions[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) { - log('shareWith:joinUsers', response.joinUsers); - joinParticipants(response.joinUsers); - } - - // participants are shared with all users - if (!response.shareWith && response.joinUsers) { - log('all:joinUsers', 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({ - userid: connection.userid, - extra: connection.extra, - messageFor: response.userid, - presenceState: 'available', - _config: response._config - }); - } - - if(response.presenceState == 'available') { - joinSession(response._config); - } - } - - if(response.donotJoin && response.messageFor == connection.userid) { - log(response.userid, 'is not joining your room.'); - } - }, - callback: function (socket) { - if (socket) defaultSocket = socket; - if (onSignalingReady) onSignalingReady(); - }, - onopen: function (socket) { - if (socket) defaultSocket = socket; - if (onSignalingReady) onSignalingReady(); - } - }); - - if (defaultSocket && onSignalingReady) setTimeout(onSignalingReady, 2000); - - 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 (typeof args.transmitRoomOnce != 'undefined') { - 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) { - if(rtcMultiSession.donotJoin && rtcMultiSession.donotJoin == _config.sessionid) { - return; - } - - rtcMultiSession.presenceState = 'available'; - connection.onstatechange('room-available'); - - // dontOverrideSession allows you force RTCMultiConnection - // to not override default session of participants; - // by default, session is always overridden and set to the session coming from initiator! - if (!connection.dontOverrideSession) { - connection.session = _config.session || {}; - } - - 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 mandatory = connection.sdpConstraints.mandatory; - if(typeof mandatory.OfferToReceiveAudio == 'undefined') { - connection.sdpConstraints.mandatory.OfferToReceiveAudio = !!connection.session.audio; - } - if(typeof mandatory.OfferToReceiveVideo == 'undefined') { - connection.sdpConstraints.mandatory.OfferToReceiveVideo = !!connection.session.video || !!connection.session.screen; - } - - log(toStr(connection.sdpConstraints.mandatory)); - - var offers = {}; - if(connection.attachStreams.length) { - var stream = connection.attachStreams[connection.attachStreams.length - 1]; - if(stream.getAudioTracks().length) { - offers.audio = true; - } - if(stream.getVideoTracks().length) { - offers.video = true; - } - } - - log(toStr(offers)); - - connection.onstatechange('connecting-with-initiator'); - defaultSocket.send({ - participant: true, - userid: connection.userid, - channel: channel, - targetUser: _config.userid, - extra: connection.extra, - session: connection.session, - offers: { - audio: !!offers.audio, - video: !!offers.video - } - }); - - connection.skipOnNewSession = false; - } - - // 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('detecting-room-presence'); - - defaultSocket.send({ - userid: connection.userid, - extra: connection.extra, - messageFor: _config.userid, - presenceState: rtcMultiSession.presenceState, - _config: { - userid: _config.userid, - extra: _config.extra || {}, - sessionid: _config.sessionid, - session: _config.session || false - } - }); - - setTimeout(function() { - if(rtcMultiSession.presenceState == 'checking') { - connection.onstatechange('room-not-available', 'Unable to reach session initiator.'); - } - }, 3000); - }; - - connection.donotJoin = function(sessionid) { - rtcMultiSession.donotJoin = sessionid; - - var session = connection.sessionDescriptions[sessionid]; - if(!session) return; - - defaultSocket.send({ - userid: connection.userid, - extra: connection.extra, - 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) { - message = JSON.stringify({ - 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 (userid) { - clearSession(userid); - connection.refresh(); - }; - - // 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) { - peer.attachStreams = [e.stream]; - } - - // detaching old streams - detachMediaStream(connection.detachStreams, peer.connection); - - if (e.stream && (session.audio || session.video || session.screen)) { - // removeStream is not yet implemented in Firefox - // if(isFirefox) peer.connection.removeStream(e.stream); - - if (isChrome || (isFirefox && !peer.connection.getLocalStreams().length)) { - peer.connection.addStream(e.stream); - } - } - - // because Firefox has no support of renegotiation yet; - // so both chrome and firefox should redial instead of renegotiate! - if (isFirefox || _peer.userinfo.browser == 'firefox') { - return _peer.redial(); - } - - 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, - userid: connection.userid, - extra: connection.extra || {}, - 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) { - log(toStr(response.offers)); - - var mandatory = connection.sdpConstraints.mandatory; - if(typeof mandatory.OfferToReceiveAudio == 'undefined') { - connection.sdpConstraints.mandatory.OfferToReceiveAudio = !!response.offers.audio; - } - if(typeof mandatory.OfferToReceiveVideo == 'undefined') { - connection.sdpConstraints.mandatory.OfferToReceiveVideo = !!response.offers.video; - } - - log(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); - - // to let user know that media resource has been captured - if (rtcMultiSession.onMediaCaptured) rtcMultiSession.onMediaCaptured(); - }); - 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/sendMessage/ - connection.sendCustomMessage = function (message) { - if (!defaultSocket) { - return setTimeout(function () { - connection.sendMessage(message); - }, 1000); - } - - defaultSocket.send({ - userid: connection.userid, - customMessage: true, - message: message - }); - }; - - // www.RTCMultiConnection.org/docs/accept/ - connection.accept = function (e) { - // for backward compatibility - if (arguments.length > 1 && typeof arguments[0] == 'string') { - 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 RTCPeerConnection = window.mozRTCPeerConnection || window.webkitRTCPeerConnection; - var RTCSessionDescription = window.mozRTCSessionDescription || window.RTCSessionDescription; - var RTCIceCandidate = window.mozRTCIceCandidate || window.RTCIceCandidate; - - 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 (type) { - log('peer type is', type); - - if (type == 'answer') { - this.setRemoteDescription(this.offerDescription); - } - - var self = this; - this.connection[type == 'offer' ? 'createOffer' : 'createAnswer'](function (sessionDescription) { - sessionDescription.sdp = self.serializeSdp(sessionDescription.sdp); - self.connection.setLocalDescription(sessionDescription); - - if (self.trickleIce) { - self.onSessionDescription(sessionDescription, self.streaminfo); - } - - if (sessionDescription.type == 'offer') { - log('offer sdp', sessionDescription.sdp); - } - }, this.onSdpError, this.constraints); - }, - serializeSdp: function (sdp) { - if(isFirefox) return sdp; - - sdp = this.setBandwidth(sdp); - if (this.holdMLine == 'both') { - if (this.hold) { - this.prevSDP = sdp; - sdp = sdp.replace(/sendonly|recvonly|sendrecv/g, 'inactive'); - } else if (this.prevSDP) { - sdp = this.prevSDP; - } - } else if (this.holdMLine == 'audio' || this.holdMLine == 'video') { - sdp = sdp.split('m='); - - var audio = ''; - var video = ''; - - if (sdp[1] && sdp[1].indexOf('audio') == 0) { - audio = 'm=' + sdp[1]; - } - if (sdp[2] && sdp[2].indexOf('audio') == 0) { - audio = 'm=' + sdp[2]; - } - - if (sdp[1] && sdp[1].indexOf('video') == 0) { - video = 'm=' + sdp[1]; - } - if (sdp[2] && sdp[2].indexOf('video') == 0) { - video = 'm=' + sdp[2]; - } - - if (this.holdMLine == 'audio') { - if (this.hold) { - this.prevSDP = sdp[0] + audio + video; - sdp = sdp[0] + audio.replace(/sendonly|recvonly|sendrecv/g, 'inactive') + video; - } else if (this.prevSDP) { - sdp = this.prevSDP; - } - } - - if (this.holdMLine == 'video') { - if (this.hold) { - this.prevSDP = sdp[0] + audio + video; - sdp = sdp[0] + audio + video.replace(/sendonly|recvonly|sendrecv/g, 'inactive'); - } else if (this.prevSDP) { - sdp = this.prevSDP; - } - } - } - return sdp; - }, - 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); - }; - - this.connection.ongatheringchange = function () { - // this method is usually not fired. - // todo: need to fix event listeners - log('iceGatheringState', self.connection.iceGatheringState); - - if (self.trickleIce) return; - if (self.connection.iceGatheringState == 'complete') { - returnSDP(); - } - }; - - function returnSDP() { - self.onSessionDescription(self.connection.localDescription, self.streaminfo); - } - - this.connection.onaddstream = function (e) { - log('onaddstream', 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 () { - self.connection && self.oniceconnectionstatechange({ - iceConnectionState: self.connection.iceConnectionState, - iceGatheringState: self.connection.iceGatheringState, - signalingState: self.connection.signalingState - }); - }; - var self = this; - }, - setBandwidth: function (sdp) { - if (isMobileDevice || isFirefox || !this.bandwidth) return sdp; - - var bandwidth = this.bandwidth; - - if (this.session.screen) { - if (!bandwidth.screen) { - warn('It seems that you are not using bandwidth for screen. Screen sharing is expected to fail.'); - } else if (bandwidth.screen < 300) { - 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 && this.session.screen) { - 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 || bandwidth.data) { - sdp = sdp.replace(/b=AS([^\r\n]+\r\n)/g, ''); - } - - if (bandwidth.audio) { - sdp = sdp.replace(/a=mid:audio\r\n/g, 'a=mid:audio\r\nb=AS:' + bandwidth.audio + '\r\n'); - } - - if (bandwidth.video) { - sdp = sdp.replace(/a=mid:video\r\n/g, 'a=mid:video\r\nb=AS:' + (bandwidth.screen || bandwidth.video) + '\r\n'); - } - - if (bandwidth.data && !this.preferSCTP) { - sdp = sdp.replace(/a=mid:data\r\n/g, 'a=mid:data\r\nb=AS:' + bandwidth.data + '\r\n'); - } - - return sdp; - }, - setConstraints: function () { - this.constraints = { - optional: this.sdpConstraints.optional || isChrome ? [{ - VoiceActivityDetection: false - }] : [], - mandatory: this.sdpConstraints.mandatory || { - OfferToReceiveAudio: !!this.session.audio, - OfferToReceiveVideo: !!this.session.video || !!this.session.screen - } - }; - - if (this.constraints.mandatory) { - log('sdp-mandatory-constraints', toStr(this.constraints.mandatory)); - } - - if (this.constraints.optional) { - log('sdp-optional-constraints', toStr(this.constraints.optional)); - } - - this.optionalArgument = { - optional: this.optionalArgument.optional || [{ - DtlsSrtpKeyAgreement: true - }], - mandatory: this.optionalArgument.mandatory || {} - }; - - if (isChrome && chromeVersion >= 32 && !isNodeWebkit) { - /* - this.optionalArgument.optional.push({ - googIPv6: true - }); - this.optionalArgument.optional.push({ - googDscp: true - }); - this.optionalArgument.optional.push({ - googImprovedWifiBwe: true - }); - */ - } - - if (!this.preferSCTP) { - this.optionalArgument.optional.push({ - RtpDataChannels: true - }); - } - - log('optional-argument', toStr(this.optionalArgument.optional)); - - if (typeof this.iceServers != 'undefined') { - this.iceServers = { - iceServers: this.iceServers - }; - } else this.iceServers = null; - - log('ice-servers', toStr(this.iceServers.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) { - if (!sessionDescription) throw 'Remote session description should NOT be NULL.'; - - log('setting remote description', sessionDescription.type, sessionDescription.sdp); - this.connection.setRemoteDescription( - new RTCSessionDescription(sessionDescription), - this.onSdpSuccess, this.onSdpError - ); - }, - addIceCandidate: function (candidate) { - var iceCandidate = new RTCIceCandidate({ - sdpMLineIndex: candidate.sdpMLineIndex, - candidate: candidate.candidate - }); - - this.connection.addIceCandidate(iceCandidate, function() {}, function() { - error('onIceFailure', arguments, candidate.candidate); - }); - }, - createDataChannel: function (channelIdentifier) { - if (!this.channels) this.channels = []; - - // protocol: 'text/chat', preset: true, stream: 16 - // maxRetransmits:0 && ordered: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) - ); - } - - var self = this; - }, - setChannelEvents: function (channel) { - var self = this; - channel.onmessage = function (event) { - self.onmessage(event.data); - }; - - var numberOfTimes = 0; - channel.onopen = function () { - channel.push = channel.send; - channel.send = function (data) { - if (channel.readyState != 'open') { - 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); - }, - attachMediaStreams: function () { - var streams = this.attachStreams; - for (var i = 0; i < streams.length; i++) { - log('attaching stream:', streams[i].streamid); - this.connection.addStream(streams[i]); - } - this.getStreamInfo(); - }, - getStreamInfo: function () { - this.streaminfo = ''; - var streams = this.attachStreams; - for (var i = 0; i < streams.length; i++) { - if (i == 0) { - this.streaminfo = streams[i].streamid; - } else { - this.streaminfo += '----' + streams[i].streamid; - } - } - this.attachStreams = []; - }, - recreateOffer: function (renegotiate, callback) { - // if(isFirefox) this.create(this.type, this); - - log('recreating offer'); - - this.type = 'offer'; - this.renegotiate = true; - 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.renegotiate = true; - 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 video_constraints = { - mandatory: {}, - optional: [] - }; - - /* by @FreCap pull request #41 */ - var currentUserMediaRequest = { - streams: [], - mutex: false, - queueRequests: [] - }; - - function getUserMedia(options) { - 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 n = navigator, - hints = options.constraints || { - audio: true, - video: video_constraints - }; - - if (hints.video == true) hints.video = video_constraints; - - // connection.mediaConstraints.audio = false; - if (typeof mediaConstraints.audio != 'undefined') { - hints.audio = mediaConstraints.audio; - } - - // connection.mediaConstraints.video = false; - if (typeof mediaConstraints.video != 'undefined' && hints.video) { - hints.video = merge(hints.video, mediaConstraints.video); - } - - // connection.media.min(320,180); - // connection.media.max(1920,1080); - var media = options.media; - if (isChrome) { - var mandatory = {}; - - if (media.minWidth) { - mandatory.minWidth = media.minWidth; - } - - if (media.minHeight) { - mandatory.minHeight = media.minHeight; - } - - if (media.maxWidth) { - mandatory.maxWidth = media.maxWidth; - } - - if (media.maxHeight) { - mandatory.maxHeight = media.maxHeight; - } - - if (media.minAspectRatio) { - mandatory.minAspectRatio = media.minAspectRatio; - } - - if (media.maxFrameRate) { - mandatory.maxFrameRate = media.maxFrameRate; - } - - if (media.minFrameRate) { - mandatory.minFrameRate = media.minFrameRate; - } - - if (mandatory.minWidth && mandatory.minHeight) { - // code.google.com/p/chromium/issues/detail?id=143631#c9 - 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 (mediaConstraints.mandatory) { - hints.video.mandatory = merge(hints.video.mandatory, mediaConstraints.mandatory); - } - - if(hints.video && hints.video.optional) { - if(media.bandwidth) { - hints.video.optional.push({ - bandwidth: media.bandwidth - }); - } - } - - // mediaConstraints.optional.bandwidth = 1638400; - if (mediaConstraints.optional) - hints.video.optional[0] = merge({}, mediaConstraints.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.optional && hints.video.mandatory) { - if (!hints.video.optional.length && isEmpty(hints.video.mandatory)) { - hints.video = true; - } - } - - log('media hints:', toStr(hints)); - - // easy way to match - var idInstance = JSON.stringify(hints); - - function streaming(stream, returnBack, streamid) { - if (!streamid) streamid = getRandomString(); - - var video = options.video; - if (video) { - video[isFirefox ? 'mozSrcObject' : 'src'] = isFirefox ? stream : 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://dev.w3.org/2011/webrtc/editor/getusermedia.html#navigatorusermedia-interface-extensions - n.getMedia(hints, streaming, function (error) { - options.onerror(error, hints); - }); - } - } - - var FileSender = { - send: function (config) { - var connection = config.connection; - var channel = config.channel; - var privateChannel = config._channel; - var file = config.file; - - if (!config.file) { - error('You must select a file or pass Blob.'); - return; - } - - // max chunk sending limit on chrome is 64k - // max chunk receiving limit on firefox is 16k - var packetSize = (!!navigator.mozGetUserMedia || connection.preferSCTP) ? 15 * 1000 : 1 * 1000; - - if (connection.chunkSize) { - packetSize = connection.chunkSize; - } - - var textToTransfer = ''; - var numberOfPackets = 0; - var packets = 0; - - file.uuid = getRandomString(); - - function processInWebWorker() { - var blob = URL.createObjectURL(new Blob(['function readFile(_file) {postMessage(new FileReaderSync().readAsDataURL(_file));};this.onmessage = function (e) {readFile(e.data);}'], { - type: 'application/javascript' - })); - - var worker = new Worker(blob); - URL.revokeObjectURL(blob); - return worker; - } - - if (!!window.Worker && !isMobileDevice) { - var webWorker = processInWebWorker(); - - webWorker.onmessage = function (event) { - onReadAsDataURL(event.data); - }; - - webWorker.postMessage(file); - } else { - var reader = new FileReader(); - reader.onload = function (e) { - onReadAsDataURL(e.target.result); - }; - reader.readAsDataURL(file); - } - - function onReadAsDataURL(dataURL, text) { - var data = { - type: 'file', - uuid: file.uuid, - maxChunks: numberOfPackets, - currentPosition: numberOfPackets - packets, - name: file.name, - fileType: file.type, - size: file.size, - - userid: connection.userid, - extra: connection.extra - }; - - if (dataURL) { - text = dataURL; - numberOfPackets = packets = data.packets = parseInt(text.length / packetSize); - - file.maxChunks = data.maxChunks = numberOfPackets; - data.currentPosition = numberOfPackets - packets; - - file.userid = connection.userid; - file.extra = connection.extra; - file.sending = true; - connection.onFileStart(file); - } - - connection.onFileProgress({ - remaining: packets--, - length: numberOfPackets, - sent: numberOfPackets - packets, - - maxChunks: numberOfPackets, - uuid: file.uuid, - currentPosition: numberOfPackets - packets, - - sending: true - }, file.uuid); - - if (text.length > packetSize) data.message = text.slice(0, packetSize); - else { - data.message = text; - data.last = true; - data.name = file.name; - - file.url = URL.createObjectURL(file); - file.userid = connection.userid; - file.extra = connection.extra; - file.sending = true; - connection.onFileEnd(file); - } - - channel.send(data, privateChannel); - - textToTransfer = text.slice(data.message.length); - if (textToTransfer.length) { - setTimeout(function () { - onReadAsDataURL(null, textToTransfer); - }, connection.chunkInterval || 100); - } - } - } - }; - - function FileReceiver(connection) { - var content = {}, - packets = {}, - numberOfPackets = {}; - - function receive(data) { - var uuid = data.uuid; - - if (typeof data.packets !== 'undefined') { - numberOfPackets[uuid] = packets[uuid] = parseInt(data.packets); - data.sending = false; - connection.onFileStart(data); - } - - connection.onFileProgress({ - remaining: packets[uuid]--, - length: numberOfPackets[uuid], - received: numberOfPackets[uuid] - packets[uuid], - - maxChunks: numberOfPackets[uuid], - uuid: uuid, - currentPosition: numberOfPackets[uuid] - packets[uuid], - - sending: false - }, uuid); - - if (!content[uuid]) content[uuid] = []; - - content[uuid].push(data.message); - - if (data.last) { - var dataURL = content[uuid].join(''); - - FileConverter.DataURLToBlob(dataURL, data.fileType, function (blob) { - blob.uuid = uuid; - blob.name = data.name; - blob.type = data.fileType; - - blob.url = (window.URL || window.webkitURL).createObjectURL(blob); - - blob.sending = false; - blob.userid = data.userid || connection.userid; - blob.extra = data.extra || connection.extra; - connection.onFileEnd(blob); - - if (connection.autoSaveToDisk) { - FileSaver.SaveToDisk(blob.url, data.name); - } - - delete content[uuid]; - }); - } - } - - return { - receive: receive - }; - } - - var FileSaver = { - SaveToDisk: function (fileUrl, fileName) { - var hyperlink = document.createElement('a'); - hyperlink.href = fileUrl; - hyperlink.target = '_blank'; - hyperlink.download = fileName || fileUrl; - - var mouseEvent = new MouseEvent('click', { - view: window, - bubbles: true, - cancelable: true - }); - - hyperlink.dispatchEvent(mouseEvent); - - // (window.URL || window.webkitURL).revokeObjectURL(hyperlink.href); - } - }; - - var FileConverter = { - DataURLToBlob: function (dataURL, fileType, callback) { - - function processInWebWorker() { - var blob = URL.createObjectURL(new Blob(['function getBlob(_dataURL, _fileType) {var binary = atob(_dataURL.substr(_dataURL.indexOf(",") + 1)),i = binary.length,view = new Uint8Array(i);while (i--) {view[i] = binary.charCodeAt(i);};postMessage(new Blob([view], {type: _fileType}));};this.onmessage = function (e) {var data = JSON.parse(e.data); getBlob(data.dataURL, data.fileType);}'], { - type: 'application/javascript' - })); - - var worker = new Worker(blob); - URL.revokeObjectURL(blob); - return worker; - } - - if (!!window.Worker && !isMobileDevice) { - var webWorker = processInWebWorker(); - - webWorker.onmessage = function (event) { - callback(event.data); - }; - - webWorker.postMessage(JSON.stringify({ - dataURL: dataURL, - fileType: fileType - })); - } else { - var binary = atob(dataURL.substr(dataURL.indexOf(',') + 1)), - i = binary.length, - view = new Uint8Array(i); - - while (i--) { - view[i] = binary.charCodeAt(i); - } - - callback(new Blob([view])); - } - } - }; - - var TextSender = { - send: function (config) { - var connection = config.connection; - - var channel = config.channel, - _channel = config._channel, - initialText = config.text, - packetSize = connection.chunkSize || 1000, - textToTransfer = '', - isobject = false; - - if (typeof initialText !== 'string') { - 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 - }; - } - - function convertToAudioStream(mediaStream) { - if(!mediaStream) throw 'MediaStream is mandatory.'; - - var context = new AudioContext(); - var mediaStreamSource = context.createMediaStreamSource(mediaStream); - - var destination = context.createMediaStreamDestination(); - mediaStreamSource.connect(destination); - - return destination.stream; - } - - var isChrome = !!navigator.webkitGetUserMedia; - var isFirefox = !!navigator.mozGetUserMedia; - 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() { - return (Math.random() * new Date().getTime()).toString(36).replace(/\./g, ''); - } - - var chromeVersion = 50; - if (isChrome && navigator.userAgent.match(/Chrom(e|ium)\/([0-9]+)\./)[2]) { - chromeVersion = parseInt(navigator.userAgent.match(/Chrom(e|ium)\/([0-9]+)\./)[2], 10); - } - - function isData(session) { - return !session.audio && !session.video && !session.screen && session.data; - } - - function isEmpty(session) { - var length = 0; - for (var s in session) { - length++; - } - return length == 0; - } - - 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; - } - - if (isChrome || isFirefox) { - var log = console.log.bind(console); - var error = console.error.bind(console); - var warn = console.warn.bind(console); - } else { - function log() { - console.log(arguments); - } - - function error() { - console.error(arguments); - } - - function warn() { - console.warn(arguments); - } - } - - 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 isAudio = session.audio && !session.video && !session.screen; - if (isChrome && stream.getAudioTracks && stream.getVideoTracks) { - isAudio = stream.getAudioTracks().length && !stream.getVideoTracks().length; - } - - var mediaElement = document.createElement(isAudio ? 'audio' : 'video'); - - // "mozSrcObject" is always preferred over "src"!! - mediaElement[isFirefox ? 'mozSrcObject' : 'src'] = isFirefox ? stream : window.webkitURL.createObjectURL(stream); - - mediaElement.controls = true; - mediaElement.autoplay = !!session.remote; - mediaElement.muted = session.remote ? false : true; - - mediaElement.play(); - - return mediaElement; - } - - 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; - if (onload) script.onload = onload; - document.documentElement.appendChild(script); - } - - function muteOrUnmute(e) { - var stream = e.stream, - root = e.root, - session = e.session || {}, - enabled = e.enabled; - - if (!session.audio && !session.video) { - if (typeof session != 'string') { - 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 ? 'mute' : 'unmute', 'session', session); - - // enable/disable audio/video tracks - - if (session.audio) { - var audioTracks = stream.getAudioTracks()[0]; - if (audioTracks) - audioTracks.enabled = !enabled; - } - - if (session.video) { - var videoTracks = stream.getVideoTracks()[0]; - if (videoTracks) - videoTracks.enabled = !enabled; - } - - root.sockets.forEach(function (socket) { - if (root.type == 'local') - socket.send({ - userid: root.rtcMultiConnection.userid, - streamid: root.streamid, - mute: !!enabled, - unmute: !enabled, - session: session - }); - - if (root.type == 'remote') - socket.send({ - userid: root.rtcMultiConnection.userid, - promptMuteUnmute: true, - streamid: root.streamid, - mute: !!enabled, - unmute: !enabled, - session: session - }); - }); - - // 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 accurate session over "onstreamended" event. - var fakeObject = merge({}, root.streamObject); - fakeObject.session = session; - fakeObject.isAudio = session.audio && !session.video; - fakeObject.isVideo = (!session.audio && session.video) || (session.audio && session.video); - if (!!enabled) { - root.rtcMultiConnection.onmute(fakeObject); - } - - if (!enabled) { - root.rtcMultiConnection.onunmute(fakeObject); - } - } - - function stopTracks(mediaStream) { - if(!mediaStream) throw 'MediaStream argument is mandatory.'; - - if(typeof mediaStream.getAudioTracks == 'undefined') { - if(mediaStream.stop) { - mediaStream.stop(); - } - return; - } - - if(mediaStream.getAudioTracks().length && mediaStream.getAudioTracks()[0].stop) { - mediaStream.getAudioTracks()[0].stop(); - } - - if(mediaStream.getVideoTracks().length && mediaStream.getVideoTracks()[0].stop) { - mediaStream.getVideoTracks()[0].stop(); - } - - if(isFirefox) { - // todo-verify: this may cause multiple-invocation of "onstreamended" - if(mediaStream.onended) mediaStream.onended(); - } - } - - // https://github.com/muaz-khan/WebRTC-Experiment/tree/master/DetectRTC - var DetectRTC = {}; - - (function () { - - DetectRTC.hasMicrophone = false; - DetectRTC.hasSpeakers = false; - DetectRTC.hasWebcam = false; - - DetectRTC.MediaDevices = []; - - // 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! - - // Firefox seems having no support of enumerateDevices feature. - // Though there seems some clues of "navigator.getMediaDevices" implementation. - if (isFirefox) { - callback && callback(); - return; - } - - if(!navigator.getMediaDevices && MediaStreamTrack && MediaStreamTrack.getSources) { - navigator.getMediaDevices = MediaStreamTrack.getSources.bind(MediaStreamTrack); - } - - // if still no "getMediaDevices"; it MUST be Firefox! - if (!navigator.getMediaDevices) { - log('navigator.getMediaDevices is undefined.'); - // assuming that it is older chrome or chromium implementation - if (isChrome) { - DetectRTC.hasMicrophone = true; - DetectRTC.hasSpeakers = true; - DetectRTC.hasWebcam = true; - } - - callback && callback(); - return; - } - - navigator.getMediaDevices(function (devices) { - devices.forEach(function (device) { - // 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; - } - - DetectRTC.MediaDevices.push(device); - - if(device.kind == 'audioinput' || device.kind == 'audio') { - DetectRTC.hasMicrophone = true; - } - - if(device.kind == 'audiooutput') { - DetectRTC.hasSpeakers = true; - } - - if(device.kind == 'videoinput' || device.kind == 'video') { - DetectRTC.hasWebcam = true; - } - - // there is no "videoouput" in the spec. - }); - - if (callback) callback(); - }); - } - - DetectRTC.isWebRTCSupported = !!window.webkitRTCPeerConnection || !!window.mozRTCPeerConnection; - DetectRTC.isAudioContextSupported = (!!window.AudioContext || !!window.webkitAudioContext) && !!AudioContext.prototype.createMediaStreamSource; - DetectRTC.isScreenCapturingSupported = isChrome && chromeVersion >= 26 && (isNodeWebkit ? true : location.protocol == 'https:'); - DetectRTC.isSctpDataChannelsSupported = !!navigator.mozGetUserMedia || (isChrome && chromeVersion >= 25); - DetectRTC.isRtpDataChannelsSupported = isChrome && chromeVersion >= 31; - - // check for microphone/camera support! - CheckDeviceSupport(); - DetectRTC.load = CheckDeviceSupport; - - var screenCallback; - - DetectRTC.screen = { - chromeMediaSource: 'screen', - getSourceId: function (callback) { - if (!callback) throw '"callback" parameter is mandatory.'; - screenCallback = callback; - window.postMessage('get-sourceId', '*'); - }, - isChromeExtensionAvailable: function (callback) { - if (!callback) return; - - if (DetectRTC.screen.chromeMediaSource == 'desktop') callback(true); - - // ask extension if it is available - window.postMessage('are-you-there', '*'); - - setTimeout(function () { - if (DetectRTC.screen.chromeMediaSource == 'screen') { - callback(false); - } else callback(true); - }, 2000); - }, - onMessageCallback: function (data) { - 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 && 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) { - if (arguments.length != 2) throw 'You must pass extension-id.'; - - if (isFirefox) return callback('not-chrome'); - - 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('installed-disabled'); - } else callback('installed-enabled'); - }, 2000); - }; - image.onerror = function () { - callback('not-installed'); - }; - } - }; - - // check if desktop-capture extension installed. - if (window.postMessage && isChrome) { - DetectRTC.screen.isChromeExtensionAvailable(); - } - })(); - - // if IE - if (!window.addEventListener) { - window.addEventListener = function (el, eventName, eventHandler) { - if (!el.attachEvent) return; - el.attachEvent('on' + eventName, eventHandler); - } - } - - window.addEventListener('message', function (event) { - if (event.origin != window.location.origin) { - return; - } - - DetectRTC.screen.onMessageCallback(event.data); - }); - - 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)); - } - }); - } - - function setDefaults(connection) { - // 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)); - }; - - var progressHelper = {}; - - // www.RTCMultiConnection.org/docs/body/ - connection.body = document.body || document.documentElement; - - // www.RTCMultiConnection.org/docs/autoSaveToDisk/ - // to make sure file-saver dialog is not invoked. - connection.autoSaveToDisk = false; - - // 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) { - warn('Now, "autoSaveToDisk" is false. Read more here: http://www.RTCMultiConnection.org/docs/autoSaveToDisk/'); - 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/openSignalingChannel/ - // https://github.com/muaz-khan/WebRTC-Experiment/blob/master/Signaling.md - 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) { - this.push(data); - }; - - if (!connection.socket) - connection.socket = firebase; - - // if (channel != connection.channel || (connection.isInitiator && channel == connection.channel)) - firebase.onDisconnect().remove(); - - setTimeout(function () { - config.callback(firebase); - }, 1); - }; - - // www.RTCMultiConnection.org/docs/dontAttachStream/ - connection.dontAttachStream = false; - - // www.RTCMultiConnection.org/docs/onstream/ - connection.onstream = function (e) { - connection.body.insertBefore(e.mediaElement, connection.body.firstChild); - }; - - // www.RTCMultiConnection.org/docs/onstreamended/ - connection.onstreamended = function (e) { - if (e.mediaElement && e.mediaElement.parentNode) { - e.mediaElement.parentNode.removeChild(e.mediaElement); - } - }; - - // todo: need to write documentation link - connection.onSessionClosed = function (session) { - warn('Session has been closed.', session); - }; - - // www.RTCMultiConnection.org/docs/onmute/ - connection.onmute = function (e) { - log('onmute', 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) { - log('onunmute', 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 = function () { - // 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, ''); - } - }; - - // www.RTCMultiConnection.org/docs/userid/ - connection.userid = connection.token(); - - // www.RTCMultiConnection.org/docs/peers/ - connection.peers = {}; - 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']; - - // 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) { - // implementation from #68 - for (var stream in this) { - if (connection._skip.indexOf(stream) == -1) { - this[stream]._private(session, enabled); - } - } - }, - stop: function (type) { - // connection.streams.stop('local'); - var _stream; - for (var stream in this) { - if (stream != 'stop' && stream != 'mute' && stream != 'unmute' && stream != '_private') { - _stream = this[stream]; - - if (!type) _stream.stop(); - - if (type == 'local' && _stream.type == 'local') - _stream.stop(); - - if (type == 'remote' && _stream.type == 'remote') - _stream.stop(); - } - } - } - }; - - // 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/session/ - connection.session = { - audio: true, - video: true - }; - - // www.RTCMultiConnection.org/docs/bandwidth/ - connection.bandwidth = { - screen: 300 // 300kbps (dirty workaround) - }; - - connection.sdpConstraints = {}; - connection.mediaConstraints = {}; - connection.optionalArgument = {}; - connection.dataChannelDict = {}; - - var iceServers = []; - - iceServers.push({ - url: 'stun:stun.l.google.com:19302' - }); - - if (isChrome && chromeVersion < 28) { - iceServers.push({ - url: 'turn:homeo@turn.bistri.com:80?transport=udp', - credential: 'homeo' - }); - - iceServers.push({ - url: 'turn:homeo@turn.bistri.com:80?transport=tcp', - credential: 'homeo' - }); - } - - if (isFirefox || (isChrome && chromeVersion >= 28)) { - iceServers.push({ - url: 'turn:turn.bistri.com:80?transport=udp', - credential: 'homeo', - username: 'homeo' - }); - } - - if (isChrome && chromeVersion >= 28) { - iceServers.push({ - url: 'turn:turn.bistri.com:80?transport=tcp', - credential: 'homeo', - username: 'homeo' - }); - } - - connection.iceServers = iceServers; - - // 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 - - if (isFirefox) { - connection.preferSCTP = true; // FF supports only SCTP! - } - - // www.RTCMultiConnection.org/docs/fakeDataChannels/ - connection.fakeDataChannels = false; - - // www.RTCMultiConnection.org/docs/UA/ - connection.UA = { - Firefox: isFirefox, - Chrome: isChrome, - Mobile: isMobileDevice, - Version: chromeVersion, - NodeWebkit: isNodeWebkit - }; - - // file queue: to store previous file objects in memory; - // and stream over newly connected peers - // www.RTCMultiConnection.org/docs/fileQueue/ - connection.fileQueue = {}; - - // www.RTCMultiConnection.org/docs/media/ - connection.media = { - min: function (width, height) { - this.minWidth = width; - this.minHeight = height; - }, - max: function (width, height) { - this.maxWidth = width; - this.maxHeight = height; - }, - bandwidth: 256, - // maxFrameRate: 32, - // minFrameRate: 3, - minAspectRatio: 1.77 - }; - - // www.RTCMultiConnection.org/docs/candidates/ - connection.candidates = { - host: true, - relay: true, - reflexive: true - }; - - // www.RTCMultiConnection.org/docs/attachStreams/ - connection.attachStreams = []; - - // www.RTCMultiConnection.org/docs/detachStreams/ - connection.detachStreams = []; - - // 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'; - - connection._getStream = function (e) { - return { - rtcMultiConnection: e.rtcMultiConnection, - streamObject: e.streamObject, - stream: e.stream, - blobURL: e.blobURL, - session: e.session, - userid: e.userid, - streamid: e.streamid, - sockets: e.socket ? [e.socket] : [], - type: e.type, - mediaElement: e.mediaElement, - stop: function (forceToStopRemoteStream) { - var self = this; - - self.sockets.forEach(function (socket) { - if (self.type == 'local') { - socket.send({ - userid: self.rtcMultiConnection.userid, - extra: self.rtcMultiConnection.extra, - streamid: self.streamid, - stopped: true - }); - } - - if (self.type == 'remote' && !!forceToStopRemoteStream) { - socket.send({ - userid: self.rtcMultiConnection.userid, - promptStreamStop: true, - streamid: self.streamid - }); - } - }); - - var stream = self.stream; - if (stream) stopTracks(stream); - }, - mute: function (session) { - this.muted = true; - this._private(session, true); - }, - unmute: function (session) { - this.muted = false; - this._private(session, false); - }, - _private: function (session, enabled) { - muteOrUnmute({ - root: this, - session: session, - enabled: enabled, - stream: this.stream - }); - }, - startRecording: function (session) { - var self = this; - - if (!session) { - session = { - audio: true, - video: true - }; - } - - if (typeof session == 'string') { - session = { - audio: session == 'audio', - video: session == 'video' - }; - } - - if (!window.RecordRTC) { - return loadScript(self.rtcMultiConnection.resources.RecordRTC, function () { - self.startRecording(session); - - log('RecordRTC.js is auto loaded from: ' + self.rtcMultiConnection.resources.RecordRTC); - }); - } - - self.videoRecorder = self.audioRecorder = null; - - log('startRecording session', session); - - if (isFirefox) { - // firefox supports both audio/video recording in single webm file - 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 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(); - }, - stopRecording: function (callback, session) { - if (!session) { - session = { - audio: true, - video: true - }; - } - - if (typeof session == 'string') { - session = { - audio: session == 'audio', - video: session == 'video' - }; - } - - 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() - }); - }); - } - } - }; - }; - - // new RTCMultiConnection().set({properties}).connect() - connection.set = function (properties) { - for (var property in properties) { - this[property] = properties[property]; - } - return this; - }; - - // www.RTCMultiConnection.org/docs/firebase/ - connection.firebase = 'chat'; - - // 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/stats/ - connection.stats = { - numberOfConnectedUsers: 0, - numberOfSessions: 0, - sessions: {} - }; - - // www.RTCMultiConnection.org/docs/getStats/ - connection.getStats = function (callback) { - var numberOfConnectedUsers = 0; - for (var peer in connection.peers) { - numberOfConnectedUsers++; - } - - connection.stats.numberOfConnectedUsers = numberOfConnectedUsers; - - if (callback) callback(connection.stats); - }; - - // 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, - checkIfScreenSharingFlagEnabled: function (callback) { - var warning; - if (isFirefox) { - warning = 'Screen sharing is NOT supported on Firefox.'; - error(warning); - if (callback) callback(false); - } - - if (location.protocol !== 'https:') { - warning = 'Screen sharing is NOT supported on ' + location.protocol + ' Try https!'; - error(warning); - if (callback) return callback(false); - } - - if (chromeVersion < 26) { - warning = 'Screen sharing support is suspicious!'; - warn(warning); - } - - var screen_constraints = { - video: { - mandatory: { - chromeMediaSource: 'screen' - } - } - }; - - var invocationInterval = 0, - stop; - (function selfInvoker() { - invocationInterval++; - if (!stop) setTimeout(selfInvoker, 10); - })(); - - navigator.webkitGetUserMedia(screen_constraints, onsuccess, onfailure); - - function onsuccess(stream) { - if (stream.stop) { - stream.stop(); - } - - if (callback) { - callback(true); - } - } - - function onfailure() { - stop = true; - if (callback) callback(invocationInterval > 5, warning); - } - }, - - RtpDataChannels: DetectRTC.isRtpDataChannelsSupported, - SctpDataChannels: DetectRTC.isSctpDataChannelsSupported - }; - - // www.RTCMultiConnection.org/docs/snapshots/ - connection.snapshots = {}; - - // www.RTCMultiConnection.org/docs/takeSnapshot/ - connection.takeSnapshot = function (userid, callback) { - for (var stream in connection.streams) { - stream = connection.streams[stream]; - if (stream.userid == userid && stream.stream && stream.stream.getVideoTracks && stream.stream.getVideoTracks().length) { - var video = stream.streamObject.mediaElement; - 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(); - callback && callback(connection.snapshots[userid]); - continue; - } - } - }; - - 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.WebRTC-Experiment.com/demos/MediaStreamTrack.getSources.html - connection._mediaSources = {}; - - // 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/devices/ - connection.devices = {}; - - // 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); - }; - - // 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 || {}; - this.attachStreams = []; - - // "drop" should detach all local streams - for (var stream in this.streams) { - if (this._skip.indexOf(stream) == -1) { - stream = this.streams[stream]; - if (stream.type == 'local') { - this.detachStreams.push(stream.streamid); - this.onstreamended(stream.streamObject); - } else this.onstreamended(stream.streamObject); - } - } - - // www.RTCMultiConnection.org/docs/sendCustomMessage/ - this.sendCustomMessage({ - drop: true, - dontRenegotiate: typeof config.renegotiate == 'undefined' ? true : config.renegotiate - }); - }; - - // 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 API key; if possible - connection.googKey = 'AIzaSyCUmCjvKRb-kOYrnoL2xaXb8I-_JJeKpf0'; - - // 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); - } - }; - - 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; - setTimeout(function () { - var root = connection.streams[streamid]; - connection.streams[streamid].sockets.forEach(function (socket) { - socket.send({ - userid: connection.userid, - streamid: root.streamid, - isVolumeChanged: true, - volume: mediaElement.volume - }); - }); - volumeChangeEventFired = false; - }, 2000); - } - }; - }; - - connection.localStreamids = []; - - // www.RTCMultiConnection.org/docs/onMediaFile/ - connection.onMediaFile = function (e) { - log('onMediaFile', e); - connection.body.appendChild(e.mediaElement); - }; - - // this object stores pre-recorded media streaming uids - // multiple pre-recorded media files can be streamed concurrently. - connection.preRecordedMedias = {}; - - // 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) { - for (var peer in connection.peers) { - connection.peers[peer].sharePartOfScreen(args); - } - - 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 (typeof element == 'string') { - 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()); - } - }); - }; - - // it is false because workaround that is used to capture connections' failures - // affects renegotiation scenarios! - // todo: fix it! - connection.autoReDialOnFailure = false; - - 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; - - // 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!'); - }; - - 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); - }; - - // this feature added to keep users privacy and - // make sure HTTPs pages NEVER auto capture users media - connection.preventSSLAutoAllowed = isChrome && location.protocol == 'https:'; - - // resources used in RTCMultiConnection - connection.resources = { - RecordRTC: 'https://www.webrtc-experiment.com/RecordRTC.js', - PreRecordedMediaStreamer: 'https://www.rtcmulticonnection.org/PreRecordedMediaStreamer.js', - customGetUserMediaBar: 'https://www.webrtc-experiment.com/navigator.customGetUserMediaBar.js', - html2canvas: 'https://www.webrtc-experiment.com/screenshot.js', - hark: 'https://www.rtcmulticonnection.org/hark.js', - firebase: 'https://www.rtcmulticonnection.org/firebase.js', - firebaseio: 'https://chat.firebaseIO.com/', - muted: 'https://www.webrtc-experiment.com/images/muted.png' - }; - - // as @serhanters proposed in #225 - // it will auto fix "all" renegotiation scenarios - connection.sdpConstraints.mandatory = { - OfferToReceiveAudio: true, - OfferToReceiveVideo: true - }; - - connection.onstatechange = function(state, reason) { - // fetching-usermedia - // usermedia-fetched - - // detecting-room-presence - // room-not-available - // room-available - - // connecting-with-initiator - // connected-with-initiator - - // failed---has reason - - // request-accepted - // request-rejected - - log('onstatechange:', state, reason ? ':- ' + reason : ''); - }; - - // auto leave on page unload - connection.leaveOnPageUnload = true; - - // part-of-screen fallback for firefox - // when { screen: true } - } -})(); diff --git a/RTCMultiConnection/Library/RTCMultiConnection-v1.9.js b/RTCMultiConnection/Library/RTCMultiConnection-v1.9.js deleted file mode 100644 index 486d20d0..00000000 --- a/RTCMultiConnection/Library/RTCMultiConnection-v1.9.js +++ /dev/null @@ -1,5301 +0,0 @@ -// Last time updated at July 30, 2014, 08:32:23 - -// 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 -// v1.9 changes log - www.RTCMultiConnection.org/changes-log/#v1.9 -// Demos - www.WebRTC-Experiment.com/RTCMultiConnection - -// _______________________ -// RTCMultiConnection-v1.9 - -/* issues/features need to be fixed & implemented: - --. https://bugzilla.mozilla.org/show_bug.cgi?id=1045810 --. workaround-added: Firefox don't yet support onended for any stream (remote/local) - --. RTCMultiConnection is updated for audio+screen from single getUserMedia request for Firefox Nightly. - --. todo: onFileProgress: if connection gets dropped; re-attempt sharing from last position/chunk. - --. screen capturing support for firefox nightly added. --. command-line flag: media.getusermedia.screensharing.enabled - --. connection.onstreamid added. --. connection.onconnected, connection.ondisconnected, connection.onfailed added. - --. connection.eject is fixed. Now, onSessionClosed has "event.isEjected" boolean. --. (fixed) connection.playRoleOfInitiator must have extra-data as well. - --. connection.dontCaptureUserMedia added. Now, connection.dontAttachStream merely prevents attachment of any stream. --. these are breaking changes for those who are using dontAttachStream in their client-scripts. - --. connection.join is improved. --. fixed: remote.streamid and remote.isScreen - --. todo: add connection.switchStream(session) to remove old ones, and add new stream(s) - --. todo: add connection.keepStreamsOpened --. support inactive at initial handshake --. todo: sharePartOfScreen must be fixed for "performance" when sharing over all users. --. audio+video recording must support single-file for Firefox. --. need to use maxaveragebitrate for audio bandwidth. --. need to add LAN connectivity support. --. (demos needed) Temasys/EasyRTC plugins for IE/Safari. --. todo: add mp3-live streaming support --. todo: add mozCaptureStreamUntilEnded streaming support. --. todo: Fix "disconnected" which happens often. Need to use WebRTC data channels for dirty workaround whenever possible; currently we're relying on signaling medium. --. todo: check if stream.onended is fired on Firefox. --. todo: add removeStream workaround for Firefox. --. todo: auto fallback to part-of-screen option for Firefox. --. todo-fix: trickleIce & renegotiation fails. --. "channel" object in the openSignalingChannel shouldn't be mandatory! --. JSON parse/stringify options for data transmitted using data-channels; e.g. connection.preferJSON = true; --. removeTrack() and addTracks() instead of "stop" --. voice translation using Translator.js -*/ - -(function () { - // www.RTCMultiConnection.org/docs/constructor/ - window.RTCMultiConnection = function (channel) { - // a reference to your constructor! - var connection = this; - - // www.RTCMultiConnection.org/docs/channel-id/ - connection.channel = channel || location.href.replace(/\/|:|#|%|\.|\[|\]/g, ''); - - var rtcMultiSession; // a reference to backbone object i.e. RTCMultiSession! - - // 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 (typeof args == 'string') { - connection.sessionid = args; - } else { - if (typeof args.transmitRoomOnce != 'undefined') { - connection.transmitRoomOnce = args.transmitRoomOnce; - } - - if (typeof args.dontTransmit != 'undefined') { - dontTransmit = args.dontTransmit; - } - - if (typeof args.sessionid != 'undefined') { - 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.stats.sessions[connection.sessionDescription.sessionid]) { - connection.stats.numberOfSessions++; - connection.stats.sessions[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) { - rtcMultiSession.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 - }); - - // to let user know that media resource has been captured - // now, he can share "sessionDescription" using sockets - if (rtcMultiSession.onMediaCaptured) rtcMultiSession.onMediaCaptured(); - }); - - 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) { - // 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.forEach) { - // 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++) { - connection.send(data[i], _channel); - } - return; - } - - // File or Blob object MUST have "type" and "size" properties - if (typeof data.size != 'undefined' && typeof data.type != 'undefined') { - // to send multiple files concurrently! - // file of any size; maximum length: 1GB - FileSender.send({ - file: data, - channel: rtcMultiSession, - _channel: _channel, - connection: connection - }); - } 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); - } - - function joinSession(session, joinAs) { - if (typeof session == 'string') { - 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 (typeof session == 'string') { - if (connection.stats.sessions[session]) { - session = connection.stats.sessions[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 "join" method'; - connection.onstatechange('failed', 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; - } - - if(connection.dontAttachStream) { - // this notification should be removed when v1.9 gets stable - error('Do you know? You can use dontCaptureUserMedia instead of dontAttachStream.'); - } - - // 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, - video: !!session.video - }; - - // if custom audio device is selected - if (connection._mediaSources.audio) { - constraints.audio = { - optional: [{ - sourceId: connection._mediaSources.audio - }] - }; - } - - // if custom video device is selected - if (connection._mediaSources.video) { - constraints.video = { - optional: [{ - sourceId: connection._mediaSources.video - }] - }; - } - - // for connection.session = {}; - if(!session.screen && !constraints.audio && !constraints.video) { - return callback(); - } - - var screen_constraints = { - audio: false, - video: { - mandatory: { - chromeMediaSource: DetectRTC.screen.chromeMediaSource, - maxWidth: 1920, - maxHeight: 1080, - minAspectRatio: 1.77 - }, - optional: [] - } - }; - - if(isFirefox && session.screen) { - warn(Firefox_Screen_Capturing_Warning); - screen_constraints.video = merge(screen_constraints.video.mandatory, { - 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 - 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) { - window.addEventListener('message', function (event) { - if (event.data && event.data.chromeMediaSourceId) { - var sourceId = event.data.chromeMediaSourceId; - - DetectRTC.screen.sourceId = sourceId; - DetectRTC.screen.chromeMediaSource = 'desktop'; - - if (sourceId == 'PermissionDeniedError') { - var mediaStreamError = { - message: 'User denied to share content of his screen.', - name: 'PermissionDeniedError', - constraintName: screen_constraints, - session: session - }; - currentUserMediaRequest.mutex = false; - 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(DetectRTC.screen.extensionid, 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 did not responded. 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); - - connection.onstatechange('fetching-usermedia'); - - function _captureUserMedia(forcedConstraints, forcedCallback, isRemoveVideoTracks, dontPreventSSLAutoAllowed) { - - 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) { - connection.onstatechange('usermedia-fetched'); - - if (isRemoveVideoTracks) { - stream = convertToAudioStream(stream); - } - - connection.localStreamids.push(streamid); - stream.onended = function () { - connection.onstreamended(streamedObject); - - // 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, - userid: _stream.rtcMultiConnection.userid, - extra: _stream.rtcMultiConnection.extra, - 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; - }; - - var mediaElement = createMediaElement(stream, session); - - mediaElement.muted = true; - - stream.streamid = streamid; - stream.isScreen = forcedConstraints == screen_constraints; - - 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.getVideoTracks().length > 0, - isAudio: !stream.getVideoTracks().length && stream.getAudioTracks().length > 0, - isScreen: forcedConstraints == screen_constraints, - isInitiator: !!connection.isInitiator - }; - - var sObject = { - stream: stream, - blobURL: streamedObject.blobURL, - userid: connection.userid, - streamid: streamid, - session: session, - type: 'local', - streamObject: streamedObject, - mediaElement: mediaElement, - rtcMultiConnection: connection - }; - - if (isFirstSession) { - connection.attachStreams.push(stream); - } - isFirstSession = false; - - connection.streams[streamid] = connection._getStream(sObject); - - 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 - }); - } - }, - onerror: function (e, constraintUsed) { - // http://dev.w3.org/2011/webrtc/editor/getusermedia.html#h2_error-handling - 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 (typeof e == 'string') { - 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 has not yet released their screen capturing modules. Still work in progress! Please try chrome for now!'); - } 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".'); - } - } - - 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); - } - } - - // www.RTCMultiConnection.org/docs/captureUserMedia/ - connection.captureUserMedia = captureUserMedia; - - // www.RTCMultiConnection.org/docs/leave/ - connection.leave = function (userid) { - 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) { - rtcMultiSession.addStream({ - renegotiate: session || { - oneway: true, - audio: true, - video: true - }, - stream: stream - }); - }; - - // www.RTCMultiConnection.org/docs/addStream/ - connection.addStream = function (session, socket) { - // www.RTCMultiConnection.org/docs/renegotiation/ - - // 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) { - // detach pre-attached streams - if (!connection.streams[streamid]) return warn('No such stream exists. Stream-id:', streamid); - - // www.RTCMultiConnection.org/docs/detachStreams/ - connection.detachStreams.push(streamid); - connection.renegotiate(); - }; - - // set RTCMultiConnection defaults on constructor invocation - setDefaults(connection); - }; - - function RTCMultiSession(connection, callbackForSignalingReady) { - var fileReceiver = new FileReceiver(connection); - var textReceiver = new TextReceiver(connection); - - function onDataChannelMessage(e) { - if (!e) return; - - e = JSON.parse(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 (typeof e.data.maxChunks != 'undefined') { - fileReceiver.receive(e.data); - } 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); - } - - var socketObjects = {}; - var sockets = []; - - var rtcMultiSession = this; - - var participants = {}; - - 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 "onstreamended" 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); - } - }; - - socketConfig.callback = function (_socket) { - socket = _socket; - socketConfig.onopen(); - }; - - var socket = connection.openSignalingChannel(socketConfig), - 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.'; - - if (!connection.candidates.host && candidate.candidate.indexOf('typ host') != -1) return; - if (!connection.candidates.relay && candidate.candidate.indexOf('typ relay') != -1) return; - if (!connection.candidates.reflexive && candidate.candidate.indexOf('typ srflx') != -1) return; - - if (!connection.iceProtocols.udp && candidate.candidate.indexOf(' udp ') != -1) return; - if (!connection.iceProtocols.tcp && candidate.candidate.indexOf(' tcp ') != -1) return; - - log(candidate.candidate); - - socket && socket.send({ - userid: connection.userid, - candidate: { - sdpMLineIndex: candidate.sdpMLineIndex, - candidate: JSON.stringify(candidate.candidate) - } - }); - }, - onmessage: onDataChannelMessage, - onaddstream: function (stream, session) { - session = session || _config.renegotiate || connection.session; - - // if it is Firefox; 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; - } - } - - if (_config.streaminfo) { - var streaminfo = _config.streaminfo.split('----'); - var strInfo = JSON.parse(streaminfo[streaminfo.length-1]); - - stream.streamid = strInfo.streamid; - stream.isScreen = strInfo.isScreen; - - streaminfo.pop(); - _config.streaminfo = streaminfo.join('----'); - } - - var mediaElement = createMediaElement(stream, merge({ - remote: true - }, session)); - - if (!stream.getVideoTracks().length) - mediaElement.addEventListener('play', function () { - setTimeout(function () { - mediaElement.muted = false; - afterRemoteStreamStartedFlowing({ - mediaElement: mediaElement, - session: session, - stream: stream - }); - }, 3000); - }, false); - else - waitUntilRemoteStreamStartsFlowing({ - mediaElement: mediaElement, - session: session, - stream: stream - }); - - if (connection.setDefaultEventsForMediaElement) { - connection.setDefaultEventsForMediaElement(mediaElement, stream.streamid); - } - }, - - onremovestream: function (event) { - warn('onremovestream', event); - }, - - 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.stats.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.stats.numberOfConnectedUsers == 1) { - connection.onstatechange('connected-with-initiator'); - } - - 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') { - connection.ondisconnected({ - userid: _config.userid, - extra: _config.extra, - peer: connection.peers[_config.userid], - targetuser: _config.userinfo - }); - } - - 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({ - userid: connection.userid, - extra: connection.extra || {}, - redial: true - }); - - // to make sure all old "remote" streams are also removed! - for (var stream in connection.streams) { - stream = connection.streams[stream]; - if (stream.userid == _config.userid && stream.type == 'remote') { - connection.onstreamended(stream.streamObject); - } - } - } - } - }, - - onsignalingstatechange: function (event) { - log('onsignalingstatechange', toStr(event)); - }, - - attachStreams: connection.dontAttachStream ? [] : connection.attachStreams, - iceServers: connection.iceServers, - 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.send({ - userid: connection.userid, - extra: connection.extra, - streamid: stream.streamid, - isScreen: stream.isScreen, - isAudio: stream.getAudioTracks().length && !stream.getVideoTracks().length, - isVideo: !!stream.getVideoTracks().length - }); - } - }; - - function waitUntilRemoteStreamStartsFlowing(args) { - - // chrome for android may have some features missing - if (isMobileDevice) { - 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)) { - afterRemoteStreamStartedFlowing(args); - } else { - if (args.numberOfTimes >= 60) { // wait 60 seconds while video is delivered! - socket.send({ - userid: connection.userid, - extra: connection.extra, - failedToReceiveRemoteVideo: true, - streamid: args.stream.streamid - }); - } else - 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 () { - connection.onstreamended(streamedObject); - }; - - var streamedObject = { - mediaElement: mediaElement, - - stream: stream, - streamid: stream.streamid, - session: session || connection.session, - - blobURL: mediaElement.mozSrcObject ? URL.createObjectURL(stream) : mediaElement.src, - type: 'remote', - - extra: _config.extra, - userid: _config.userid, - - isVideo: stream.getVideoTracks().length > 0, - isAudio: !stream.getVideoTracks().length && stream.getAudioTracks().length > 0, - isScreen: stream.isScreen, - isInitiator: !!_config.isInitiator - }; - - // connection.streams['stream-id'].mute({audio:true}) - connection.streams[stream.streamid] = connection._getStream({ - stream: stream, - blobURL: streamedObject.blobURL, - userid: _config.userid, - streamid: stream.streamid, - socket: socket, - type: 'remote', - streamObject: streamedObject, - mediaElement: mediaElement, - rtcMultiConnection: connection, - session: session || connection.session - }); - - connection.onstream(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 - }); - - // 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.stats.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 (typeof bandwidth == 'string') 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({ - userid: connection.userid, - extra: connection.extra || {}, - changeBandwidth: true, - bandwidth: bandwidth - }); - }, - sendCustomMessage: function (message) { - // connection.peers['user-id'].sendCustomMessage(); - - this.socket.send({ - userid: connection.userid, - extra: connection.extra || {}, - customMessage: true, - message: message - }); - }, - onCustomMessage: function (message) { - log('Received "private" message from', this.userid, - typeof message == 'string' ? 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); - connection.onstreamended(stream.streamObject); - } - - if (stream.type == 'remote' && stream.userid == this.userid) { - connection.onstreamended(stream.streamObject); - } - } - } - - !dontSendMessage && this.socket.send({ - userid: connection.userid, - extra: connection.extra || {}, - drop: true - }); - }, - hold: function (holdMLine) { - // connection.peers['user-id'].hold(); - - this.socket.send({ - userid: connection.userid, - extra: connection.extra || {}, - 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(); - - this.socket.send({ - userid: connection.userid, - extra: connection.extra || {}, - 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.streamObject)); - - // www.RTCMultiConnection.org/docs/onunhold/ - if (!isHold) - connection.onunhold(merge({ - kind: kind - }, stream.streamObject)); - } - } - } - }, - 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') { - connection.onstreamended(stream.streamObject); - } - } - } - - log('ReDialing...'); - - socket.send({ - userid: connection.userid, - extra: connection.extra, - recreatePeer: true - }); - - peer = new PeerConnection(); - peer.create('offer', peerConfig); - }, - sharePartOfScreen: function (args) { - // www.RTCMultiConnection.org/docs/onpartofscreen/ - - var element = args.element; - var that = this; - - if (!window.html2canvas) { - return loadScript(connection.resources.html2canvas, function () { - that.sharePartOfScreen(args); - }); - } - - if (typeof element == 'string') { - element = document.querySelector(element); - if (!element) element = document.getElementById(element); - } - if (!element) throw 'HTML Element is inaccessible!'; - - 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); - } - - // html2canvas.js is used to take screenshots - html2canvas(element, { - onrendered: function (canvas) { - var screenshot = canvas.toDataURL(); - - 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({ - userid: connection.userid, - extra: connection.extra, - screenshot: screenshot, - isPartOfScreen: true - }); - } - - // "once" can be used to share single screenshot - !args.once && setTimeout(partOfScreenCapturer, args.interval || 200); - } - }); - } - - partOfScreenCapturer(); - }, - getStats: function(callback, interval) { - if(!callback) throw 'callback is mandatory.'; - - if(!window.getRTCMultiConnectionStats) { - loadScript(connection.resources.getRTCMultiConnectionStats, invoker); - } - else invoker(); - - function invoker() { - getRTCMultiConnectionStats({ - peer: peer.connection, - callback: callback, - interval: interval, - connection: connection, - targetuser: _config.userid - }); - } - } - }; - } - - function onSessionOpened() { - // original conferencing infrastructure! - if (connection.isInitiator && getLength(participants) && getLength(participants) <= connection.maxParticipantsAllowed) { - log('syncing participants', participants); - log('inner-config-object', _config); - if (!connection.session.oneway && !connection.session.broadcast) { - defaultSocket.send({ - sessionid: connection.sessionid, - newParticipant: _config.userid || socket.channel, - userid: connection.userid, - extra: connection.extra, - 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 (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({ - sdpMLineIndex: response.candidate.sdpMLineIndex, - candidate: JSON.parse(response.candidate.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.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].streamObject; - } - - var session = response.session; - var fakeObject = merge({}, streamObject); - fakeObject.session = session; - fakeObject.isAudio = session.audio && !session.video; - fakeObject.isVideo = (!session.audio && session.video) || (session.audio && session.video); - - 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]) { - connection.onstreamended(connection.streams[response.streamid].streamObject); - } - } - - // to stop remote stream - if (response.promptStreamStop /* && !connection.isInitiator */ ) { - // var forceToStopRemoteStream = true; - // connection.streams['remote-stream-id'].stop( forceToStopRemoteStream ); - 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) { - stopTracks(stream); - connection.onstreamended(stream.streamObject); - } - } - } - - if (peer && peer.connection) { - if (peer.connection.signalingState != 'closed') { - 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); - - connection.onleave({ - userid: response.userid, - extra: response.extra, - entireSessionClosed: !!response.closeEntireSession - }); - } - - // 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); - } - 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) { - if (!connection.peers[response.userid]) throw 'No such peer exists.'; - connection.peers[response.userid].peer.hold = true; - connection.peers[response.userid].peer.holdMLine = response.holdMLine; - connection.peers[response.userid].renegotiate(); - - connection.peers[response.userid].fireHoldUnHoldEvents({ - kind: response.holdMLine, - isHold: true, - userid: response.userid - }); - } - - if (response.unhold) { - if (!connection.peers[response.userid]) throw 'No such peer exists.'; - connection.peers[response.userid].peer.hold = false; - connection.peers[response.userid].peer.holdMLine = response.holdMLine; - connection.peers[response.userid].renegotiate(); - - connection.peers[response.userid].fireHoldUnHoldEvents({ - kind: response.holdMLine, - isHold: false, - userid: response.userid - }); - } - - // 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({ - userid: connection.userid, - extra: connection.extra, - 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; - - if (isChrome || (isFirefox && !peer.connection.getLocalStreams().length)) { - peer.addStream(stream); - } - - connection.renegotiatedSessions[JSON.stringify(_config.renegotiate)] = { - session: _config.renegotiate, - stream: stream - }; - - delete _config.renegotiate; - - createAnswer(); - }, _config.renegotiate); - } - - function createAnswer() { - // because Firefox has no support of renegotiation yet; - // so both chrome and firefox should redial instead of renegotiate! - if (isFirefox || _config.userinfo.browser == 'firefox') { - if (connection.peers[_config.userid]) { - connection.peers[_config.userid].redial(); - } - return; - } - - peer.recreateAnswer(sdp, session, function (_sdp, streaminfo) { - sendsdp({ - sdp: _sdp, - socket: socket, - streaminfo: streaminfo - }); - }); - } - } - } - - 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({ - userid: connection.userid, - sdp: JSON.stringify(e.sdp), - extra: connection.extra, - 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, - userid: connection.userid, - targetUser: channel, - channel: new_channel, - extra: connection.extra - }); - } - - // if a user leaves - - function clearSession() { - connection.stats.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 - }); - } - } - - 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) { - connection.onstreamended(stream.streamObject); - 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++) { - stopTracks(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.stats = { - numberOfConnectedUsers: 0, - numberOfSessions: 0, - sessions: {} - }; - - 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) { - connection.onstreamended(connection.streams[stream].streamObject); - delete connection.streams[stream]; - } - } - - socketObjects = {}; - sockets = []; - participants = {}; - }; - - // www.RTCMultiConnection.org/docs/reject/ - connection.reject = function (userid) { - if (typeof userid != 'string') userid = userid.userid; - defaultSocket.send({ - rejectedRequestOf: userid, - userid: connection.userid, - extra: connection.extra || {} - }); - - // remove relevant data to allow him join again - connection.remove(userid); - }; - - window.addEventListener('beforeunload', function () { - if (!connection.leaveOnPageUnload) return; - - clearSession(); - }, false); - - window.addEventListener('keyup', function (e) { - if (!connection.leaveOnPageUnload) return; - - if (e.keyCode == 116) { - clearSession(); - } - }, false); - - 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.send({ - userid: connection.userid, - extra: connection.extra, - 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 - }); - } - } - } - - // 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 = connection.openSignalingChannel({ - onmessage: function (response) { - if (response.userid == connection.userid) return; - - if (response.sessionid && response.userid) { - if (!connection.stats.sessions[response.sessionid]) { - connection.stats.numberOfSessions++; - connection.stats.sessions[response.sessionid] = response; - } - } - - if (connection.isAcceptNewSession && response.sessionid && response.userid && !connection.sessionDescriptions[response.sessionid]) { - connection.sessionDescriptions[response.sessionid] = response; - - 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 && !participants[response.userid]) { - acceptRequest(response); - } - - if (response.acceptedRequestOf == connection.userid) { - connection.onstatechange('request-accepted'); - } - - if (response.rejectedRequestOf == connection.userid) { - connection.onstatechange('request-rejected'); - } - - 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); - connection.onstreamended(stream.streamObject); - } else connection.onstreamended(stream.streamObject); - } - } - - 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({ - userid: connection.userid, - extra: connection.extra, - sessionDescription: connection.sessionDescription, - responseFor: response.userid - }); - } - - if (response.sessionDescription && response.responseFor == connection.userid) { - var sessionDescription = response.sessionDescription; - if (!connection.stats.sessions[sessionDescription.sessionid]) { - connection.stats.numberOfSessions++; - connection.stats.sessions[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) { - log('shareWith:joinUsers', response.joinUsers); - joinParticipants(response.joinUsers); - } - - // participants are shared with all users - if (!response.shareWith && response.joinUsers) { - log('all:joinUsers', 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({ - userid: connection.userid, - extra: connection.extra, - messageFor: response.userid, - presenceState: 'available', - _config: response._config - }); - } - - if (response.presenceState == 'available') { - rtcMultiSession.presenceState = 'available'; - connection.onstatechange('room-available'); - - joinSession(response._config); - } - } - - if (response.donotJoin && response.messageFor == connection.userid) { - log(response.userid, 'is not joining your room.'); - } - }, - callback: function (socket) { - if (socket) defaultSocket = socket; - if (onSignalingReady) onSignalingReady(); - }, - onopen: function (socket) { - if (socket) defaultSocket = socket; - if (onSignalingReady) onSignalingReady(); - } - }); - - if (defaultSocket && onSignalingReady) setTimeout(onSignalingReady, 2000); - - if(connection.session.screen) { - loadScreenFrame(); - } - - connection.getExternalIceServers && loadIceFrame(function(iceServers) { - connection.iceServers = connection.iceServers.concat(iceServers); - }); - - 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 (typeof args.transmitRoomOnce != 'undefined') { - 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 of participants; - // by default, session is always overridden and set to the session coming from initiator! - if (!connection.dontOverrideSession) { - connection.session = _config.session || {}; - } - - 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 mandatory = connection.sdpConstraints.mandatory; - if (typeof mandatory.OfferToReceiveAudio == 'undefined') { - connection.sdpConstraints.mandatory.OfferToReceiveAudio = !!connection.session.audio; - } - if (typeof mandatory.OfferToReceiveVideo == 'undefined') { - connection.sdpConstraints.mandatory.OfferToReceiveVideo = !!connection.session.video || !!connection.session.screen; - } - - log(toStr(connection.sdpConstraints.mandatory)); - - var offers = {}; - if (connection.attachStreams.length) { - var stream = connection.attachStreams[connection.attachStreams.length - 1]; - if (stream.getAudioTracks().length) { - offers.audio = true; - } - if (stream.getVideoTracks().length) { - offers.video = true; - } - } - - log(toStr(offers)); - - connection.onstatechange('connecting-with-initiator'); - defaultSocket.send({ - participant: true, - userid: connection.userid, - channel: channel, - targetUser: _config.userid, - extra: connection.extra, - session: connection.session, - offers: { - audio: !!offers.audio, - video: !!offers.video - } - }); - - connection.skipOnNewSession = false; - } - - // 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('detecting-room-presence'); - - function contactInitiator() { - defaultSocket.send({ - userid: connection.userid, - extra: connection.extra, - 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') { - warn('Unable to reach initiator. Waiting for someone to open the room.'); - connection.onstatechange('room-not-available', 'Unable to reach session initiator.'); - 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({ - userid: connection.userid, - extra: connection.extra, - 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) { - message = JSON.stringify({ - 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) { - peer.attachStreams = [e.stream]; - } - - // detaching old streams - detachMediaStream(connection.detachStreams, peer.connection); - - if (e.stream && (session.audio || session.video || session.screen)) { - // removeStream is not yet implemented in Firefox - // if(isFirefox) peer.connection.removeStream(e.stream); - - if (isChrome || (isFirefox && !peer.connection.getLocalStreams().length)) { - peer.addStream(e.stream); - } - } - - // because Firefox has no support of renegotiation yet; - // so both chrome and firefox should redial instead of renegotiate! - if (isFirefox || _peer.userinfo.browser == 'firefox') { - return _peer.redial(); - } - - 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, - userid: connection.userid, - extra: connection.extra || {}, - 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.'); - } - - var mandatory = connection.sdpConstraints.mandatory; - if (typeof mandatory.OfferToReceiveAudio == 'undefined') { - connection.sdpConstraints.mandatory.OfferToReceiveAudio = !!response.offers.audio; - } - if (typeof mandatory.OfferToReceiveVideo == 'undefined') { - 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); - - // to let user know that media resource has been captured - if (rtcMultiSession.onMediaCaptured) rtcMultiSession.onMediaCaptured(); - }); - 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/sendCustomMessage/ - connection.sendCustomMessage = function (message) { - if (!defaultSocket) { - return setTimeout(function () { - connection.sendCustomMessage(message); - }, 1000); - } - - defaultSocket.send({ - userid: connection.userid, - customMessage: true, - message: message - }); - }; - - // www.RTCMultiConnection.org/docs/accept/ - connection.accept = function (e) { - // for backward compatibility - if (arguments.length > 1 && typeof arguments[0] == 'string') { - 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 RTCPeerConnection = window.mozRTCPeerConnection || window.webkitRTCPeerConnection; - var RTCSessionDescription = window.mozRTCSessionDescription || window.RTCSessionDescription; - var RTCIceCandidate = window.mozRTCIceCandidate || window.RTCIceCandidate; - - 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 (type) { - log('peer type is', type); - - if (type == 'answer') { - this.setRemoteDescription(this.offerDescription); - } - - var self = this; - this.connection[type == 'offer' ? 'createOffer' : 'createAnswer'](function (sessionDescription) { - sessionDescription.sdp = self.serializeSdp(sessionDescription.sdp); - self.connection.setLocalDescription(sessionDescription); - - if (self.trickleIce) { - self.onSessionDescription(sessionDescription, self.streaminfo); - } - - if (sessionDescription.type == 'offer') { - log('offer sdp', sessionDescription.sdp); - } - }, this.onSdpError, this.constraints); - }, - serializeSdp: function (sdp) { - // it is "connection.processSdp" - sdp = this.processSdp(sdp); - - if (isFirefox) return sdp; - - sdp = this.setBandwidth(sdp); - if (this.holdMLine == 'both') { - if (this.hold) { - this.prevSDP = sdp; - sdp = sdp.replace(/sendonly|recvonly|sendrecv/g, 'inactive'); - } else if (this.prevSDP) { - sdp = this.prevSDP; - } - } else if (this.holdMLine == 'audio' || this.holdMLine == 'video') { - sdp = sdp.split('m='); - - var audio = ''; - var video = ''; - - if (sdp[1] && sdp[1].indexOf('audio') == 0) { - audio = 'm=' + sdp[1]; - } - if (sdp[2] && sdp[2].indexOf('audio') == 0) { - audio = 'm=' + sdp[2]; - } - - if (sdp[1] && sdp[1].indexOf('video') == 0) { - video = 'm=' + sdp[1]; - } - if (sdp[2] && sdp[2].indexOf('video') == 0) { - video = 'm=' + sdp[2]; - } - - if (this.holdMLine == 'audio') { - if (this.hold) { - this.prevSDP = sdp[0] + audio + video; - sdp = sdp[0] + audio.replace(/sendonly|recvonly|sendrecv/g, 'inactive') + video; - } else if (this.prevSDP) { - sdp = this.prevSDP; - } - } - - if (this.holdMLine == 'video') { - if (this.hold) { - this.prevSDP = sdp[0] + audio + video; - sdp = sdp[0] + audio + video.replace(/sendonly|recvonly|sendrecv/g, 'inactive'); - } else if (this.prevSDP) { - sdp = this.prevSDP; - } - } - } - return sdp; - }, - 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); - }; - - this.connection.ongatheringchange = function () { - // this method is usually not fired. - // todo: need to fix event listeners - log('iceGatheringState', self.connection.iceGatheringState); - - if (self.trickleIce) return; - if (self.connection.iceGatheringState == 'complete') { - returnSDP(); - } - }; - - function returnSDP() { - self.onSessionDescription(self.connection.localDescription, self.streaminfo); - } - - this.connection.onaddstream = function (e) { - log('onaddstream', 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 () { - self.connection && self.oniceconnectionstatechange({ - iceConnectionState: self.connection.iceConnectionState, - iceGatheringState: self.connection.iceGatheringState, - signalingState: self.connection.signalingState - }); - }; - var self = this; - }, - setBandwidth: function (sdp) { - if (isMobileDevice || isFirefox || !this.bandwidth) return sdp; - - var bandwidth = this.bandwidth; - - if (this.session.screen) { - if (!bandwidth.screen) { - warn('It seems that you are not using bandwidth for screen. Screen sharing is expected to fail.'); - } else if (bandwidth.screen < 300) { - 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 && this.session.screen) { - 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 || bandwidth.data) { - sdp = sdp.replace(/b=AS([^\r\n]+\r\n)/g, ''); - } - - if (bandwidth.audio) { - sdp = sdp.replace(/a=mid:audio\r\n/g, 'a=mid:audio\r\nb=AS:' + bandwidth.audio + '\r\n'); - } - - if (bandwidth.video) { - sdp = sdp.replace(/a=mid:video\r\n/g, 'a=mid:video\r\nb=AS:' + (this.session.screen ? bandwidth.screen : bandwidth.video) + '\r\n'); - } - - if (bandwidth.data && !this.preferSCTP) { - sdp = sdp.replace(/a=mid:data\r\n/g, 'a=mid:data\r\nb=AS:' + bandwidth.data + '\r\n'); - } - - return sdp; - }, - setConstraints: function () { - this.constraints = { - optional: this.sdpConstraints.optional || isChrome ? [{ - VoiceActivityDetection: false - }] : [], - mandatory: this.sdpConstraints.mandatory || { - OfferToReceiveAudio: !!this.session.audio, - OfferToReceiveVideo: !!this.session.video || !!this.session.screen - } - }; - - if (this.constraints.mandatory) { - log('sdp-mandatory-constraints', toStr(this.constraints.mandatory)); - } - - if (this.constraints.optional) { - log('sdp-optional-constraints', toStr(this.constraints.optional)); - } - - this.optionalArgument = { - optional: this.optionalArgument.optional || [{ - DtlsSrtpKeyAgreement: true - }], - mandatory: this.optionalArgument.mandatory || {} - }; - - if (!this.preferSCTP) { - this.optionalArgument.optional.push({ - RtpDataChannels: true - }); - } - - log('optional-argument', toStr(this.optionalArgument.optional)); - - if (typeof this.iceServers != 'undefined') { - this.iceServers = { - iceServers: this.iceServers - }; - } else this.iceServers = null; - - log('ice-servers', toStr(this.iceServers.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) { - if (!sessionDescription) throw 'Remote session description should NOT be NULL.'; - - log('setting remote description', sessionDescription.type, sessionDescription.sdp); - this.connection.setRemoteDescription( - new RTCSessionDescription(sessionDescription), - this.onSdpSuccess, this.onSdpError - ); - }, - addIceCandidate: function (candidate) { - var iceCandidate = new RTCIceCandidate({ - sdpMLineIndex: candidate.sdpMLineIndex, - candidate: candidate.candidate - }); - - this.connection.addIceCandidate(iceCandidate, function () {}, function () { - error('onIceFailure', arguments, candidate.candidate); - }); - }, - createDataChannel: function (channelIdentifier) { - if (!this.channels) this.channels = []; - - // protocol: 'text/chat', preset: true, stream: 16 - // maxRetransmits:0 && ordered: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) - ); - } - - var self = this; - }, - setChannelEvents: function (channel) { - var self = this; - channel.onmessage = function (event) { - self.onmessage(event.data); - }; - - var numberOfTimes = 0; - channel.onopen = function () { - channel.push = channel.send; - channel.send = function (data) { - if (channel.readyState != 'open') { - 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) { - stream.streamid = getRandomString(); - } - - log('attaching stream:', stream.streamid); - - if(!stream.isScreen) { - stream.isScreen = false; - } - - 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 || false - }); - } else { - this.streaminfo += '----' + JSON.stringify({ - streamid: streams[i].streamid || '', - isScreen: streams[i].isScreen || false - }); - } - } - }, - recreateOffer: function (renegotiate, callback) { - // if(isFirefox) this.create(this.type, this); - - log('recreating offer'); - - this.type = 'offer'; - this.renegotiate = true; - 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.renegotiate = true; - 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 video_constraints = { - mandatory: {}, - optional: [] - }; - - /* by @FreCap pull request #41 */ - var currentUserMediaRequest = { - streams: [], - mutex: false, - queueRequests: [] - }; - - function getUserMedia(options) { - 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 n = navigator; - var hints = options.constraints || { - audio: true, - video: video_constraints - }; - - if(hints.video && hints.video.mozMediaSource) { - // todo: verify this case - mediaConstraints = {}; - } - - if (hints.video == true) hints.video = video_constraints; - - // connection.mediaConstraints.audio = false; - if (typeof mediaConstraints.audio != 'undefined') { - hints.audio = mediaConstraints.audio; - } - - // connection.mediaConstraints.video = false; - if (typeof mediaConstraints.video != 'undefined' && hints.video) { - hints.video = merge(hints.video, mediaConstraints.video); - } - - // connection.media.min(320,180); - // connection.media.max(1920,1080); - var mandatoryConstraints = mediaConstraints.mandatory; - if (mandatoryConstraints) { - var mandatory = {}; - - if (mandatoryConstraints.minWidth) { - mandatory.minWidth = mandatoryConstraints.minWidth; - } - - if (mandatoryConstraints.minHeight) { - mandatory.minHeight = mandatoryConstraints.minHeight; - } - - if (mandatoryConstraints.maxWidth) { - mandatory.maxWidth = mandatoryConstraints.maxWidth; - } - - if (mandatoryConstraints.maxHeight) { - mandatory.maxHeight = mandatoryConstraints.maxHeight; - } - - if (mandatoryConstraints.minAspectRatio) { - mandatory.minAspectRatio = mandatoryConstraints.minAspectRatio; - } - - if (mandatoryConstraints.maxFrameRate) { - mandatory.maxFrameRate = mandatoryConstraints.maxFrameRate; - } - - if (mandatoryConstraints.minFrameRate) { - mandatory.minFrameRate = mandatoryConstraints.minFrameRate; - } - - if (mandatory.minWidth && mandatory.minHeight) { - // code.google.com/p/chromium/issues/detail?id=143631#c9 - 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 (mediaConstraints.mandatory) { - hints.video.mandatory = merge(hints.video.mandatory, mediaConstraints.mandatory); - } - - // mediaConstraints.optional.bandwidth = 1638400; - if (mediaConstraints.optional && mediaConstraints.optional.forEach) { - hints.video.optional = mediaConstraints.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; - } - } - - log('media hints:', toStr(hints)); - - // easy way to match - var idInstance = JSON.stringify(hints); - - function streaming(stream, returnBack, streamid) { - if (!streamid) streamid = getRandomString(); - - var video = options.video; - if (video) { - video[isFirefox ? 'mozSrcObject' : 'src'] = isFirefox ? stream : 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://dev.w3.org/2011/webrtc/editor/getusermedia.html#navigatorusermedia-interface-extensions - n.getMedia(hints, streaming, function (error) { - options.onerror(error, hints); - }); - } - } - - var FileSender = { - send: function (config) { - var connection = config.connection; - var channel = config.channel; - var privateChannel = config._channel; - var file = config.file; - - if (!config.file) { - error('You must select a file or pass Blob.'); - return; - } - - // max chunk sending limit on chrome is 64k - // max chunk receiving limit on firefox is 16k - var packetSize = (!!navigator.mozGetUserMedia || connection.preferSCTP) ? 15 * 1000 : 1 * 1000; - - if (connection.chunkSize) { - packetSize = connection.chunkSize; - } - - var textToTransfer = ''; - var numberOfPackets = 0; - var packets = 0; - - file.uuid = getRandomString(); - - function processInWebWorker() { - var blob = URL.createObjectURL(new Blob(['function readFile(_file) {postMessage(new FileReaderSync().readAsDataURL(_file));};this.onmessage = function (e) {readFile(e.data);}'], { - type: 'application/javascript' - })); - - var worker = new Worker(blob); - URL.revokeObjectURL(blob); - return worker; - } - - if (!!window.Worker && !isMobileDevice) { - var webWorker = processInWebWorker(); - - webWorker.onmessage = function (event) { - onReadAsDataURL(event.data); - }; - - webWorker.postMessage(file); - } else { - var reader = new FileReader(); - reader.onload = function (e) { - onReadAsDataURL(e.target.result); - }; - reader.readAsDataURL(file); - } - - function onReadAsDataURL(dataURL, text) { - var data = { - type: 'file', - uuid: file.uuid, - maxChunks: numberOfPackets, - currentPosition: numberOfPackets - packets, - name: file.name, - fileType: file.type, - size: file.size, - - userid: connection.userid, - extra: connection.extra - }; - - if (dataURL) { - text = dataURL; - numberOfPackets = packets = data.packets = parseInt(text.length / packetSize); - - file.maxChunks = data.maxChunks = numberOfPackets; - data.currentPosition = numberOfPackets - packets; - - file.userid = connection.userid; - file.extra = connection.extra; - file.sending = true; - connection.onFileStart(file); - } - - connection.onFileProgress({ - remaining: packets--, - length: numberOfPackets, - sent: numberOfPackets - packets, - - maxChunks: numberOfPackets, - uuid: file.uuid, - currentPosition: numberOfPackets - packets, - - sending: true - }, file.uuid); - - if (text.length > packetSize) data.message = text.slice(0, packetSize); - else { - data.message = text; - data.last = true; - data.name = file.name; - - file.url = URL.createObjectURL(file); - file.userid = connection.userid; - file.extra = connection.extra; - file.sending = true; - connection.onFileEnd(file); - } - - channel.send(data, privateChannel); - - textToTransfer = text.slice(data.message.length); - if (textToTransfer.length) { - setTimeout(function () { - onReadAsDataURL(null, textToTransfer); - }, connection.chunkInterval || 100); - } - } - } - }; - - function FileReceiver(connection) { - var content = {}, - packets = {}, - numberOfPackets = {}; - - function receive(data) { - var uuid = data.uuid; - - if (typeof data.packets !== 'undefined') { - numberOfPackets[uuid] = packets[uuid] = parseInt(data.packets); - data.sending = false; - connection.onFileStart(data); - } - - connection.onFileProgress({ - remaining: packets[uuid]--, - length: numberOfPackets[uuid], - received: numberOfPackets[uuid] - packets[uuid], - - maxChunks: numberOfPackets[uuid], - uuid: uuid, - currentPosition: numberOfPackets[uuid] - packets[uuid], - - sending: false - }, uuid); - - if (!content[uuid]) content[uuid] = []; - - content[uuid].push(data.message); - - if (data.last) { - var dataURL = content[uuid].join(''); - - FileConverter.DataURLToBlob(dataURL, data.fileType, function (blob) { - blob.uuid = uuid; - blob.name = data.name; - blob.type = data.fileType; - - blob.url = (window.URL || window.webkitURL).createObjectURL(blob); - - blob.sending = false; - blob.userid = data.userid || connection.userid; - blob.extra = data.extra || connection.extra; - connection.onFileEnd(blob); - - if (connection.autoSaveToDisk) { - FileSaver.SaveToDisk(blob.url, data.name); - } - - delete content[uuid]; - }); - } - } - - return { - receive: receive - }; - } - - var FileSaver = { - SaveToDisk: function (fileUrl, fileName) { - var hyperlink = document.createElement('a'); - hyperlink.href = fileUrl; - hyperlink.target = '_blank'; - hyperlink.download = fileName || fileUrl; - - var mouseEvent = new MouseEvent('click', { - view: window, - bubbles: true, - cancelable: true - }); - - hyperlink.dispatchEvent(mouseEvent); - - // (window.URL || window.webkitURL).revokeObjectURL(hyperlink.href); - } - }; - - var FileConverter = { - DataURLToBlob: function (dataURL, fileType, callback) { - - function processInWebWorker() { - var blob = URL.createObjectURL(new Blob(['function getBlob(_dataURL, _fileType) {var binary = atob(_dataURL.substr(_dataURL.indexOf(",") + 1)),i = binary.length,view = new Uint8Array(i);while (i--) {view[i] = binary.charCodeAt(i);};postMessage(new Blob([view], {type: _fileType}));};this.onmessage = function (e) {var data = JSON.parse(e.data); getBlob(data.dataURL, data.fileType);}'], { - type: 'application/javascript' - })); - - var worker = new Worker(blob); - URL.revokeObjectURL(blob); - return worker; - } - - if (!!window.Worker && !isMobileDevice) { - var webWorker = processInWebWorker(); - - webWorker.onmessage = function (event) { - callback(event.data); - }; - - webWorker.postMessage(JSON.stringify({ - dataURL: dataURL, - fileType: fileType - })); - } else { - var binary = atob(dataURL.substr(dataURL.indexOf(',') + 1)), - i = binary.length, - view = new Uint8Array(i); - - while (i--) { - view[i] = binary.charCodeAt(i); - } - - callback(new Blob([view])); - } - } - }; - - var TextSender = { - send: function (config) { - var connection = config.connection; - - var channel = config.channel, - _channel = config._channel, - initialText = config.text, - packetSize = connection.chunkSize || 1000, - textToTransfer = '', - isobject = false; - - if (typeof initialText !== 'string') { - 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 - }; - } - - var webAudioMediaStreamSources = []; - - function convertToAudioStream(mediaStream) { - if (!mediaStream) throw 'MediaStream is mandatory.'; - - var context = new AudioContext(); - var mediaStreamSource = context.createMediaStreamSource(mediaStream); - - var destination = context.createMediaStreamDestination(); - mediaStreamSource.connect(destination); - - webAudioMediaStreamSources.push(mediaStreamSource); - - return destination.stream; - } - - var isChrome = !!navigator.webkitGetUserMedia; - var isFirefox = !!navigator.mozGetUserMedia; - 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() { - return (Math.random() * new Date().getTime()).toString(36).replace(/\./g, ''); - } - - var chromeVersion = 50; - if (isChrome && navigator.userAgent.match(/Chrom(e|ium)\/([0-9]+)\./)[2]) { - chromeVersion = parseInt(navigator.userAgent.match(/Chrom(e|ium)\/([0-9]+)\./)[2], 10); - } - - function isData(session) { - return !session.audio && !session.video && !session.screen && session.data; - } - - function isEmpty(session) { - var length = 0; - for (var s in session) { - length++; - } - return length == 0; - } - - 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; - } - - if (isChrome || isFirefox) { - var log = console.log.bind(console); - var error = console.error.bind(console); - var warn = console.warn.bind(console); - } else { - function log() { - console.log(arguments); - } - - function error() { - console.error(arguments); - } - - function warn() { - console.warn(arguments); - } - } - - 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 isAudio = session.audio && !session.video && !session.screen; - if (isChrome && stream.getAudioTracks && stream.getVideoTracks) { - isAudio = stream.getAudioTracks().length && !stream.getVideoTracks().length; - } - - var mediaElement = document.createElement(isAudio ? 'audio' : 'video'); - - // "mozSrcObject" is always preferred over "src"!! - mediaElement[isFirefox ? 'mozSrcObject' : 'src'] = isFirefox ? stream : window.webkitURL.createObjectURL(stream); - - mediaElement.controls = true; - mediaElement.autoplay = !!session.remote; - mediaElement.muted = session.remote ? false : true; - - // https://bugzilla.mozilla.org/show_bug.cgi?id=1045810 - // Firefox don't yet support onended for any stream (remote/local) - isFirefox && mediaElement.addEventListener('ended', function() { - stream.onended(); - }, false); - - mediaElement.play(); - - return mediaElement; - } - - 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); - } - - 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 - }, '*'); - } - }; - }; - - var iceFrame, loadedIceFrame; - - function loadIceFrame(callback, skip) { - if (loadedIceFrame) return; - if (!skip) return loadIceFrame(callback, true); - - loadedIceFrame = true; - - var iframe = document.createElement('iframe'); - iframe.onload = function () { - iframe.isLoaded = true; - - window.addEventListener('message', function (event) { - if (!event.data || !event.data.iceServers) return; - callback(event.data.iceServers); - }); - - iframe.contentWindow.postMessage('get-ice-servers', '*'); - }; - iframe.src = 'https://cdn.webrtc-experiment.com/getIceServers/'; - iframe.style.display = 'none'; - (document.body || document.documentElement).appendChild(iframe); - }; - - function muteOrUnmute(e) { - var stream = e.stream, - root = e.root, - session = e.session || {}, - enabled = e.enabled; - - if (!session.audio && !session.video) { - if (typeof session != 'string') { - 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 ? 'mute' : 'unmute', 'session', session); - - // enable/disable audio/video tracks - - if (session.audio) { - var audioTracks = stream.getAudioTracks()[0]; - if (audioTracks) - audioTracks.enabled = !enabled; - } - - if (session.video) { - var videoTracks = stream.getVideoTracks()[0]; - if (videoTracks) - videoTracks.enabled = !enabled; - } - - root.sockets.forEach(function (socket) { - if (root.type == 'local') - socket.send({ - userid: root.rtcMultiConnection.userid, - streamid: root.streamid, - mute: !!enabled, - unmute: !enabled, - session: session - }); - - if (root.type == 'remote') - socket.send({ - userid: root.rtcMultiConnection.userid, - promptMuteUnmute: true, - streamid: root.streamid, - mute: !!enabled, - unmute: !enabled, - session: session - }); - }); - - // 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 accurate session over "onstreamended" event. - var fakeObject = merge({}, root.streamObject); - fakeObject.session = session; - fakeObject.isAudio = session.audio && !session.video; - fakeObject.isVideo = (!session.audio && session.video) || (session.audio && session.video); - if (!!enabled) { - root.rtcMultiConnection.onmute(fakeObject); - } - - if (!enabled) { - root.rtcMultiConnection.onunmute(fakeObject); - } - } - - function stopTracks(mediaStream) { - if (!mediaStream) throw 'MediaStream argument is mandatory.'; - - if (typeof mediaStream.getAudioTracks == 'undefined') { - if (mediaStream.stop) { - mediaStream.stop(); - } - return; - } - - if (mediaStream.getAudioTracks().length && mediaStream.getAudioTracks()[0].stop) { - mediaStream.getAudioTracks()[0].stop(); - } - - if (mediaStream.getVideoTracks().length && mediaStream.getVideoTracks()[0].stop) { - mediaStream.getVideoTracks()[0].stop(); - } - - if (isFirefox) { - // Firefox don't yet support onended for any stream (remote/local) - if (mediaStream.onended) mediaStream.onended(); - } - } - - 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.'; - var ReservedExtensionID = 'ajhifddimkapgcifgcodmmfdlknahffk'; - - // if you deployed your own extension on Google App Store - var useCustomChromeExtensionForScreenCapturing = document.domain.indexOf('webrtc-experiment.com') != -1; - - // https://github.com/muaz-khan/WebRTC-Experiment/tree/master/DetectRTC - var DetectRTC = {}; - - (function () { - - DetectRTC.hasMicrophone = false; - DetectRTC.hasSpeakers = false; - DetectRTC.hasWebcam = false; - - DetectRTC.MediaDevices = []; - - // 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! - - // Firefox seems having no support of enumerateDevices feature. - // Though there seems some clues of "navigator.getMediaDevices" implementation. - if (isFirefox) { - callback && callback(); - return; - } - - if (!navigator.getMediaDevices && MediaStreamTrack && MediaStreamTrack.getSources) { - navigator.getMediaDevices = MediaStreamTrack.getSources.bind(MediaStreamTrack); - } - - // if still no "getMediaDevices"; it MUST be Firefox! - if (!navigator.getMediaDevices) { - log('navigator.getMediaDevices is undefined.'); - // assuming that it is older chrome or chromium implementation - if (isChrome) { - DetectRTC.hasMicrophone = true; - DetectRTC.hasSpeakers = true; - DetectRTC.hasWebcam = true; - } - - callback && callback(); - return; - } - - navigator.getMediaDevices(function (devices) { - devices.forEach(function (device) { - // 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; - } - - DetectRTC.MediaDevices.push(device); - - if (device.kind == 'audioinput' || device.kind == 'audio') { - DetectRTC.hasMicrophone = true; - } - - if (device.kind == 'audiooutput') { - DetectRTC.hasSpeakers = true; - } - - if (device.kind == 'videoinput' || device.kind == 'video') { - DetectRTC.hasWebcam = true; - } - - // there is no "videoouput" in the spec. - }); - - if (callback) callback(); - }); - } - - DetectRTC.isWebRTCSupported = !!window.webkitRTCPeerConnection || !!window.mozRTCPeerConnection; - DetectRTC.isAudioContextSupported = (!!window.AudioContext || !!window.webkitAudioContext) && !!AudioContext.prototype.createMediaStreamSource; - DetectRTC.isScreenCapturingSupported = isChrome && chromeVersion >= 26 && (isNodeWebkit ? true : location.protocol == 'https:'); - DetectRTC.isSctpDataChannelsSupported = !!navigator.mozGetUserMedia || (isChrome && chromeVersion >= 25); - DetectRTC.isRtpDataChannelsSupported = isChrome && chromeVersion >= 31; - - // check for microphone/camera support! - CheckDeviceSupport(); - DetectRTC.load = CheckDeviceSupport; - - var screenCallback; - - DetectRTC.screen = { - chromeMediaSource: 'screen', - extensionid: ReservedExtensionID, - getSourceId: function (callback) { - if (!callback) throw '"callback" parameter is mandatory.'; - screenCallback = callback; - window.postMessage('get-sourceId', '*'); - - // sometimes content-script mismatched URLs - // causes infinite delay. - setTimeout(function () { - if(!DetectRTC.screen.sourceId && DetectRTC.screen.chromeMediaSource == 'screen') { - callback('No-Response'); - } - }, 2000); - }, - isChromeExtensionAvailable: function (callback) { - if (!callback) return; - - if (DetectRTC.screen.chromeMediaSource == 'desktop') return callback(true); - - // ask extension if it is available - window.postMessage('are-you-there', '*'); - - setTimeout(function () { - if (DetectRTC.screen.chromeMediaSource == 'screen') { - callback(false); - } else callback(true); - }, 2000); - }, - onMessageCallback: function (data) { - if (!(typeof data == 'string' || !!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) { - 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.extensionid == extensionid ? 'installed-enabled' : 'installed-disabled'); - } else callback('installed-enabled'); - }, 2000); - }; - image.onerror = function () { - callback('not-installed'); - }; - } - }; - - // check if desktop-capture extension installed. - if (window.postMessage && isChrome) { - DetectRTC.screen.isChromeExtensionAvailable(); - } - })(); - - // if IE - if (!window.addEventListener) { - window.addEventListener = function (el, eventName, eventHandler) { - if (!el.attachEvent) return; - el.attachEvent('on' + eventName, eventHandler); - } - } - - window.addEventListener('message', function (event) { - if (event.origin != window.location.origin) { - return; - } - - DetectRTC.screen.onMessageCallback(event.data); - }); - - 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)); - } - }); - } - - function setDefaults(connection) { - // 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)); - }; - - var progressHelper = {}; - - // www.RTCMultiConnection.org/docs/body/ - connection.body = document.body || document.documentElement; - - // www.RTCMultiConnection.org/docs/autoSaveToDisk/ - // to make sure file-saver dialog is not invoked. - connection.autoSaveToDisk = false; - - // 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) { - warn('Now, "autoSaveToDisk" is false. Read more here: http://www.RTCMultiConnection.org/docs/autoSaveToDisk/'); - 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/openSignalingChannel/ - // https://github.com/muaz-khan/WebRTC-Experiment/blob/master/Signaling.md - 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(typeof data[prop] == 'undefined') { - data[prop] = false; - } - } - - this.push(data); - }; - - if (!connection.socket) - connection.socket = firebase; - - // if (channel != connection.channel || (connection.isInitiator && channel == connection.channel)) - firebase.onDisconnect().remove(); - - setTimeout(function () { - config.callback(firebase); - }, 1); - }; - - // www.RTCMultiConnection.org/docs/dontAttachStream/ - connection.dontAttachStream = false; - - // www.RTCMultiConnection.org/docs/dontCaptureUserMedia/ - connection.dontCaptureUserMedia = false; - - // www.RTCMultiConnection.org/docs/onstream/ - connection.onstream = function (e) { - connection.body.insertBefore(e.mediaElement, connection.body.firstChild); - }; - - // www.RTCMultiConnection.org/docs/onstreamended/ - connection.onstreamended = function (e) { - if (e.mediaElement && e.mediaElement.parentNode) { - 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) { - log('onmute', 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) { - log('onunmute', 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 = function () { - // 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, ''); - } - }; - - // www.RTCMultiConnection.org/docs/userid/ - connection.userid = connection.token(); - - // www.RTCMultiConnection.org/docs/peers/ - connection.peers = {}; - 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']; - - // 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) { - // implementation from #68 - for (var stream in this) { - if (connection._skip.indexOf(stream) == -1) { - this[stream]._private(session, enabled); - } - } - }, - stop: function (type) { - // connection.streams.stop('local'); - var _stream; - for (var stream in this) { - if (stream != 'stop' && stream != 'mute' && stream != 'unmute' && stream != '_private') { - _stream = this[stream]; - - if (!type) _stream.stop(); - - if (type == 'local' && _stream.type == 'local') - _stream.stop(); - - if (type == 'remote' && _stream.type == 'remote') - _stream.stop(); - } - } - } - }; - - // 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/session/ - connection.session = { - audio: true, - video: true - }; - - // www.RTCMultiConnection.org/docs/bandwidth/ - connection.bandwidth = { - screen: 300 // 300kbps (dirty workaround) - }; - - connection.sdpConstraints = {}; - connection.optionalArgument = {}; - connection.dataChannelDict = {}; - - var iceServers = []; - - iceServers.push({ - url: 'stun:stun.l.google.com:19302' - }); - - iceServers.push({ - url: 'stun:stun.anyfirewall.com:3478' - }); - - iceServers.push({ - url: 'turn:turn.bistri.com:80', - credential: 'homeo', - username: 'homeo' - }); - - iceServers.push({ - url: 'turn:turn.anyfirewall.com:443?transport=tcp', - credential: 'webrtc', - username: 'webrtc' - }); - - connection.iceServers = iceServers; - - // 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 - - if (isFirefox) { - connection.preferSCTP = true; // FF supports only SCTP! - } - - // www.RTCMultiConnection.org/docs/fakeDataChannels/ - connection.fakeDataChannels = false; - - // www.RTCMultiConnection.org/docs/UA/ - connection.UA = { - Firefox: isFirefox, - Chrome: isChrome, - Mobile: isMobileDevice, - Version: chromeVersion, - NodeWebkit: isNodeWebkit - }; - - // file queue: to store previous file objects in memory; - // and stream over newly connected peers - // www.RTCMultiConnection.org/docs/fileQueue/ - connection.fileQueue = {}; - - // www.RTCMultiConnection.org/docs/media/ - connection.media = { - min: function (width, height) { - if(!connection.mediaConstraints.mandatory) { - connection.mediaConstraints.mandatory = {}; - } - connection.mediaConstraints.mandatory.minWidth = width; - connection.mediaConstraints.mandatory.minHeight = height; - }, - max: function (width, height) { - if(!connection.mediaConstraints.mandatory) { - connection.mediaConstraints.mandatory = {}; - } - - connection.mediaConstraints.mandatory.maxWidth = width; - connection.mediaConstraints.mandatory.maxHeight = height; - } - }; - - connection.mediaConstraints = { - mandatory: {}, - optional: [] - }; - - // www.RTCMultiConnection.org/docs/candidates/ - connection.candidates = { - host: true, - relay: true, - reflexive: true - }; - - // www.RTCMultiConnection.org/docs/attachStreams/ - connection.attachStreams = []; - - // www.RTCMultiConnection.org/docs/detachStreams/ - connection.detachStreams = []; - - // 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'; - - connection._getStream = function (e) { - return { - rtcMultiConnection: e.rtcMultiConnection, - streamObject: e.streamObject, - stream: e.stream, - blobURL: e.blobURL, - session: e.session, - userid: e.userid, - streamid: e.streamid, - sockets: e.socket ? [e.socket] : [], - type: e.type, - mediaElement: e.mediaElement, - stop: function (forceToStopRemoteStream) { - var self = this; - - self.sockets.forEach(function (socket) { - if (self.type == 'local') { - socket.send({ - userid: self.rtcMultiConnection.userid, - extra: self.rtcMultiConnection.extra, - streamid: self.streamid, - stopped: true - }); - } - - if (self.type == 'remote' && !!forceToStopRemoteStream) { - socket.send({ - userid: self.rtcMultiConnection.userid, - promptStreamStop: true, - streamid: self.streamid - }); - } - }); - - var stream = self.stream; - if (stream) stopTracks(stream); - }, - mute: function (session) { - this.muted = true; - this._private(session, true); - }, - unmute: function (session) { - this.muted = false; - this._private(session, false); - }, - _private: function (session, enabled) { - muteOrUnmute({ - root: this, - session: session, - enabled: enabled, - stream: this.stream - }); - }, - startRecording: function (session) { - var self = this; - - if (!session) { - session = { - audio: true, - video: true - }; - } - - if (typeof session == 'string') { - session = { - audio: session == 'audio', - video: session == 'video' - }; - } - - if (!window.RecordRTC) { - return loadScript(self.rtcMultiConnection.resources.RecordRTC, function () { - self.startRecording(session); - }); - } - - self.videoRecorder = self.audioRecorder = null; - - log('startRecording session', session); - - if (isFirefox) { - // firefox supports both audio/video recording in single webm file - 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 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(); - }, - stopRecording: function (callback, session) { - if (!session) { - session = { - audio: true, - video: true - }; - } - - if (typeof session == 'string') { - session = { - audio: session == 'audio', - video: session == 'video' - }; - } - - 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() - }); - }); - } - } - }; - }; - - // new RTCMultiConnection().set({properties}).connect() - connection.set = function (properties) { - for (var property in properties) { - this[property] = properties[property]; - } - return this; - }; - - // www.RTCMultiConnection.org/docs/firebase/ - connection.firebase = 'chat'; - - // 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/stats/ - connection.stats = { - numberOfConnectedUsers: 0, - numberOfSessions: 0, - sessions: {} - }; - - // www.RTCMultiConnection.org/docs/getStats/ - connection.getStats = function (callback) { - var numberOfConnectedUsers = 0; - for (var peer in connection.peers) { - numberOfConnectedUsers++; - } - - connection.stats.numberOfConnectedUsers = numberOfConnectedUsers; - - if (callback) callback(connection.stats); - }; - - // 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, - checkIfScreenSharingFlagEnabled: function (callback) { - var warning; - if (isFirefox) { - warning = 'Screen sharing is NOT supported on Firefox.'; - error(warning); - if (callback) callback(false); - } - - if (location.protocol !== 'https:') { - warning = 'Screen sharing is NOT supported on ' + location.protocol + ' Try https!'; - error(warning); - if (callback) return callback(false); - } - - if (chromeVersion < 26) { - warning = 'Screen sharing support is suspicious!'; - warn(warning); - } - - var screen_constraints = { - video: { - mandatory: { - chromeMediaSource: 'screen' - } - } - }; - - var invocationInterval = 0, - stop; - (function selfInvoker() { - invocationInterval++; - if (!stop) setTimeout(selfInvoker, 10); - })(); - - navigator.webkitGetUserMedia(screen_constraints, onsuccess, onfailure); - - function onsuccess(stream) { - if (stream.stop) { - stream.stop(); - } - - if (callback) { - callback(true); - } - } - - function onfailure() { - stop = true; - if (callback) callback(invocationInterval > 5, warning); - } - }, - - RtpDataChannels: DetectRTC.isRtpDataChannelsSupported, - SctpDataChannels: DetectRTC.isSctpDataChannelsSupported - }; - - // www.RTCMultiConnection.org/docs/snapshots/ - connection.snapshots = {}; - - // www.RTCMultiConnection.org/docs/takeSnapshot/ - connection.takeSnapshot = function (userid, callback) { - for (var stream in connection.streams) { - stream = connection.streams[stream]; - if (stream.userid == userid && stream.stream && stream.stream.getVideoTracks && stream.stream.getVideoTracks().length) { - var video = stream.streamObject.mediaElement; - 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(); - callback && callback(connection.snapshots[userid]); - continue; - } - } - }; - - 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.WebRTC-Experiment.com/demos/MediaStreamTrack.getSources.html - connection._mediaSources = {}; - - // 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/devices/ - connection.devices = {}; - - // 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); - }; - - // 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 || {}; - this.attachStreams = []; - - // "drop" should detach all local streams - for (var stream in this.streams) { - if (this._skip.indexOf(stream) == -1) { - stream = this.streams[stream]; - if (stream.type == 'local') { - this.detachStreams.push(stream.streamid); - this.onstreamended(stream.streamObject); - } else this.onstreamended(stream.streamObject); - } - } - - // www.RTCMultiConnection.org/docs/sendCustomMessage/ - this.sendCustomMessage({ - drop: true, - dontRenegotiate: typeof config.renegotiate == 'undefined' ? true : config.renegotiate - }); - }; - - // 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 API key; if possible - connection.googKey = 'AIzaSyCUmCjvKRb-kOYrnoL2xaXb8I-_JJeKpf0'; - - // 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); - } - }; - - 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; - setTimeout(function () { - var root = connection.streams[streamid]; - connection.streams[streamid].sockets.forEach(function (socket) { - socket.send({ - userid: connection.userid, - streamid: root.streamid, - isVolumeChanged: true, - volume: mediaElement.volume - }); - }); - volumeChangeEventFired = false; - }, 2000); - } - }; - }; - - connection.localStreamids = []; - - // www.RTCMultiConnection.org/docs/onMediaFile/ - connection.onMediaFile = function (e) { - log('onMediaFile', e); - connection.body.appendChild(e.mediaElement); - }; - - // this object stores pre-recorded media streaming uids - // multiple pre-recorded media files can be streamed concurrently. - connection.preRecordedMedias = {}; - - // 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) { - for (var peer in connection.peers) { - connection.peers[peer].sharePartOfScreen(args); - } - - 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 (typeof element == 'string') { - 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()); - } - }); - }; - - // it is false because workaround that is used to capture connections' failures - // affects renegotiation scenarios! - // todo: fix it! - connection.autoReDialOnFailure = false; - - 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; - - // 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!'); - }; - - 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); - }; - - // this feature added to keep users privacy and - // make sure HTTPs pages NEVER auto capture users media - // isChrome && location.protocol == 'https:' - connection.preventSSLAutoAllowed = false; - - // 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://chat.firebaseIO.com/', - muted: 'https://cdn.webrtc-experiment.com/images/muted.png', - getRTCMultiConnectionStats: 'https://cdn.webrtc-experiment.com/getRTCMultiConnectionStats.js' - }; - - // as @serhanters proposed in #225 - // it will auto fix "all" renegotiation scenarios - connection.sdpConstraints.mandatory = { - OfferToReceiveAudio: true, - OfferToReceiveVideo: true - }; - - connection.onstatechange = function (state, reason) { - // fetching-usermedia - // usermedia-fetched - - // detecting-room-presence - // room-not-available - // room-available - - // connecting-with-initiator - // connected-with-initiator - - // failed---has reason - - // request-accepted - // request-rejected - - log('onstatechange:', state, reason ? ':- ' + reason : ''); - }; - - // auto leave on page unload - connection.leaveOnPageUnload = true; - - connection.processSdp = function (sdp) { - // process sdp here - return sdp; - }; - - connection.iceProtocols = { - tcp: true, - udp: true - }; - - connection.getExternalIceServers = true; - - connection.onfailed = function(event) { - if (isFirefox || event.targetuser.browser == 'firefox') { - warn('ICE connectivity check is failed. Re-establishing peer connection.'); - event.peer.redial(); - } else { - warn('ICE connectivity check is failed. Renegotiating peer connection.'); - event.peer.renegotiate(); - } - }; - - connection.onconnected = function(event) { - // event.peer.addStream || event.peer.getStats - log('Peer connection has been established between you and', event.userid); - }; - - connection.ondisconnected = function(event) { - log('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. - for (var stream in connection.streams) { - stream = connection.streams[stream]; - if (stream.userid == event.userid && stream.type == 'remote') { - connection.onstreamended(stream.streamObject); - } - } - - connection.remove(event.userid); - }, 5000); - }; - - connection.onstreamid = function(event) { - // event.isScreen || event.isVideo || event.isAudio - log('got remote streamid', event.streamid, 'from', event.userid); - }; - } -})(); diff --git a/RTCMultiConnection/Library/RTCMultiConnection-v2.0.js b/RTCMultiConnection/Library/RTCMultiConnection-v2.0.js deleted file mode 100644 index 96dcdb2d..00000000 --- a/RTCMultiConnection/Library/RTCMultiConnection-v2.0.js +++ /dev/null @@ -1,6014 +0,0 @@ -// Last time updated at Sep 27, 2014, 08:32:23 - -// 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-v2.0.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.0 - -/* issues/features need to be fixed & implemented: - --. v2.0.* changes-log here: http://www.rtcmulticonnection.org/changes-log/#v2.0 --. trello: https://trello.com/b/8bhi1G6n/rtcmulticonnection - --. Bug-Fixed: Now, MediaStream is removed from "attachStreams" array when stopped. --. Bug-Fixed: Now, OfferToReceiveAudio/OfferToReceiveVideo are "NOT" forced to be false even for data-only connections. Because it affects renegotiation scenarios. --. onMediaCaptured event. --. onStreamEndedHandler updated. --. connection.onopen is now having "event.channel" object. --. 2nd invocation of createDataChannel disabled. -*/ - -(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, ''); - - // 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.stats.sessions[connection.sessionDescription.sessionid]) { - connection.stats.numberOfSessions++; - connection.stats.sessions[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.stats.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 (!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.stats.sessions[session]) { - session = connection.stats.sessions[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 custom video device is selected - if (connection._mediaSources.video) { - constraints.video = { - optional: [{ - sourceId: connection._mediaSources.video - }] - }; - } - - // 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 = merge(screen_constraints.video.mandatory, { - 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 - 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) { - window.addEventListener('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(DetectRTC.screen.extensionid, 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) { - 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.audio || !!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 - }); - } - }, - 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); - } - } - - // 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.stats.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 - }); - }; - - // www.RTCMultiConnection.org/docs/addStream/ - connection.addStream = function(session, socket) { - // www.RTCMultiConnection.org/docs/renegotiation/ - - if (connection.stats.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.stats.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) { - 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(); - } - - 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); - } - } - } - 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.stats.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) { - 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 (!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 Firefox; 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 (!isPluginRTC && !stream.getVideoTracks().length) - mediaElement.addEventListener('play', function() { - setTimeout(function() { - mediaElement.muted = false; - afterRemoteStreamStartedFlowing({ - mediaElement: mediaElement, - session: session, - stream: stream, - preMuted: preMuted - }); - }, 3000); - }, false); - else waitUntilRemoteStreamStartsFlowing({ - mediaElement: mediaElement, - session: session, - stream: stream, - preMuted: preMuted - }); - - if (connection.setDefaultEventsForMediaElement) { - connection.setDefaultEventsForMediaElement(mediaElement, stream.streamid); - } - }, - - 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.stats.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.stats.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.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)) { - afterRemoteStreamStartedFlowing(args); - } else { - if (args.numberOfTimes >= 60) { // wait 60 seconds while video is delivered! - socket.send({ - failedToReceiveRemoteVideo: true, - streamid: args.stream.streamid - }); - } else - 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.stats.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 (isDeletedRTCMultiSession) 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; - - if (isChrome || (isFirefox && !peer.connection.getLocalStreams().length)) { - peer.addStream(stream); - } - - connection.renegotiatedSessions[JSON.stringify(_config.renegotiate)] = { - session: _config.renegotiate, - stream: stream - }; - - delete _config.renegotiate; - - createAnswer(); - }, _config.renegotiate); - } - - function createAnswer() { - // because Firefox has no support of renegotiation yet; - // so both chrome and firefox should redial instead of renegotiate! - if (isFirefox || _config.userinfo.browser == 'firefox') { - if (connection.peers[_config.userid]) { - connection.peers[_config.userid].redial(); - } - return; - } - - 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.stats.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.stats = { - numberOfConnectedUsers: 0, - numberOfSessions: 0, - sessions: {} - }; - - 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); - }; - - window.addEventListener('beforeunload', function() { - if (!connection.leaveOnPageUnload) return; - - clearSession(); - }, false); - - window.addEventListener('keyup', function(e) { - if (!connection.leaveOnPageUnload) return; - - if (e.keyCode == 116) { - clearSession(); - } - }, false); - - 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 - }); - } - } - } - - // 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 = connection.openSignalingChannel({ - onmessage: function(response) { - if (isDeletedRTCMultiSession) return; - - if (response.userid == connection.userid) return; - - if (response.sessionid && response.userid) { - if (!connection.stats.sessions[response.sessionid]) { - connection.stats.numberOfSessions++; - connection.stats.sessions[response.sessionid] = response; - } - } - - if (connection.isAcceptNewSession && response.sessionid && response.userid && !connection.sessionDescriptions[response.sessionid]) { - connection.sessionDescriptions[response.sessionid] = response; - - 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.stats.sessions[sessionDescription.sessionid]) { - connection.stats.numberOfSessions++; - connection.stats.sessions[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); - }; - } - } - }); - - rtcMultiSession.defaultSocket = defaultSocket; - - if (defaultSocket && onSignalingReady) setTimeout(onSignalingReady, 2000); - - if (connection.session.screen) { - loadScreenFrame(); - } - - connection.getExternalIceServers && loadIceFrame(function(iceServers) { - connection.iceServers = connection.iceServers.concat(iceServers); - }); - - 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) { - peer.attachStreams = [e.stream]; - } - - // detaching old streams - detachMediaStream(connection.detachStreams, peer.connection); - - if (e.stream && (session.audio || session.video || session.screen)) { - // removeStream is not yet implemented in Firefox - // if(isFirefox) peer.connection.removeStream(e.stream); - - if (isChrome || (isFirefox && !peer.connection.getLocalStreams().length)) { - peer.addStream(e.stream); - } - } - - // because Firefox has no support of renegotiation yet; - // so both chrome and firefox should redial instead of renegotiate! - if (isFirefox || _peer.userinfo.browser == 'firefox') { - return _peer.redial(); - } - - 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 isDeletedRTCMultiSession = 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; - isDeletedRTCMultiSession = 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(); - - // it will not work, though :) - delete this; - - log('Disconnected your sockets, peers, streams and everything except RTCMultiConnection object.'); - }; - } - - var RTCPeerConnection = window.mozRTCPeerConnection || window.webkitRTCPeerConnection; - var RTCSessionDescription = window.mozRTCSessionDescription || window.RTCSessionDescription; - var RTCIceCandidate = window.mozRTCIceCandidate || window.RTCIceCandidate; - - 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 = self.serializeSdp(sessionDescription.sdp, createType); - 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); - } - }, - serializeSdp: function(sdp, createType) { - // it is "connection.processSdp=function(sdp){return sdp;}" - sdp = this.processSdp(sdp); - - if (isFirefox) return sdp; - - if (this.renegotiate) { - // sdp = sdp.replace(/a=rtpmap:.* rtx.*\r\n/gi, ''); - // sdp = sdp.replace(/a=fmtp:.* apt=.*\r\n/gi, ''); - // sdp = sdp.replace(/a=rtcp-fb.*\r\n/gi, ''); - // sdp = sdp.replace(/a=candidate:.*\r\n/gi, ''); - } - - if (this.session.inactive && !this.holdMLine) { - this.hold = true; - if ((this.session.screen || this.session.video) && this.session.audio) { - this.holdMLine = 'both'; - } else if (this.session.screen || this.session.video) { - this.holdMLine = 'video'; - } else if (this.session.audio) { - this.holdMLine = 'audio'; - } - } - - sdp = this.setBandwidth(sdp); - if (this.holdMLine == 'both') { - if (this.hold) { - this.prevSDP = sdp; - sdp = sdp.replace(/a=sendonly|a=recvonly|a=sendrecv/g, 'a=inactive'); - } else if (this.prevSDP) { - if (!this.session.inactive) { - // it means that DTSL key exchange already happened for single or multiple media lines. - // this block checks, key-exchange must be happened for all media lines. - sdp = this.prevSDP; - - // todo: test it: makes sense? - if (chromeVersion <= 35) { - return sdp; - } - } - } - } else if (this.holdMLine == 'audio' || this.holdMLine == 'video') { - sdp = sdp.split('m='); - - var audio = ''; - var video = ''; - - if (sdp[1] && sdp[1].indexOf('audio') == 0) { - audio = 'm=' + sdp[1]; - } - if (sdp[2] && sdp[2].indexOf('audio') == 0) { - audio = 'm=' + sdp[2]; - } - - if (sdp[1] && sdp[1].indexOf('video') == 0) { - video = 'm=' + sdp[1]; - } - if (sdp[2] && sdp[2].indexOf('video') == 0) { - video = 'm=' + sdp[2]; - } - - if (this.holdMLine == 'audio') { - if (this.hold) { - this.prevSDP = sdp[0] + audio + video; - sdp = sdp[0] + audio.replace(/a=sendonly|a=recvonly|a=sendrecv/g, 'a=inactive') + video; - } else if (this.prevSDP) { - sdp = this.prevSDP; - } - } - - if (this.holdMLine == 'video') { - if (this.hold) { - this.prevSDP = sdp[0] + audio + video; - sdp = sdp[0] + audio + video.replace(/a=sendonly|a=recvonly|a=sendrecv/g, 'a=inactive'); - } else if (this.prevSDP) { - sdp = this.prevSDP; - } - } - } - - if (!this.hold && this.session.inactive) { - // transport.cc&l=852 - http://goo.gl/0FxxqG - // dtlstransport.h&l=234 - http://goo.gl/7E4sYF - // http://tools.ietf.org/html/rfc4340 - - // From RFC 4145, SDP setup attribute values. - // http://goo.gl/xETJEp && http://goo.gl/3Wgcau - if (createType == 'offer') { - sdp = sdp.replace(/a=setup:passive|a=setup:active|a=setup:holdconn/g, 'a=setup:actpass'); - } else { - sdp = sdp.replace(/a=setup:actpass|a=setup:passive|a=setup:holdconn/g, 'a=setup:active'); - } - - // whilst doing handshake, either media lines were "inactive" - // or no media lines were present - sdp = sdp.replace(/a=inactive/g, 'a=sendrecv'); - } - // this.session.inactive = false; - return sdp; - }, - 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() { - self.connection && 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; - }, - setBandwidth: function(sdp) { - if (isMobileDevice || isFirefox || !this.bandwidth) return sdp; - - var bandwidth = this.bandwidth; - - if (this.session.screen) { - if (!bandwidth.screen) { - warn('It seems that you are not using bandwidth for screen. Screen sharing is expected to fail.'); - } else if (bandwidth.screen < 300) { - 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 && this.session.screen) { - 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 || bandwidth.data) { - sdp = sdp.replace(/b=AS([^\r\n]+\r\n)/g, ''); - } - - if (bandwidth.audio) { - sdp = sdp.replace(/a=mid:audio\r\n/g, 'a=mid:audio\r\nb=AS:' + bandwidth.audio + '\r\n'); - } - - if (bandwidth.video) { - sdp = sdp.replace(/a=mid:video\r\n/g, 'a=mid:video\r\nb=AS:' + (this.session.screen ? bandwidth.screen : bandwidth.video) + '\r\n'); - } - - if (bandwidth.data && !this.preferSCTP) { - sdp = sdp.replace(/a=mid:data\r\n/g, 'a=mid:data\r\nb=AS:' + bandwidth.data + '\r\n'); - } - - return sdp; - }, - setConstraints: function() { - this.constraints = { - optional: this.sdpConstraints.optional || isChrome ? [{ - VoiceActivityDetection: false - }] : [], - mandatory: this.sdpConstraints.mandatory || { - OfferToReceiveAudio: !!this.session.audio, - OfferToReceiveVideo: !!this.session.video || !!this.session.screen - } - }; - - if (this.constraints.mandatory) { - log('sdp-mandatory-constraints', toStr(this.constraints.mandatory)); - } - - if (this.constraints.optional) { - log('sdp-optional-constraints', toStr(this.constraints.optional)); - } - - this.optionalArgument = { - optional: this.optionalArgument.optional || [{ - DtlsSrtpKeyAgreement: true - }, { - googImprovedWifiBwe: true - }], - mandatory: this.optionalArgument.mandatory || {} - }; - - if (!this.preferSCTP) { - this.optionalArgument.optional.push({ - RtpDataChannels: true - }); - } - - log('optional-argument', toStr(this.optionalArgument.optional)); - - 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, - iceTransports: this.rtcConfiguration.iceTransports - }; - } 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; - - 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) { - // if(isFirefox) this.create(this.type, this); - - log('recreating offer'); - - this.type = 'offer'; - this.renegotiate = true; - 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.renegotiate = true; - 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 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.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); - }); - } - } - - var FileSaver = { - SaveToDisk: function(fileUrl, fileName) { - var hyperlink = document.createElement('a'); - hyperlink.href = fileUrl; - hyperlink.target = '_blank'; - hyperlink.download = fileName || fileUrl; - - var mouseEvent = new MouseEvent('click', { - view: window, - bubbles: true, - cancelable: true - }); - - hyperlink.dispatchEvent(mouseEvent); - - // (window.URL || window.webkitURL).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 - }; - } - - var webAudioMediaStreamSources = []; - - function convertToAudioStream(mediaStream) { - if (!mediaStream) throw 'MediaStream is mandatory.'; - - 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; - if (isChrome && navigator.userAgent.match(/Chrom(e|ium)\/([0-9]+)\./)[2]) { - chromeVersion = parseInt(navigator.userAgent.match(/Chrom(e|ium)\/([0-9]+)\./)[2], 10); - } - - var firefoxVersion = 50; - if (isFirefox && window.navigator.userAgent.match(/Firefox\/(.*)/)[1]) { - firefoxVersion = parseInt(window.navigator.userAgent.match(/Firefox\/(.*)/)[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.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 - }, '*'); - } - }; - } - - var iceFrame, loadedIceFrame; - - function loadIceFrame(callback, skip) { - if (loadedIceFrame) return; - if (!skip) return loadIceFrame(callback, true); - - loadedIceFrame = true; - - var iframe = document.createElement('iframe'); - iframe.onload = function() { - iframe.isLoaded = true; - - window.addEventListener('message', iFrameLoaderCallback); - - function iFrameLoaderCallback(event) { - if (!event.data || !event.data.iceServers) return; - callback(event.data.iceServers); - - // this event listener is no more needed - window.removeEventListener('message', iFrameLoaderCallback); - } - - iframe.contentWindow.postMessage('get-ice-servers', '*'); - }; - iframe.src = 'https://cdn.webrtc-experiment.com/getIceServers/'; - iframe.style.display = 'none'; - (document.body || document.documentElement).appendChild(iframe); - } - - 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; - - var MediaStreamTrack = window.MediaStreamTrack; - - // http://goo.gl/6ScAV9 - var DetectRTC = {}; - - (function() { - - DetectRTC.hasMicrophone = false; - DetectRTC.hasSpeakers = false; - DetectRTC.hasWebcam = false; - - DetectRTC.MediaDevices = []; - - // http://goo.gl/UVQRKk - // todo: switch to enumerateDevices when landed in canary. - function CheckDeviceSupport(callback) { - // This method is useful only for Chrome! - - // Firefox seems having no support of enumerateDevices feature yet. - if (isPluginRTC || isFirefox) { - callback && callback(); - return; - } - - // if "getSources" is available; prefer it over "getMediaDevices" - if (MediaStreamTrack && MediaStreamTrack.getSources) { - navigator.getMediaDevices = MediaStreamTrack.getSources.bind(MediaStreamTrack); - } - - // if still no "getMediaDevices"; it MUST be Firefox! - if (!navigator.getMediaDevices) { - log('navigator.getMediaDevices is undefined.'); - // assuming that it is older chrome or chromium implementation - if (isChrome) { - DetectRTC.hasMicrophone = true; - DetectRTC.hasSpeakers = true; - DetectRTC.hasWebcam = true; - } - - callback && callback(); - return; - } - - navigator.getMediaDevices(function(devices) { - devices.forEach(function(device) { - // 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; - } - - DetectRTC.MediaDevices.push(device); - - if (device.kind == 'audioinput' || device.kind == 'audio') { - DetectRTC.hasMicrophone = true; - } - - if (device.kind == 'audiooutput') { - DetectRTC.hasSpeakers = true; - } - - if (device.kind == 'videoinput' || device.kind == 'video') { - DetectRTC.hasWebcam = true; - } - - // there is no "videoouput" in the spec. - }); - - if (callback) callback(); - }); - } - - DetectRTC.isWebRTCSupported = !!window.webkitRTCPeerConnection || !!window.mozRTCPeerConnection; - DetectRTC.isAudioContextSupported = (!!window.AudioContext || !!window.webkitAudioContext) && !!AudioContext.prototype.createMediaStreamSource; - DetectRTC.isScreenCapturingSupported = isChrome && chromeVersion >= 26 && (isNodeWebkit ? true : location.protocol == 'https:'); - DetectRTC.isSctpDataChannelsSupported = !!navigator.mozGetUserMedia || (isChrome && chromeVersion >= 25); - DetectRTC.isRtpDataChannelsSupported = isChrome && chromeVersion >= 31; - - // check for microphone/camera support! - CheckDeviceSupport(); - DetectRTC.load = CheckDeviceSupport; - - var screenCallback; - - DetectRTC.screen = { - chromeMediaSource: 'screen', - extensionid: ReservedExtensionID, - getSourceId: function(callback) { - if (!callback) throw '"callback" parameter is mandatory.'; - screenCallback = callback; - window.postMessage('get-sourceId', '*'); - - // sometimes content-script mismatched URLs - // causes infinite delay. - setTimeout(function() { - if (!DetectRTC.screen.sourceId && DetectRTC.screen.chromeMediaSource == 'screen') { - callback('No-Response'); - } - }, 2000); - }, - isChromeExtensionAvailable: function(callback) { - if (!callback) return; - - if (DetectRTC.screen.chromeMediaSource == 'desktop') return callback(true); - - // ask extension if it is available - window.postMessage('are-you-there', '*'); - - setTimeout(function() { - if (DetectRTC.screen.chromeMediaSource == 'screen') { - callback(false); - } else callback(true); - }, 2000); - }, - 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) { - 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.extensionid == extensionid ? 'installed-enabled' : 'installed-disabled'); - } else callback('installed-enabled'); - }, 2000); - }; - image.onerror = function() { - callback('not-installed'); - }; - } - }; - - // check if desktop-capture extension installed. - if (window.postMessage && isChrome) { - DetectRTC.screen.isChromeExtensionAvailable(); - } - })(); - - // if IE - if (!window.addEventListener) { - window.addEventListener = function(el, eventName, eventHandler) { - if (!el.attachEvent) return; - el.attachEvent('on' + eventName, eventHandler); - }; - } - - window.addEventListener('message', function(event) { - if (event.origin != window.location.origin) { - return; - } - - DetectRTC.screen.onMessageCallback(event.data); - }); - - 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'); - } - - 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 = {}; - 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://chat.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'; - - // www.RTCMultiConnection.org/docs/stats/ - connection.stats = { - numberOfConnectedUsers: 0, - numberOfSessions: 0, - sessions: {} - }; - - // 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]; - } - }; - - var iceServers = []; - - iceServers.push({ - url: 'stun:stun.l.google.com:19302' - }); - - iceServers.push({ - url: 'stun:stun.anyfirewall.com:3478' - }); - - iceServers.push({ - url: 'turn:turn.bistri.com:80', - credential: 'homeo', - username: 'homeo' - }); - - iceServers.push({ - url: 'turn:turn.anyfirewall.com:443?transport=tcp', - credential: 'webrtc', - username: 'webrtc' - }); - - connection.iceServers = iceServers; - - connection.rtcConfiguration = { - iceServers: connection.iceServers, - 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 (session.video) { - self.videoRecorder = RecordRTC(self.stream, { - type: 'video' - }); - } else if (session.audio) { - self.audioRecorder = RecordRTC(self.stream, { - type: 'audio' - }); - } - } else if (isChrome) { - // 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/getStats/ - connection.getStats = function(callback) { - var numberOfConnectedUsers = 0; - for (var peer in connection.peers) { - numberOfConnectedUsers++; - } - - connection.stats.numberOfConnectedUsers = numberOfConnectedUsers; - - if (callback) callback(connection.stats); - }; - - // 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++; - - if (isFirefox || event.targetuser.browser == 'firefox') { - error('ICE connectivity check is failed. Re-establishing peer connection.'); - event.peer.numOfRetries < 2 && event.peer.redial(); - } else { - 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) { - mediaStream.onended(); - return; - } - - // remove stream from "localStreams" object - // when native-stop method invoked. - if (connection.localStreams[mediaStream.streamid]) { - delete connection.localStreams[mediaStream.streamid]; - } - - if (!mediaStream.getAudioTracks) { - if (mediaStream.stop) { - mediaStream.stop(); - } - return; - } - - if (mediaStream.getAudioTracks().length) { - mediaStream.getAudioTracks().forEach(function(track) { - track.stop(); - }); - } - - if (mediaStream.getVideoTracks().length) { - mediaStream.getVideoTracks().forEach(function(track) { - track.stop(); - }); - } - - if (isFirefox) { - // Firefox don't yet support onended for any stream (remote/local) - if (mediaStream.onended) mediaStream.onended(); - } - }; - - 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; - - // if (channel != connection.channel || (connection.isInitiator && channel == connection.channel)) - firebase.onDisconnect().remove(); - - setTimeout(function() { - config.callback(firebase); - }, 1); - }; - - connection.Plugin = Plugin; - } -})(); diff --git a/RTCMultiConnection/Library/RTCMultiConnection-v2.1.js b/RTCMultiConnection/Library/RTCMultiConnection-v2.1.js deleted file mode 100644 index ac654ca6..00000000 --- a/RTCMultiConnection/Library/RTCMultiConnection-v2.1.js +++ /dev/null @@ -1,6014 +0,0 @@ -// Last time updated at Sep 27, 2014, 08:32:23 - -// 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-v2.1.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.1 - -/* issues/features need to be fixed & implemented: - --. v2.0.* changes-log here: http://www.rtcmulticonnection.org/changes-log/#v2.0 --. trello: https://trello.com/b/8bhi1G6n/rtcmulticonnection - --. Bug-Fixed: Now, MediaStream is removed from "attachStreams" array when stopped. --. Bug-Fixed: Now, OfferToReceiveAudio/OfferToReceiveVideo are "NOT" forced to be false even for data-only connections. Because it affects renegotiation scenarios. --. onMediaCaptured event. --. onStreamEndedHandler updated. --. connection.onopen is now having "event.channel" object. --. 2nd invocation of createDataChannel disabled. -*/ - -(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, ''); - - // 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.stats.sessions[connection.sessionDescription.sessionid]) { - connection.stats.numberOfSessions++; - connection.stats.sessions[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.stats.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 (!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.stats.sessions[session]) { - session = connection.stats.sessions[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 custom video device is selected - if (connection._mediaSources.video) { - constraints.video = { - optional: [{ - sourceId: connection._mediaSources.video - }] - }; - } - - // 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 = merge(screen_constraints.video.mandatory, { - 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 - 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) { - window.addEventListener('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(DetectRTC.screen.extensionid, 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) { - 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.audio || !!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 - }); - } - }, - 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); - } - } - - // 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.stats.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 - }); - }; - - // www.RTCMultiConnection.org/docs/addStream/ - connection.addStream = function(session, socket) { - // www.RTCMultiConnection.org/docs/renegotiation/ - - if (connection.stats.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.stats.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) { - 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(); - } - - 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); - } - } - } - 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.stats.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) { - 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 (!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 Firefox; 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 (!isPluginRTC && !stream.getVideoTracks().length) - mediaElement.addEventListener('play', function() { - setTimeout(function() { - mediaElement.muted = false; - afterRemoteStreamStartedFlowing({ - mediaElement: mediaElement, - session: session, - stream: stream, - preMuted: preMuted - }); - }, 3000); - }, false); - else waitUntilRemoteStreamStartsFlowing({ - mediaElement: mediaElement, - session: session, - stream: stream, - preMuted: preMuted - }); - - if (connection.setDefaultEventsForMediaElement) { - connection.setDefaultEventsForMediaElement(mediaElement, stream.streamid); - } - }, - - 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.stats.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.stats.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.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)) { - afterRemoteStreamStartedFlowing(args); - } else { - if (args.numberOfTimes >= 60) { // wait 60 seconds while video is delivered! - socket.send({ - failedToReceiveRemoteVideo: true, - streamid: args.stream.streamid - }); - } else - 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.stats.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 (isDeletedRTCMultiSession) 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; - - if (isChrome || (isFirefox && !peer.connection.getLocalStreams().length)) { - peer.addStream(stream); - } - - connection.renegotiatedSessions[JSON.stringify(_config.renegotiate)] = { - session: _config.renegotiate, - stream: stream - }; - - delete _config.renegotiate; - - createAnswer(); - }, _config.renegotiate); - } - - function createAnswer() { - // because Firefox has no support of renegotiation yet; - // so both chrome and firefox should redial instead of renegotiate! - if (isFirefox || _config.userinfo.browser == 'firefox') { - if (connection.peers[_config.userid]) { - connection.peers[_config.userid].redial(); - } - return; - } - - 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.stats.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.stats = { - numberOfConnectedUsers: 0, - numberOfSessions: 0, - sessions: {} - }; - - 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); - }; - - window.addEventListener('beforeunload', function() { - if (!connection.leaveOnPageUnload) return; - - clearSession(); - }, false); - - window.addEventListener('keyup', function(e) { - if (!connection.leaveOnPageUnload) return; - - if (e.keyCode == 116) { - clearSession(); - } - }, false); - - 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 - }); - } - } - } - - // 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 = connection.openSignalingChannel({ - onmessage: function(response) { - if (isDeletedRTCMultiSession) return; - - if (response.userid == connection.userid) return; - - if (response.sessionid && response.userid) { - if (!connection.stats.sessions[response.sessionid]) { - connection.stats.numberOfSessions++; - connection.stats.sessions[response.sessionid] = response; - } - } - - if (connection.isAcceptNewSession && response.sessionid && response.userid && !connection.sessionDescriptions[response.sessionid]) { - connection.sessionDescriptions[response.sessionid] = response; - - 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.stats.sessions[sessionDescription.sessionid]) { - connection.stats.numberOfSessions++; - connection.stats.sessions[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); - }; - } - } - }); - - rtcMultiSession.defaultSocket = defaultSocket; - - if (defaultSocket && onSignalingReady) setTimeout(onSignalingReady, 2000); - - if (connection.session.screen) { - loadScreenFrame(); - } - - connection.getExternalIceServers && loadIceFrame(function(iceServers) { - connection.iceServers = connection.iceServers.concat(iceServers); - }); - - 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) { - peer.attachStreams = [e.stream]; - } - - // detaching old streams - detachMediaStream(connection.detachStreams, peer.connection); - - if (e.stream && (session.audio || session.video || session.screen)) { - // removeStream is not yet implemented in Firefox - // if(isFirefox) peer.connection.removeStream(e.stream); - - if (isChrome || (isFirefox && !peer.connection.getLocalStreams().length)) { - peer.addStream(e.stream); - } - } - - // because Firefox has no support of renegotiation yet; - // so both chrome and firefox should redial instead of renegotiate! - if (isFirefox || _peer.userinfo.browser == 'firefox') { - return _peer.redial(); - } - - 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 isDeletedRTCMultiSession = 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; - isDeletedRTCMultiSession = 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(); - - // it will not work, though :) - delete this; - - log('Disconnected your sockets, peers, streams and everything except RTCMultiConnection object.'); - }; - } - - var RTCPeerConnection = window.mozRTCPeerConnection || window.webkitRTCPeerConnection; - var RTCSessionDescription = window.mozRTCSessionDescription || window.RTCSessionDescription; - var RTCIceCandidate = window.mozRTCIceCandidate || window.RTCIceCandidate; - - 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 = self.serializeSdp(sessionDescription.sdp, createType); - 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); - } - }, - serializeSdp: function(sdp, createType) { - // it is "connection.processSdp=function(sdp){return sdp;}" - sdp = this.processSdp(sdp); - - if (isFirefox) return sdp; - - if (this.renegotiate) { - // sdp = sdp.replace(/a=rtpmap:.* rtx.*\r\n/gi, ''); - // sdp = sdp.replace(/a=fmtp:.* apt=.*\r\n/gi, ''); - // sdp = sdp.replace(/a=rtcp-fb.*\r\n/gi, ''); - // sdp = sdp.replace(/a=candidate:.*\r\n/gi, ''); - } - - if (this.session.inactive && !this.holdMLine) { - this.hold = true; - if ((this.session.screen || this.session.video) && this.session.audio) { - this.holdMLine = 'both'; - } else if (this.session.screen || this.session.video) { - this.holdMLine = 'video'; - } else if (this.session.audio) { - this.holdMLine = 'audio'; - } - } - - sdp = this.setBandwidth(sdp); - if (this.holdMLine == 'both') { - if (this.hold) { - this.prevSDP = sdp; - sdp = sdp.replace(/a=sendonly|a=recvonly|a=sendrecv/g, 'a=inactive'); - } else if (this.prevSDP) { - if (!this.session.inactive) { - // it means that DTSL key exchange already happened for single or multiple media lines. - // this block checks, key-exchange must be happened for all media lines. - sdp = this.prevSDP; - - // todo: test it: makes sense? - if (chromeVersion <= 35) { - return sdp; - } - } - } - } else if (this.holdMLine == 'audio' || this.holdMLine == 'video') { - sdp = sdp.split('m='); - - var audio = ''; - var video = ''; - - if (sdp[1] && sdp[1].indexOf('audio') == 0) { - audio = 'm=' + sdp[1]; - } - if (sdp[2] && sdp[2].indexOf('audio') == 0) { - audio = 'm=' + sdp[2]; - } - - if (sdp[1] && sdp[1].indexOf('video') == 0) { - video = 'm=' + sdp[1]; - } - if (sdp[2] && sdp[2].indexOf('video') == 0) { - video = 'm=' + sdp[2]; - } - - if (this.holdMLine == 'audio') { - if (this.hold) { - this.prevSDP = sdp[0] + audio + video; - sdp = sdp[0] + audio.replace(/a=sendonly|a=recvonly|a=sendrecv/g, 'a=inactive') + video; - } else if (this.prevSDP) { - sdp = this.prevSDP; - } - } - - if (this.holdMLine == 'video') { - if (this.hold) { - this.prevSDP = sdp[0] + audio + video; - sdp = sdp[0] + audio + video.replace(/a=sendonly|a=recvonly|a=sendrecv/g, 'a=inactive'); - } else if (this.prevSDP) { - sdp = this.prevSDP; - } - } - } - - if (!this.hold && this.session.inactive) { - // transport.cc&l=852 - http://goo.gl/0FxxqG - // dtlstransport.h&l=234 - http://goo.gl/7E4sYF - // http://tools.ietf.org/html/rfc4340 - - // From RFC 4145, SDP setup attribute values. - // http://goo.gl/xETJEp && http://goo.gl/3Wgcau - if (createType == 'offer') { - sdp = sdp.replace(/a=setup:passive|a=setup:active|a=setup:holdconn/g, 'a=setup:actpass'); - } else { - sdp = sdp.replace(/a=setup:actpass|a=setup:passive|a=setup:holdconn/g, 'a=setup:active'); - } - - // whilst doing handshake, either media lines were "inactive" - // or no media lines were present - sdp = sdp.replace(/a=inactive/g, 'a=sendrecv'); - } - // this.session.inactive = false; - return sdp; - }, - 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() { - self.connection && 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; - }, - setBandwidth: function(sdp) { - if (isMobileDevice || isFirefox || !this.bandwidth) return sdp; - - var bandwidth = this.bandwidth; - - if (this.session.screen) { - if (!bandwidth.screen) { - warn('It seems that you are not using bandwidth for screen. Screen sharing is expected to fail.'); - } else if (bandwidth.screen < 300) { - 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 && this.session.screen) { - 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 || bandwidth.data) { - sdp = sdp.replace(/b=AS([^\r\n]+\r\n)/g, ''); - } - - if (bandwidth.audio) { - sdp = sdp.replace(/a=mid:audio\r\n/g, 'a=mid:audio\r\nb=AS:' + bandwidth.audio + '\r\n'); - } - - if (bandwidth.video) { - sdp = sdp.replace(/a=mid:video\r\n/g, 'a=mid:video\r\nb=AS:' + (this.session.screen ? bandwidth.screen : bandwidth.video) + '\r\n'); - } - - if (bandwidth.data && !this.preferSCTP) { - sdp = sdp.replace(/a=mid:data\r\n/g, 'a=mid:data\r\nb=AS:' + bandwidth.data + '\r\n'); - } - - return sdp; - }, - setConstraints: function() { - this.constraints = { - optional: this.sdpConstraints.optional || isChrome ? [{ - VoiceActivityDetection: false - }] : [], - mandatory: this.sdpConstraints.mandatory || { - OfferToReceiveAudio: !!this.session.audio, - OfferToReceiveVideo: !!this.session.video || !!this.session.screen - } - }; - - if (this.constraints.mandatory) { - log('sdp-mandatory-constraints', toStr(this.constraints.mandatory)); - } - - if (this.constraints.optional) { - log('sdp-optional-constraints', toStr(this.constraints.optional)); - } - - this.optionalArgument = { - optional: this.optionalArgument.optional || [{ - DtlsSrtpKeyAgreement: true - }, { - googImprovedWifiBwe: true - }], - mandatory: this.optionalArgument.mandatory || {} - }; - - if (!this.preferSCTP) { - this.optionalArgument.optional.push({ - RtpDataChannels: true - }); - } - - log('optional-argument', toStr(this.optionalArgument.optional)); - - 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, - iceTransports: this.rtcConfiguration.iceTransports - }; - } 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; - - 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) { - // if(isFirefox) this.create(this.type, this); - - log('recreating offer'); - - this.type = 'offer'; - this.renegotiate = true; - 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.renegotiate = true; - 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 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.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); - }); - } - } - - var FileSaver = { - SaveToDisk: function(fileUrl, fileName) { - var hyperlink = document.createElement('a'); - hyperlink.href = fileUrl; - hyperlink.target = '_blank'; - hyperlink.download = fileName || fileUrl; - - var mouseEvent = new MouseEvent('click', { - view: window, - bubbles: true, - cancelable: true - }); - - hyperlink.dispatchEvent(mouseEvent); - - // (window.URL || window.webkitURL).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 - }; - } - - var webAudioMediaStreamSources = []; - - function convertToAudioStream(mediaStream) { - if (!mediaStream) throw 'MediaStream is mandatory.'; - - 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; - if (isChrome && navigator.userAgent.match(/Chrom(e|ium)\/([0-9]+)\./)[2]) { - chromeVersion = parseInt(navigator.userAgent.match(/Chrom(e|ium)\/([0-9]+)\./)[2], 10); - } - - var firefoxVersion = 50; - if (isFirefox && window.navigator.userAgent.match(/Firefox\/(.*)/)[1]) { - firefoxVersion = parseInt(window.navigator.userAgent.match(/Firefox\/(.*)/)[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.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 - }, '*'); - } - }; - } - - var iceFrame, loadedIceFrame; - - function loadIceFrame(callback, skip) { - if (loadedIceFrame) return; - if (!skip) return loadIceFrame(callback, true); - - loadedIceFrame = true; - - var iframe = document.createElement('iframe'); - iframe.onload = function() { - iframe.isLoaded = true; - - window.addEventListener('message', iFrameLoaderCallback); - - function iFrameLoaderCallback(event) { - if (!event.data || !event.data.iceServers) return; - callback(event.data.iceServers); - - // this event listener is no more needed - window.removeEventListener('message', iFrameLoaderCallback); - } - - iframe.contentWindow.postMessage('get-ice-servers', '*'); - }; - iframe.src = 'https://cdn.webrtc-experiment.com/getIceServers/'; - iframe.style.display = 'none'; - (document.body || document.documentElement).appendChild(iframe); - } - - 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; - - var MediaStreamTrack = window.MediaStreamTrack; - - // http://goo.gl/6ScAV9 - var DetectRTC = {}; - - (function() { - - DetectRTC.hasMicrophone = false; - DetectRTC.hasSpeakers = false; - DetectRTC.hasWebcam = false; - - DetectRTC.MediaDevices = []; - - // http://goo.gl/UVQRKk - // todo: switch to enumerateDevices when landed in canary. - function CheckDeviceSupport(callback) { - // This method is useful only for Chrome! - - // Firefox seems having no support of enumerateDevices feature yet. - if (isPluginRTC || isFirefox) { - callback && callback(); - return; - } - - // if "getSources" is available; prefer it over "getMediaDevices" - if (MediaStreamTrack && MediaStreamTrack.getSources) { - navigator.getMediaDevices = MediaStreamTrack.getSources.bind(MediaStreamTrack); - } - - // if still no "getMediaDevices"; it MUST be Firefox! - if (!navigator.getMediaDevices) { - log('navigator.getMediaDevices is undefined.'); - // assuming that it is older chrome or chromium implementation - if (isChrome) { - DetectRTC.hasMicrophone = true; - DetectRTC.hasSpeakers = true; - DetectRTC.hasWebcam = true; - } - - callback && callback(); - return; - } - - navigator.getMediaDevices(function(devices) { - devices.forEach(function(device) { - // 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; - } - - DetectRTC.MediaDevices.push(device); - - if (device.kind == 'audioinput' || device.kind == 'audio') { - DetectRTC.hasMicrophone = true; - } - - if (device.kind == 'audiooutput') { - DetectRTC.hasSpeakers = true; - } - - if (device.kind == 'videoinput' || device.kind == 'video') { - DetectRTC.hasWebcam = true; - } - - // there is no "videoouput" in the spec. - }); - - if (callback) callback(); - }); - } - - DetectRTC.isWebRTCSupported = !!window.webkitRTCPeerConnection || !!window.mozRTCPeerConnection; - DetectRTC.isAudioContextSupported = (!!window.AudioContext || !!window.webkitAudioContext) && !!AudioContext.prototype.createMediaStreamSource; - DetectRTC.isScreenCapturingSupported = isChrome && chromeVersion >= 26 && (isNodeWebkit ? true : location.protocol == 'https:'); - DetectRTC.isSctpDataChannelsSupported = !!navigator.mozGetUserMedia || (isChrome && chromeVersion >= 25); - DetectRTC.isRtpDataChannelsSupported = isChrome && chromeVersion >= 31; - - // check for microphone/camera support! - CheckDeviceSupport(); - DetectRTC.load = CheckDeviceSupport; - - var screenCallback; - - DetectRTC.screen = { - chromeMediaSource: 'screen', - extensionid: ReservedExtensionID, - getSourceId: function(callback) { - if (!callback) throw '"callback" parameter is mandatory.'; - screenCallback = callback; - window.postMessage('get-sourceId', '*'); - - // sometimes content-script mismatched URLs - // causes infinite delay. - setTimeout(function() { - if (!DetectRTC.screen.sourceId && DetectRTC.screen.chromeMediaSource == 'screen') { - callback('No-Response'); - } - }, 2000); - }, - isChromeExtensionAvailable: function(callback) { - if (!callback) return; - - if (DetectRTC.screen.chromeMediaSource == 'desktop') return callback(true); - - // ask extension if it is available - window.postMessage('are-you-there', '*'); - - setTimeout(function() { - if (DetectRTC.screen.chromeMediaSource == 'screen') { - callback(false); - } else callback(true); - }, 2000); - }, - 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) { - 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.extensionid == extensionid ? 'installed-enabled' : 'installed-disabled'); - } else callback('installed-enabled'); - }, 2000); - }; - image.onerror = function() { - callback('not-installed'); - }; - } - }; - - // check if desktop-capture extension installed. - if (window.postMessage && isChrome) { - DetectRTC.screen.isChromeExtensionAvailable(); - } - })(); - - // if IE - if (!window.addEventListener) { - window.addEventListener = function(el, eventName, eventHandler) { - if (!el.attachEvent) return; - el.attachEvent('on' + eventName, eventHandler); - }; - } - - window.addEventListener('message', function(event) { - if (event.origin != window.location.origin) { - return; - } - - DetectRTC.screen.onMessageCallback(event.data); - }); - - 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'); - } - - 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 = {}; - 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://chat.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'; - - // www.RTCMultiConnection.org/docs/stats/ - connection.stats = { - numberOfConnectedUsers: 0, - numberOfSessions: 0, - sessions: {} - }; - - // 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]; - } - }; - - var iceServers = []; - - iceServers.push({ - url: 'stun:stun.l.google.com:19302' - }); - - iceServers.push({ - url: 'stun:stun.anyfirewall.com:3478' - }); - - iceServers.push({ - url: 'turn:turn.bistri.com:80', - credential: 'homeo', - username: 'homeo' - }); - - iceServers.push({ - url: 'turn:turn.anyfirewall.com:443?transport=tcp', - credential: 'webrtc', - username: 'webrtc' - }); - - connection.iceServers = iceServers; - - connection.rtcConfiguration = { - iceServers: connection.iceServers, - 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 (session.video) { - self.videoRecorder = RecordRTC(self.stream, { - type: 'video' - }); - } else if (session.audio) { - self.audioRecorder = RecordRTC(self.stream, { - type: 'audio' - }); - } - } else if (isChrome) { - // 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/getStats/ - connection.getStats = function(callback) { - var numberOfConnectedUsers = 0; - for (var peer in connection.peers) { - numberOfConnectedUsers++; - } - - connection.stats.numberOfConnectedUsers = numberOfConnectedUsers; - - if (callback) callback(connection.stats); - }; - - // 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++; - - if (isFirefox || event.targetuser.browser == 'firefox') { - error('ICE connectivity check is failed. Re-establishing peer connection.'); - event.peer.numOfRetries < 2 && event.peer.redial(); - } else { - 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) { - mediaStream.onended(); - return; - } - - // remove stream from "localStreams" object - // when native-stop method invoked. - if (connection.localStreams[mediaStream.streamid]) { - delete connection.localStreams[mediaStream.streamid]; - } - - if (!mediaStream.getAudioTracks) { - if (mediaStream.stop) { - mediaStream.stop(); - } - return; - } - - if (mediaStream.getAudioTracks().length) { - mediaStream.getAudioTracks().forEach(function(track) { - track.stop(); - }); - } - - if (mediaStream.getVideoTracks().length) { - mediaStream.getVideoTracks().forEach(function(track) { - track.stop(); - }); - } - - if (isFirefox) { - // Firefox don't yet support onended for any stream (remote/local) - if (mediaStream.onended) mediaStream.onended(); - } - }; - - 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; - - // if (channel != connection.channel || (connection.isInitiator && channel == connection.channel)) - firebase.onDisconnect().remove(); - - setTimeout(function() { - config.callback(firebase); - }, 1); - }; - - connection.Plugin = Plugin; - } -})(); diff --git a/RTCMultiConnection/README.md b/RTCMultiConnection/README.md index eb2a285f..661e6205 100644 --- a/RTCMultiConnection/README.md +++ b/RTCMultiConnection/README.md @@ -1,19 +1,61 @@ - / Wiki Pages / Documentation / FAQ + / Wiki Pages / Documentation / FAQ / Demos / Changes Log -## WebRTC Library [![npm](https://img.shields.io/npm/v/rtcmulticonnection.svg)](https://npmjs.org/package/rtcmulticonnection) [![downloads](https://img.shields.io/npm/dm/rtcmulticonnection.svg)](https://npmjs.org/package/rtcmulticonnection) +## WebRTC Library [![npm](https://img.shields.io/npm/v/rtcmulticonnection.svg)](https://npmjs.org/package/rtcmulticonnection) [![downloads](https://img.shields.io/npm/dm/rtcmulticonnection.svg)](https://npmjs.org/package/rtcmulticonnection) [![Build Status: Linux](https://travis-ci.org/muaz-khan/RTCMultiConnection.png?branch=master)](https://travis-ci.org/muaz-khan/RTCMultiConnection) Quick-Demos for newbies: 1. http://jsfiddle.net/zar6fg60/ (simplest) 2. http://jsfiddle.net/c46de0L8/ (simple) -Changes log for current version: http://www.rtcmulticonnection.org/changes-log/#v2.0 + + + + + + + + + + + + +
+ + Demos + Demos + + + + Getting-Started + Getting Started + + + + Documentation + Documentation + + + + Changes Log + Changes Log + + + + FAQ + FAQ + +
RTCMultiConnection is a WebRTC JavaScript wrapper library runs top over RTCPeerConnection API to provide multi-session establishment scenarios. It also provides dozens of features as hybrid-less mesh networking model, a reliable presence detection and syncing system; complex renegotiation scenarios; and much more. It provides everything you've in your mind! Just understand the API and you'll enjoy using it! It is simple and its syntax is similar as WebSockets JavaScript API and RTCPeerConnection API. It is MIT Licenced, which means that you can use it in any commercial/non-commercial product, free of cost. +## Upgrade? Please check [list of breaking changes](https://github.com/muaz-khan/RTCMultiConnection/wiki/Breaking-Changes) + ``` +bower install rtcmulticonnection + +# or NPM! npm install rtcmulticonnection ``` @@ -59,7 +101,7 @@ moderator.userid = MODERATOR_ID; moderator.extra = MODERATOR_EXTRA; moderator.open({ dontTransmit: true, - sessionid: MODERATOR_SESSION_ID + sessionid : MODERATOR_SESSION_ID }); ``` @@ -69,9 +111,9 @@ moderator.open({ var participants = new RTCMultiConnection(MODERATOR_CHANNEL_ID); participants.join({ sessionid: MODERATOR_SESSION_ID, - userid: MODERATOR_ID, - extra: MODERATOR_EXTRA, - session: MODERATOR_SESSION + userid : MODERATOR_ID, + extra : MODERATOR_EXTRA, + session : MODERATOR_SESSION }); ``` @@ -120,53 +162,14 @@ moderator.onstreamended = function(event) { ``` - - - - - - - - - - - - -
- - Demos - Demos - - - - Getting-Started - Getting Started - - - - Documentation - Documentation - - - - Changes Log - Changes Log - - - - FAQ - FAQ - -
- ## Demos using [RTCMultiConnection](http://www.RTCMultiConnection.org/) | Experiment Name | Demo | Source Code | @@ -212,17 +215,13 @@ document.getElementById('joinRoom').onclick = function() { | takeSnapshot i.e. Take Snapshot of Local/Remote streams | [Demo](https://www.webrtc-experiment.com/RTCMultiConnection/takeSnapshot.html) | [Source](https://github.com/muaz-khan/RTCMultiConnection/tree/master/demos/takeSnapshot.html) | | Audio/Video/Screen sharing and recording | [Demo](https://www.webrtc-experiment.com/RTCMultiConnection/audio-video-screen-sharing-recording.html) | [Source](https://github.com/muaz-khan/RTCMultiConnection/tree/master/demos/audio-video-screen-sharing-recording.html) | | Broadcast Multiple-Cameras | [Demo](https://www.webrtc-experiment.com/RTCMultiConnection/Broadcast-Multiple-Cameras.html) | [Source](https://github.com/muaz-khan/RTCMultiConnection/tree/master/demos/Broadcast-Multiple-Cameras.html) | +| Remote Stream Forwarding | [Demo](https://www.webrtc-experiment.com/RTCMultiConnection/remote-stream-forwarding.html) | [Source](https://github.com/muaz-khan/RTCMultiConnection/tree/master/demos/remote-stream-forwarding.html) | +| WebRTC Scalable Broadcast | Socketio/Nodejs | [Source](https://github.com/muaz-khan/WebRTC-Scalable-Broadcast) | -## Credits - -[Muaz Khan](https://github.com/muaz-khan): +## Credits & Contributors -1. Personal Webpage: http://www.muazkhan.com -2. Email: muazkh@gmail.com -3. Twitter: https://twitter.com/muazkh and https://twitter.com/WebRTCWeb -4. Google+: https://plus.google.com/+WebRTC-Experiment -5. Facebook: https://www.facebook.com/WebRTC +* [CONTRIBUTING.md](https://github.com/muaz-khan/RTCMultiConnection/blob/master/CONTRIBUTING.md) ## License -[RTCMultiConnection](https://github.com/muaz-khan/RTCMultiConnection) is released under [MIT licence](https://www.webrtc-experiment.com/licence/) . Copyright (c) [Muaz Khan](https://plus.google.com/+MuazKhan). +[RTCMultiConnection](https://github.com/muaz-khan/RTCMultiConnection) is released under [MIT licence](https://github.com/muaz-khan/RTCMultiConnection/blob/master/LICENSE.md) . Copyright (c) [Muaz Khan](http://www.MuazKhan.com/). diff --git a/RTCMultiConnection/Library/RTCMultiConnection-v2.2.js b/RTCMultiConnection/RTCMultiConnection.js similarity index 80% rename from RTCMultiConnection/Library/RTCMultiConnection-v2.2.js rename to RTCMultiConnection/RTCMultiConnection.js index d1aeb785..dfa26b12 100644 --- a/RTCMultiConnection/Library/RTCMultiConnection-v2.2.js +++ b/RTCMultiConnection/RTCMultiConnection.js @@ -1,4 +1,4 @@ -// Last time updated at Nov 05, 2014, 08:32:23 +// Last time updated at Nov 18, 2014, 08:32:23 // Quick-Demo for newbies: http://jsfiddle.net/c46de0L8/ // Another simple demo: http://jsfiddle.net/zar6fg60/ @@ -13,34 +13,31 @@ // Demos - www.WebRTC-Experiment.com/RTCMultiConnection // _________________________ -// RTCMultiConnection-v2.2.3 +// RTCMultiConnection-v2.2.4 /* issues/features need to be fixed & implemented: -. v2.0.* changes-log here: http://www.rtcmulticonnection.org/changes-log/#v2.2 -. trello: https://trello.com/b/8bhi1G6n/rtcmulticonnection --. v2.2.1 breaking updates: ---. connection.stats.sessions is removed; use connection.sessionDescriptions instead. ---. connection.stats.numberOfSessions is removed; use connection.numberOfSessions instead. ---. connection.stats.numberOfConnectedUsers is removed; use connection.numberOfConnectedUsers instead. ---. connection.getStats and connection.stats are removed. - --. screen capturing improved & some bugs fixed. --. connection.stopMediaStream improved. --. fixed: audio-only stream & crash. --. added: connection.attachExternalStream(MediaStream, isScreen); +--. connection.candidates={relay:true} fixed. (a=candidate is removed). +--. connection.numberOfConnectedUsers is fixed. -connection.rtcConfiguration +--. connection.rtcConfiguration */ -(function() { +'use strict'; - // RMC == RTCMultiConnection +(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, ''); + window.RMCDefaultChannel = location.href.replace(/\/|:|#|\?|\$|\^|%|\.|`|~|!|@|\[|\||]|\|*. /g, '').split('\n').join('').split('\r').join(''); // www.RTCMultiConnection.org/docs/constructor/ window.RTCMultiConnection = function(channel) { @@ -91,7 +88,10 @@ connection.rtcConfiguration connection.socket.remove(); } - if (!connection.sessionid) connection.sessionid = connection.channel; + if (!connection.sessionid) { + connection.sessionid = connection.channel; + } + connection.sessionDescription = { sessionid: connection.sessionid, userid: connection.userid, @@ -115,14 +115,16 @@ connection.rtcConfiguration } // 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 - }); + if (!rtcMultiSession.captureUserMediaOnDemand) { + captureUserMedia(function() { + rtcMultiSession.initSession({ + sessionDescription: connection.sessionDescription, + dontTransmit: dontTransmit + }); - invokeMediaCaptured(connection); - }); + invokeMediaCaptured(connection); + }); + } if (rtcMultiSession.captureUserMediaOnDemand) { rtcMultiSession.initSession({ @@ -164,8 +166,9 @@ connection.rtcConfiguration } // send file/data or /text - if (!data) + 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! @@ -174,7 +177,9 @@ connection.rtcConfiguration // 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); + if (data[i].size && data[i].type) { + connection.send(data[i], _channel); + } } return; } @@ -198,9 +203,12 @@ connection.rtcConfiguration }, 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); + rtcMultiSession.fileBufferReader.getNextChunk(uuid, function(nextChunk) { + if (_channel) { + _channel.send(nextChunk); + } else { + rtcMultiSession.send(nextChunk); + } }); }, extra); } else { @@ -225,14 +233,18 @@ connection.rtcConfiguration // RTCMultiSession is the backbone object; // this object MUST be initialized once! - if (rtcMultiSession) return onSignalingReady(); + if (rtcMultiSession) { + return onSignalingReady(); + } // your everything is passed over RTCMultiSession constructor! rtcMultiSession = new RTCMultiSession(connection, onSignalingReady); } connection.disconnect = function() { - if (rtcMultiSession) rtcMultiSession.disconnect(); + if (rtcMultiSession) { + rtcMultiSession.disconnect(); + } rtcMultiSession = null; }; @@ -257,11 +269,12 @@ connection.rtcConfiguration if (isString(session)) { if (connection.sessionDescriptions[session]) { session = connection.sessionDescriptions[session]; - } else + } else { return setTimeout(function() { log('Session-Descriptions not found. Rechecking..'); joinSession(session, joinAs); }, 1000); + } } // connection.join('sessionid', { audio: true }); @@ -275,15 +288,15 @@ connection.rtcConfiguration if (!session || !session.userid || !session.sessionid) { error('missing arguments', arguments); - var error = 'Invalid data passed over "connection.join" method.'; + var err = 'Invalid data passed over "connection.join" method.'; connection.onstatechange({ userid: 'browser', extra: {}, name: 'Unexpected data detected.', - reason: error + reason: err }); - throw error; + throw err; } if (!connection.dontOverrideSession) { @@ -313,7 +326,9 @@ connection.rtcConfiguration var session = _session || connection.session; if (isEmpty(session)) { - if (callback) callback(); + if (callback) { + callback(); + } return; } @@ -361,7 +376,7 @@ connection.rtcConfiguration return callback(); } - var screen_constraints = { + var screenConstraints = { audio: false, video: { mandatory: { @@ -373,13 +388,45 @@ connection.rtcConfiguration } }; + 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: screenConstraints, + 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 (isFirefox && session.screen) { if (location.protocol !== 'https:') { return error(SCREEN_COMMON_FAILURE); } - warn(Firefox_Screen_Capturing_Warning); + warn(firefoxScreenCapturingWarning); - screen_constraints.video = merge(screen_constraints.video.mandatory, { + screenConstraints.video = merge(screenConstraints.video.mandatory, { mozMediaSource: 'window', // mozMediaSource is redundant here mediaSource: 'window' // 'screen' || 'window' }); @@ -388,54 +435,22 @@ connection.rtcConfiguration // audio+video+screen will become audio+screen for Firefox // because Firefox isn't supporting multi-streams feature if (constraints.audio /* && !session.video */ ) { - screen_constraints.audio = true; + screenConstraints.audio = true; constraints = {}; } - delete screen_constraints.video.chromeMediaSource; + delete screenConstraints.video.chromeMediaSource; } // if screen is prompted if (session.screen) { - if (isChrome && DetectRTC.screen.extensionid != ReservedExtensionID) { + 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(); } @@ -445,30 +460,30 @@ connection.rtcConfiguration } // 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) { + 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') { + if (status === 'installed-enabled') { DetectRTC.screen.chromeMediaSource = 'desktop'; } captureUserMedia(callback, _session, true); - log('chrome extension is installed?', DetectRTC.screen.chromeMediaSource == 'desktop'); + log('chrome extension is installed?', DetectRTC.screen.chromeMediaSource === 'desktop'); }); return; } - if (isChrome && useCustomChromeExtensionForScreenCapturing && DetectRTC.screen.chromeMediaSource == 'desktop' && !DetectRTC.screen.sourceId) { + if (isChrome && useCustomChromeExtensionForScreenCapturing && DetectRTC.screen.chromeMediaSource === 'desktop' && !DetectRTC.screen.sourceId) { DetectRTC.screen.getSourceId(function(sourceId) { - if (sourceId == 'PermissionDeniedError') { + if (sourceId === 'PermissionDeniedError') { var mediaStreamError = { message: 'User denied to share content of his screen.', name: 'PermissionDeniedError', - constraintName: screen_constraints, + constraintName: screenConstraints, session: session }; currentUserMediaRequest.mutex = false; @@ -476,7 +491,7 @@ connection.rtcConfiguration return connection.onMediaError(mediaStreamError); } - if (sourceId == 'No-Response') { + 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); @@ -487,19 +502,23 @@ connection.rtcConfiguration return; } - if (isChrome && DetectRTC.screen.chromeMediaSource == 'desktop') { - screen_constraints.video.mandatory.chromeMediaSourceId = DetectRTC.screen.sourceId; + if (isChrome && DetectRTC.screen.chromeMediaSource === 'desktop') { + screenConstraints.video.mandatory.chromeMediaSourceId = DetectRTC.screen.sourceId; } var _isFirstSession = isFirstSession; - _captureUserMedia(screen_constraints, constraints.audio || constraints.video ? function() { + _captureUserMedia(screenConstraints, constraints.audio || constraints.video ? function() { - if (_isFirstSession) isFirstSession = true; + if (_isFirstSession) { + isFirstSession = true; + } _captureUserMedia(constraints, callback); } : callback); - } else _captureUserMedia(constraints, callback, session.audio && !session.video); + } else { + _captureUserMedia(constraints, callback, session.audio && !session.video); + } function _captureUserMedia(forcedConstraints, forcedCallback, isRemoveVideoTracks, dontPreventSSLAutoAllowed) { connection.onstatechange({ @@ -534,12 +553,14 @@ connection.rtcConfiguration var mediaConfig = { onsuccess: function(stream, returnBack, idInstance, streamid) { - onStreamSuccessCallback(stream, returnBack, idInstance, streamid, forcedConstraints, forcedCallback, isRemoveVideoTracks, screen_constraints, constraints, session); + onStreamSuccessCallback(stream, returnBack, idInstance, streamid, forcedConstraints, forcedCallback, isRemoveVideoTracks, screenConstraints, constraints, session); }, onerror: function(e, constraintUsed) { + var mediaStreamError; + // http://goo.gl/hrwF1a if (isFirefox) { - if (e == 'PERMISSION_DENIED') { + if (e === 'PERMISSION_DENIED') { e = { message: '', name: 'PermissionDeniedError', @@ -551,7 +572,7 @@ connection.rtcConfiguration if (isFirefox && constraintUsed.video && constraintUsed.video.mozMediaSource) { mediaStreamError = { - message: Firefox_Screen_Capturing_Warning, + message: firefoxScreenCapturingWarning, name: e.name || 'PermissionDeniedError', constraintName: constraintUsed, session: session @@ -572,8 +593,8 @@ connection.rtcConfiguration // 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: '; + if (e.name && (e.name === 'PermissionDeniedError' || e.name === 'DevicesNotFoundError')) { + 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.'; @@ -630,8 +651,8 @@ connection.rtcConfiguration } } - if (e.name && e.name == 'ConstraintNotSatisfiedError') { - var mediaStreamError = 'Either: '; + if (e.name && e.name === 'ConstraintNotSatisfiedError') { + mediaStreamError = 'Either: '; mediaStreamError += '\n You are prompting unknown media resolutions.'; mediaStreamError += '\n You are using invalid media constraints.'; @@ -651,9 +672,9 @@ connection.rtcConfiguration if (session.screen) { if (isFirefox) { - error(Firefox_Screen_Capturing_Warning); + error(firefoxScreenCapturingWarning); } else if (location.protocol !== 'https:') { - if (!isNodeWebkit && (location.protocol == 'file:' || location.protocol == 'http:')) { + 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 { @@ -678,8 +699,10 @@ connection.rtcConfiguration } } - function onStreamSuccessCallback(stream, returnBack, idInstance, streamid, forcedConstraints, forcedCallback, isRemoveVideoTracks, screen_constraints, constraints, session) { - if (!streamid) streamid = getRandomString(); + function onStreamSuccessCallback(stream, returnBack, idInstance, streamid, forcedConstraints, forcedCallback, isRemoveVideoTracks, screenConstraints, constraints, session) { + if (!streamid) { + streamid = getRandomString(); + } connection.onstatechange({ userid: 'browser', @@ -700,7 +723,7 @@ connection.rtcConfiguration // when a stream is stopped; it must be removed from "attachStreams" array connection.attachStreams.forEach(function(_stream, index) { - if (_stream == stream) { + if (_stream === stream) { delete connection.attachStreams[index]; connection.attachStreams = swap(connection.attachStreams); } @@ -735,9 +758,9 @@ connection.rtcConfiguration if (!isIE) { stream.streamid = streamid; - stream.isScreen = forcedConstraints == screen_constraints; - stream.isVideo = forcedConstraints == constraints && !!constraints.video; - stream.isAudio = forcedConstraints == constraints && !!constraints.audio && !constraints.video; + stream.isScreen = forcedConstraints === screenConstraints; + stream.isVideo = forcedConstraints === constraints && !!constraints.video; + stream.isAudio = forcedConstraints === constraints && !!constraints.audio && !constraints.video; // if muted stream is negotiated stream.preMuted = { @@ -780,7 +803,9 @@ connection.rtcConfiguration connection.setDefaultEventsForMediaElement(mediaElement, streamid); } - if (forcedCallback) forcedCallback(stream, streamedObject); + if (forcedCallback) { + forcedCallback(stream, streamedObject); + } if (connection.onspeaking) { initHark({ @@ -796,7 +821,9 @@ connection.rtcConfiguration // www.RTCMultiConnection.org/docs/leave/ connection.leave = function(userid) { - if (!rtcMultiSession) return; + if (!rtcMultiSession) { + return; + } isFirstSession = true; @@ -810,8 +837,14 @@ connection.rtcConfiguration // 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.'; + 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 }); @@ -852,13 +885,13 @@ connection.rtcConfiguration constraints.video = true; } - var screen_constraints = { + var screenConstraints = { video: { - chromeMediaSource: 'fake' + chromeMediaSource: 'external-stream' } }; - var forcedConstraints = isScreen ? screen_constraints : constraints; - onStreamSuccessCallback(stream, false, '', null, forcedConstraints, false, false, screen_constraints, constraints, constraints); + var forcedConstraints = isScreen ? screenConstraints : constraints; + onStreamSuccessCallback(stream, false, '', null, forcedConstraints, false, false, screenConstraints, constraints, constraints); }; // www.RTCMultiConnection.org/docs/addStream/ @@ -888,7 +921,9 @@ connection.rtcConfiguration } addStream(stream); }, session); - } else addStream(); + } else { + addStream(); + } function addStream(stream) { rtcMultiSession.addStream({ @@ -910,71 +945,84 @@ connection.rtcConfiguration return; } - if (!streamid) streamid = 'all'; - if (!isString(streamid) || streamid.search(/all|audio|video|screen/gi) != -1) { - for (var stream in connection.streams) { - if (connection._skip.indexOf(stream) == -1) { - _stream = connection.streams[stream]; + function _detachStream(_stream, config) { + if (config.local && _stream.type !== 'local') { + return; + } - if (streamid == 'all') _detachStream(_stream, { - audio: true, - video: true, - screen: true - }); + if (config.remote && _stream.type !== 'remote') { + return; + } - else if (isString(streamid)) { - // connection.removeStream('screen'); - var config = {}; - config[streamid] = true; - _detachStream(_stream, config); - } else _detachStream(_stream, streamid); - } + // connection.removeStream({screen:true}); + if (config.screen && !!_stream.isScreen) { + connection.detachStreams.push(_stream.streamid); } - if (!dontRenegotiate && connection.detachStreams.length) { - connection.renegotiate(); + // connection.removeStream({audio:true}); + if (config.audio && !!_stream.isAudio) { + connection.detachStreams.push(_stream.streamid); } - function _detachStream(_stream, config) { - if (config.local && _stream.type != 'local') return; - if (config.remote && _stream.type != 'remote') return; + // connection.removeStream({video:true}); + if (config.video && !!_stream.isVideo) { + connection.detachStreams.push(_stream.streamid); + } - // connection.removeStream({screen:true}); - if (config.screen && !!_stream.isScreen) { - connection.detachStreams.push(_stream.streamid); - } + // connection.removeStream({}); + if (!config.audio && !config.video && !config.screen) { + connection.detachStreams.push(_stream.streamid); + } - // connection.removeStream({audio:true}); - if (config.audio && !!_stream.isAudio) { - connection.detachStreams.push(_stream.streamid); - } + if (connection.detachStreams.indexOf(_stream.streamid) !== -1) { + log('removing stream', _stream.streamid); + onStreamEndedHandler(_stream, connection); - // connection.removeStream({video:true}); - if (config.video && !!_stream.isVideo) { - connection.detachStreams.push(_stream.streamid); + if (config.stop) { + connection.stopMediaStream(_stream.stream); } + } + } - // connection.removeStream({}); - if (!config.audio && !config.video && !config.screen) { - connection.detachStreams.push(_stream.streamid); - } + if (!streamid) { + streamid = 'all'; + } - if (connection.detachStreams.indexOf(_stream.streamid) != -1) { - log('removing stream', _stream.streamid); - onStreamEndedHandler(_stream, connection); + var stream; + if (!isString(streamid) || streamid.search(/all|audio|video|screen/gi) !== -1) { + for (stream in connection.streams) { + if (connection._skip.indexOf(stream) === -1) { + var _stream = connection.streams[stream]; - if (config.stop) { - connection.stopMediaStream(_stream.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]; + stream = connection.streams[streamid]; // detach pre-attached streams - if (!stream) return warn('No such stream exists. Stream-id:', streamid); + if (!stream) { + return warn('No such stream exists. Stream-id:', streamid); + } // www.RTCMultiConnection.org/docs/detachStreams/ connection.detachStreams.push(stream.streamid); @@ -1058,24 +1106,37 @@ connection.rtcConfiguration e.data = translatedText; connection.onmessage(e); }); - } else connection.onmessage(e); + } else { + connection.onmessage(e); + } } } function onNewSession(session) { - if (connection.skipOnNewSession) return; + if (connection.skipOnNewSession) { + return; + } + + if (!session.session) { + session.session = {}; + } - if (!session.session) session.session = {}; - if (!session.extra) session.extra = {}; + 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.sessionid && session.sessionid !== connection.sessionid) { + return; + } if (connection.onNewSession) { session.join = function(forceSession) { - if (!forceSession) return connection.join(session); + if (!forceSession) { + return connection.join(session); + } for (var f in forceSession) { session.session[f] = forceSession[f]; @@ -1093,7 +1154,10 @@ connection.rtcConfiguration connection.dontCaptureUserMedia = isDontCaptureUserMedia; }, forceSession); }; - if (!session.extra) session.extra = {}; + + if (!session.extra) { + session.extra = {}; + } return connection.onNewSession(session); } @@ -1117,11 +1181,16 @@ connection.rtcConfiguration channel: _config.channel, onmessage: socketResponse, onopen: function(_socket) { - if (_socket) socket = _socket; + if (_socket) { + socket = _socket; + } if (isofferer && !peer) { peerConfig.session = connection.session; - if (!peer) peer = new PeerConnection(); + if (!peer) { + peer = new PeerConnection(); + } + peer.create('offer', peerConfig); } @@ -1149,7 +1218,9 @@ connection.rtcConfiguration }; var socket = connection.openSignalingChannel(socketConfig); - if (socket) socketConfig.onopen(socket); + if (socket) { + socketConfig.onopen(socket); + } var isofferer = _config.isofferer, peer; @@ -1157,29 +1228,58 @@ connection.rtcConfiguration 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.'; + 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 (!isNull(iceCandidates.reflexive)) { + stun = iceCandidates.reflexive; + } + + if (!isNull(iceCandidates.relay)) { + turn = iceCandidates.relay; + } + + if (!iceCandidates.host && !!candidate.candidate.match(/typ host/g)) { + return; + } - 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; + if (!turn && !!candidate.candidate.match(/typ relay/g)) { + return; + } + + if (!stun && !!candidate.candidate.match(/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 (!protocol.udp && !!candidate.candidate.match(/ udp /g)) { + return; + } + + if (!protocol.tcp && !!candidate.candidate.match(/ tcp /g)) { + return; + } + + if (!window.selfNPObject) { + window.selfNPObject = candidate; + } - if (!window.selfNPObject) window.selfNPObject = candidate; + if (!socket) { + return; + } - socket && socket.send({ + socket.send({ candidate: JSON.stringify({ candidate: candidate.candidate, sdpMid: candidate.sdpMid, @@ -1188,10 +1288,12 @@ connection.rtcConfiguration }); }, onmessage: function(data) { - if (!data) return; + if (!data) { + return; + } var abToStr = ab2str(data); - if (abToStr.indexOf('"userid":') != -1) { + if (abToStr.indexOf('"userid":') !== -1) { abToStr = JSON.parse(abToStr); onDataChannelMessage(abToStr); } else if (data instanceof ArrayBuffer || data instanceof DataView) { @@ -1214,7 +1316,7 @@ connection.rtcConfiguration if (chunk.maxChunks || chunk.readyForNextChunk) { // if target peer requested next chunk if (chunk.readyForNextChunk) { - fileBufferReader.getNextChunk(chunk.uuid, function(nextChunk, isLastChunk, extra) { + fileBufferReader.getNextChunk(chunk.uuid, function(nextChunk) { rtcMultiSession.send(nextChunk); }); return; @@ -1241,7 +1343,9 @@ connection.rtcConfiguration session = session || _config.renegotiate || connection.session; // if it is Firefox; then return. - if (isData(session)) return; + if (isData(session)) { + return; + } if (session.screen && (session.audio || session.video)) { if (!_config.gotAudioOrVideo) { @@ -1257,6 +1361,20 @@ connection.rtcConfiguration var preMuted = {}; + function eventListener() { + setTimeout(function() { + mediaElement.muted = false; + afterRemoteStreamStartedFlowing({ + mediaElement: mediaElement, + session: session, + stream: stream, + preMuted: preMuted + }); + }, 3000); + + mediaElement.removeEventListener('play', eventListener); + } + if (_config.streaminfo) { var streaminfo = _config.streaminfo.split('----'); var strInfo = JSON.parse(streaminfo[streaminfo.length - 1]); @@ -1282,19 +1400,6 @@ connection.rtcConfiguration } 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); } @@ -1313,7 +1418,9 @@ connection.rtcConfiguration log('on:stream:ended via on:remove:stream', stream); onStreamEndedHandler(stream, connection); } - } else log('on:remove:stream', stream); + } else { + log('on:remove:stream', stream); + } }, onclose: function(e) { @@ -1335,7 +1442,7 @@ connection.rtcConfiguration 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) { + 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, @@ -1344,7 +1451,7 @@ connection.rtcConfiguration }); } - if (!connection.isInitiator && peer.connection && peer.connection.iceConnectionState == 'connected' && peer.connection.iceGatheringState == 'complete' && peer.connection.signalingState == 'stable' && connection.numberOfConnectedUsers == 1) { + 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, @@ -1358,7 +1465,7 @@ connection.rtcConfiguration } // if ICE connectivity check is failed; renegotiate or redial - if (connection.peers[_config.userid] && connection.peers[_config.userid].peer.connection.iceConnectionState == 'failed') { + if (connection.peers[_config.userid] && connection.peers[_config.userid].peer.connection.iceConnectionState === 'failed') { connection.onfailed({ userid: _config.userid, extra: _config.extra, @@ -1367,24 +1474,28 @@ connection.rtcConfiguration }); } - 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 - }); + if (connection.peers[_config.userid] && connection.peers[_config.userid].peer.connection.iceConnectionState === 'disconnected') { + if (!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.autoReDialOnFailure) { + return; + } if (connection.peers[_config.userid]) { - if (connection.peers[_config.userid].peer.connection.iceConnectionState != 'disconnected') { + if (connection.peers[_config.userid].peer.connection.iceConnectionState !== 'disconnected') { _config.redialing = false; } - if (connection.peers[_config.userid].peer.connection.iceConnectionState == 'disconnected' && !_config.redialing) { + 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({ @@ -1424,7 +1535,11 @@ connection.rtcConfiguration trickleIce: connection.trickleIce, processSdp: connection.processSdp, sendStreamId: function(stream) { - socket && socket.send({ + if (!socket) { + return; + } + + socket.send({ streamid: stream.streamid, isScreen: !!stream.isScreen, isAudio: !!stream.isAudio, @@ -1436,14 +1551,17 @@ connection.rtcConfiguration function waitUntilRemoteStreamStartsFlowing(args) { // chrome for android may have some features missing - if (isMobileDevice || isPluginRTC || (isNull(connection.waitUntilRemoteStreamStartsFlowing) || !connection.waitUntilRemoteStreamStartsFlowing)) { + if (isMobileDevice || isPluginRTC || (!isNull(connection.waitUntilRemoteStreamStartsFlowing) && connection.waitUntilRemoteStreamStartsFlowing === false)) { return afterRemoteStreamStartedFlowing(args); } - if (!args.numberOfTimes) args.numberOfTimes = 0; + if (!args.numberOfTimes) { + args.numberOfTimes = 0; + } + args.numberOfTimes++; - if (!(args.mediaElement.readyState <= HTMLMediaElement.HAVE_CURRENT_DATA || args.mediaElement.paused || args.mediaElement.currentTime <= 0)) { + if (!(args.mediaElement.readyState <= window.HTMLMediaElement.HAVE_CURRENT_DATA || args.mediaElement.paused || args.mediaElement.currentTime <= 0)) { return afterRemoteStreamStartedFlowing(args); } @@ -1461,7 +1579,9 @@ connection.rtcConfiguration } function initFakeChannel() { - if (!connection.fakeDataChannels || connection.channels[_config.userid]) return; + if (!connection.fakeDataChannels || connection.channels[_config.userid]) { + return; + } // for non-data connections; allow fake data sender! if (!connection.session.data) { @@ -1569,7 +1689,9 @@ connection.rtcConfiguration connection.send(connection.fileQueue[q], channel); } - if (isData(connection.session)) onSessionOpened(); + if (isData(connection.session)) { + onSessionOpened(); + } if (connection.partOfScreen && connection.partOfScreen.sharing) { connection.peers[_config.userid].sharePartOfScreen(connection.partOfScreen); @@ -1578,8 +1700,9 @@ connection.rtcConfiguration function updateSocket() { // todo: need to check following {if-block} MUST not affect "redial" process - if (socket.userid == _config.userid) + if (socket.userid === _config.userid) { return; + } socket.userid = _config.userid; sockets[_config.socketIndex] = socket; @@ -1598,8 +1721,9 @@ connection.rtcConfiguration connection.addStream(session00, this.socket); }, removeStream: function(streamid) { - if (!connection.streams[streamid]) + if (!connection.streams[streamid]) { return warn('No such stream exists. Stream-id:', streamid); + } this.peer.connection.removeStream(connection.streams[streamid].stream); this.renegotiate(); @@ -1612,8 +1736,13 @@ connection.rtcConfiguration 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}'; + 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; @@ -1640,28 +1769,32 @@ connection.rtcConfiguration // connection.peers['user-id'].drop(); for (var stream in connection.streams) { - if (connection._skip.indexOf(stream) == -1) { + if (connection._skip.indexOf(stream) === -1) { stream = connection.streams[stream]; - if (stream.userid == connection.userid && stream.type == 'local') { + 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) { + if (stream.type === 'remote' && stream.userid === this.userid) { onStreamEndedHandler(stream, connection); } } } - !dontSendMessage && this.socket.send({ + if (!!dontSendMessage) { + return; + } + + this.socket.send({ drop: true }); }, hold: function(holdMLine) { // connection.peers['user-id'].hold(); - if (peer.prevCreateType == 'answer') { + if (peer.prevCreateType === 'answer') { this.socket.send({ unhold: true, holdMLine: holdMLine || 'both', @@ -1686,7 +1819,7 @@ connection.rtcConfiguration unhold: function(holdMLine) { // connection.peers['user-id'].unhold(); - if (peer.prevCreateType == 'answer') { + if (peer.prevCreateType === 'answer') { this.socket.send({ unhold: true, holdMLine: holdMLine || 'both', @@ -1720,21 +1853,23 @@ connection.rtcConfiguration // 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) { + if (connection._skip.indexOf(stream) === -1) { stream = connection.streams[stream]; - if (stream.userid == userid) { + if (stream.userid === userid) { // www.RTCMultiConnection.org/docs/onhold/ - if (isHold) + if (isHold) { connection.onhold(merge({ kind: kind }, stream)); + } // www.RTCMultiConnection.org/docs/onunhold/ - if (!isHold) + if (!isHold) { connection.onunhold(merge({ kind: kind }, stream)); + } } } } @@ -1744,10 +1879,10 @@ connection.rtcConfiguration // 1st of all; remove all relevant remote media streams for (var stream in connection.streams) { - if (connection._skip.indexOf(stream) == -1) { + if (connection._skip.indexOf(stream) === -1) { stream = connection.streams[stream]; - if (stream.userid == this.userid && stream.type == 'remote') { + if (stream.userid === this.userid && stream.type === 'remote') { onStreamEndedHandler(stream, connection); } } @@ -1796,7 +1931,7 @@ connection.rtcConfiguration } // don't share repeated content - if (screenshot != lastScreenshot) { + if (screenshot !== lastScreenshot) { lastScreenshot = screenshot; connection.channels[that.userid].send({ screenshot: screenshot, @@ -1805,7 +1940,11 @@ connection.rtcConfiguration } // "once" can be used to share single screenshot - !args.once && setTimeout(partOfScreenCapturer, args.interval || 200); + if (!!args.once) { + return; + } + + setTimeout(partOfScreenCapturer, args.interval || 200); } }); } @@ -1813,15 +1952,22 @@ connection.rtcConfiguration partOfScreenCapturer(); }, getConnectionStats: function(callback, interval) { - if (!callback) throw 'callback is mandatory.'; + if (!callback) { + throw 'callback is mandatory.'; + } if (!window.getConnectionStats) { loadScript(connection.resources.getConnectionStats, invoker); - } else invoker(); + } else { + invoker(); + } function invoker() { RTCPeerConnection.prototype.getConnectionStats = window.getConnectionStats; - peer.connection && peer.connection.getConnectionStats(callback, interval); + if (!peer.connection) { + return; + } + peer.connection.getConnectionStats(callback, interval); } }, takeSnapshot: function(callback) { @@ -1852,7 +1998,7 @@ connection.rtcConfiguration // 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) { + 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) { @@ -1866,10 +2012,13 @@ connection.rtcConfiguration } function socketResponse(response) { - if (isRMSDeleted) return; + if (isRMSDeleted) { + return; + } - if (response.userid == connection.userid) + if (response.userid === connection.userid) { return; + } if (response.sdp) { _config.userid = response.userid; @@ -1881,7 +2030,7 @@ connection.rtcConfiguration var sdp = JSON.parse(response.sdp); - if (sdp.type == 'offer') { + if (sdp.type === 'offer') { // to synchronize SCTP or RTP peerConfig.preferSCTP = !!response.preferSCTP; connection.fakeDataChannels = !!response.fakeDataChannels; @@ -1893,8 +2042,8 @@ connection.rtcConfiguration sdpInvoker(sdp, response.labels); } - if (response.candidate) { - peer && peer.addIceCandidate(JSON.parse(response.candidate)); + if (response.candidate && peer) { + peer.addIceCandidate(JSON.parse(response.candidate)); } if (response.streamid) { @@ -1941,8 +2090,13 @@ connection.rtcConfiguration 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.mute) { + connection.onmute(fakeObject || response); + } + + if (response.unmute) { + connection.onunmute(fakeObject || response); + } } } @@ -1950,7 +2104,9 @@ connection.rtcConfiguration 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; + if (mediaElement) { + mediaElement.volume = response.volume; + } } } @@ -1985,7 +2141,7 @@ connection.rtcConfiguration var userLeft = response.userid; for (var stream in connection.streams) { stream = connection.streams[stream]; - if (stream.userid == userLeft) { + if (stream.userid === userLeft) { connection.stopMediaStream(stream); onStreamEndedHandler(stream, connection); } @@ -1994,13 +2150,15 @@ connection.rtcConfiguration if (peer && peer.connection) { // todo: verify if-block's 2nd condition - if (peer.connection.signalingState != 'closed' && peer.connection.iceConnectionState.search(/disconnected|failed/gi) == -1) { + 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 (participants[response.userid]) { + delete participants[response.userid]; + } if (response.closeEntireSession) { connection.onSessionClosed(response); @@ -2031,7 +2189,7 @@ connection.rtcConfiguration delete participants[connection.userid]; } - if (sockets[0] && sockets[0].userid == response.userid) { + if (sockets[0] && sockets[0].userid === response.userid) { delete sockets[0]; sockets = swap(sockets); } @@ -2045,7 +2203,9 @@ connection.rtcConfiguration } if (response.changeBandwidth) { - if (!connection.peers[response.userid]) throw 'No such peer exists.'; + if (!connection.peers[response.userid]) { + throw 'No such peer exists.'; + } // synchronize bandwidth connection.peers[response.userid].peer.bandwidth = response.bandwidth; @@ -2055,9 +2215,12 @@ connection.rtcConfiguration } if (response.customMessage) { - if (!connection.peers[response.userid]) throw 'No such peer exists.'; + if (!connection.peers[response.userid]) { + throw 'No such peer exists.'; + } + if (response.message.ejected) { - if (connection.sessionDescriptions[connection.sessionid].userid != response.userid) { + if (connection.sessionDescriptions[connection.sessionid].userid !== response.userid) { throw 'only initiator can eject a user.'; } // initiator ejected this user @@ -2068,11 +2231,16 @@ connection.rtcConfiguration extra: response.extra || _config.extra, isEjected: true }); - } else connection.peers[response.userid].onCustomMessage(response.message); + } else { + connection.peers[response.userid].onCustomMessage(response.message); + } } if (response.drop) { - if (!connection.peers[response.userid]) throw 'No such peer exists.'; + if (!connection.peers[response.userid]) { + throw 'No such peer exists.'; + } + connection.peers[response.userid].drop(true); connection.peers[response.userid].renegotiate(); @@ -2080,7 +2248,9 @@ connection.rtcConfiguration } if (response.hold || response.unhold) { - if (!connection.peers[response.userid]) throw 'No such peer exists.'; + if (!connection.peers[response.userid]) { + throw 'No such peer exists.'; + } if (response.takeAction) { connection.peers[response.userid][!!response.hold ? 'hold' : 'unhold'](response.holdMLine); @@ -2127,11 +2297,11 @@ connection.rtcConfiguration if (response.redial) { if (connection.peers[response.userid]) { - if (connection.peers[response.userid].peer.connection.iceConnectionState != 'disconnected') { + if (connection.peers[response.userid].peer.connection.iceConnectionState !== 'disconnected') { _config.redialing = false; } - if (connection.peers[response.userid].peer.connection.iceConnectionState == 'disconnected' && !_config.redialing) { + 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..'); @@ -2149,7 +2319,11 @@ connection.rtcConfiguration }; connection.askToShareParticipants = function() { - defaultSocket && defaultSocket.send({ + if (!defaultSocket) { + return; + } + + defaultSocket.send({ askToShareParticipants: true }); }; @@ -2162,24 +2336,33 @@ connection.rtcConfiguration }; if (args) { - if (args.dontShareWith) message.dontShareWith = args.dontShareWith; - if (args.shareWith) message.shareWith = args.shareWith; + 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') { + if (sdp.type === 'answer') { peer.setRemoteDescription(sdp); updateSocket(); return; } - if (!_config.renegotiate && sdp.type == 'offer') { + if (!_config.renegotiate && sdp.type === 'offer') { peerConfig.offerDescription = sdp; peerConfig.session = connection.session; - if (!peer) peer = new PeerConnection(); + + if (!peer) { + peer = new PeerConnection(); + } + peer.create('answer', peerConfig); updateSocket(); @@ -2194,8 +2377,9 @@ connection.rtcConfiguration createAnswer(); delete _config.renegotiate; } else { - if (_config.capturing) + if (_config.capturing) { return; + } _config.capturing = true; @@ -2220,7 +2404,7 @@ connection.rtcConfiguration function createAnswer() { // because Firefox has no support of renegotiation yet; // so both chrome and firefox should redial instead of renegotiate! - if (isFirefox || _config.userinfo.browser == 'firefox') { + if (isFirefox || _config.userinfo.browser === 'firefox') { if (connection.peers[_config.userid]) { connection.peers[_config.userid].redial(); } @@ -2240,7 +2424,10 @@ connection.rtcConfiguration } function detachMediaStream(labels, peer) { - if (!labels) return; + if (!labels) { + return; + } + for (var i = 0; i < labels.length; i++) { var label = labels[i]; if (connection.streams[label]) { @@ -2272,12 +2459,13 @@ connection.rtcConfiguration function onNewParticipant(response) { var channel = response.newParticipant; - if (!channel || !!participants[channel] || channel == connection.userid) + if (!channel || !!participants[channel] || channel === connection.userid) { return; + } - var new_channel = connection.token(); + var newChannel = connection.token(); newPrivateSocket({ - channel: new_channel, + channel: newChannel, extra: response.userData ? response.userData.extra : response.extra, userid: response.userData ? response.userData.userid : response.userid }); @@ -2285,15 +2473,13 @@ connection.rtcConfiguration defaultSocket.send({ participant: true, targetUser: channel, - channel: new_channel + channel: newChannel }); } // if a user leaves function clearSession() { - connection.numberOfConnectedUsers--; - var alertMessage = { left: true, extra: connection.extra || {}, @@ -2340,11 +2526,13 @@ connection.rtcConfiguration // www.RTCMultiConnection.org/docs/remove/ connection.remove = function(userid) { - if (rtcMultiSession.requestsFrom && rtcMultiSession.requestsFrom[userid]) delete rtcMultiSession.requestsFrom[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') { + if (connection.peers[userid].peer.connection.signalingState !== 'closed') { connection.peers[userid].peer.connection.close(); } connection.peers[userid].peer.connection = null; @@ -2355,11 +2543,11 @@ connection.rtcConfiguration delete participants[userid]; } - for (var stream in connection.streams) { - stream = connection.streams[stream]; - if (stream.userid == userid) { + for (var streamid in connection.streams) { + var stream = connection.streams[streamid]; + if (stream.userid === userid) { onStreamEndedHandler(stream, connection); - delete connection.streams[stream]; + delete connection.streams[streamid]; } } @@ -2410,14 +2598,14 @@ connection.rtcConfiguration connection.renegotiatedSessions = {}; for (var peer in connection.peers) { - if (peer != connection.userid) { + 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) { + if (connection._skip.indexOf(stream) === -1) { onStreamEndedHandler(connection.streams[stream], connection); delete connection.streams[stream]; } @@ -2430,7 +2618,10 @@ connection.rtcConfiguration // www.RTCMultiConnection.org/docs/reject/ connection.reject = function(userid) { - if (!isString(userid)) userid = userid.userid; + if (!isString(userid)) { + userid = userid.userid; + } + defaultSocket.send({ rejectedRequestOf: userid }); @@ -2440,13 +2631,15 @@ connection.rtcConfiguration }; rtcMultiSession.leaveHandler = function(e) { - if (!connection.leaveOnPageUnload) return; + if (!connection.leaveOnPageUnload) { + return; + } if (isNull(e.keyCode)) { return clearSession(); } - if (e.keyCode == 116) { + if (e.keyCode === 116) { clearSession(); } }; @@ -2473,15 +2666,18 @@ connection.rtcConfiguration listenEventHandler('offline', rtcMultiSession.onLineOffLineHandler); function onSignalingReady() { - if (rtcMultiSession.signalingReady) return; + if (rtcMultiSession.signalingReady) { + return; + } + rtcMultiSession.signalingReady = true; setTimeout(callbackForSignalingReady, 1000); - if (!connection.isInitiator) { + if (!connection.isInitiator && defaultSocket) { // as soon as signaling gateway is connected; // user should check existing rooms! - defaultSocket && defaultSocket.send({ + defaultSocket.send({ searchingForRooms: true }); } @@ -2503,11 +2699,15 @@ connection.rtcConfiguration function getDefaultSocketRef() { return connection.openSignalingChannel({ onmessage: function(response) { - // RMS == RTCMultiSession - if (isRMSDeleted) return; + // RMS === RTCMultiSession + if (isRMSDeleted) { + return; + } // if message is sent by same user - if (response.userid == connection.userid) return; + if (response.userid === connection.userid) { + return; + } if (response.sessionid && response.userid) { if (!connection.sessionDescriptions[response.sessionid]) { @@ -2529,12 +2729,12 @@ connection.rtcConfiguration } if (response.newParticipant && !connection.isAcceptNewSession && rtcMultiSession.broadcasterid === response.userid) { - if (response.newParticipant != connection.userid) { + if (response.newParticipant !== connection.userid) { onNewParticipant(response); } } - if (getLength(participants) < connection.maxParticipantsAllowed && response.targetUser == connection.userid && response.participant) { + 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]; @@ -2547,7 +2747,7 @@ connection.rtcConfiguration } } - if (response.acceptedRequestOf == connection.userid) { + if (response.acceptedRequestOf === connection.userid) { connection.onstatechange({ userid: response.userid, extra: response.extra, @@ -2556,7 +2756,7 @@ connection.rtcConfiguration }); } - if (response.rejectedRequestOf == connection.userid) { + if (response.rejectedRequestOf === connection.userid) { connection.onstatechange({ userid: response.userid, extra: response.extra, @@ -2572,12 +2772,14 @@ connection.rtcConfiguration connection.attachStreams = []; // "drop" should detach all local streams for (var stream in connection.streams) { - if (connection._skip.indexOf(stream) == -1) { + if (connection._skip.indexOf(stream) === -1) { stream = connection.streams[stream]; - if (stream.type == 'local') { + if (stream.type === 'local') { connection.detachStreams.push(stream.streamid); onStreamEndedHandler(stream, connection); - } else onStreamEndedHandler(stream, connection); + } else { + onStreamEndedHandler(stream, connection); + } } } @@ -2590,14 +2792,14 @@ connection.rtcConfiguration } } - if (connection.isInitiator && response.searchingForRooms) { - defaultSocket && defaultSocket.send({ + if (connection.isInitiator && response.searchingForRooms && defaultSocket) { + defaultSocket.send({ sessionDescription: connection.sessionDescription, responseFor: response.userid }); } - if (response.sessionDescription && response.responseFor == connection.userid) { + if (response.sessionDescription && response.responseFor === connection.userid) { var sessionDescription = response.sessionDescription; if (!connection.sessionDescriptions[sessionDescription.sessionid]) { connection.numberOfSessions++; @@ -2612,21 +2814,23 @@ connection.rtcConfiguration } // participants are shared with single user - if (response.shareWith == connection.userid && response.dontShareWith != connection.userid && response.joinUsers) { + 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) { + if (connection.userid !== response.dontShareWith) { joinParticipants(response.joinUsers); } - } else joinParticipants(response.joinUsers); + } else { + joinParticipants(response.joinUsers); + } } - if (response.messageFor == connection.userid && response.presenceState) { - if (response.presenceState == 'checking') { + if (response.messageFor === connection.userid && response.presenceState) { + if (response.presenceState === 'checking') { defaultSocket.send({ messageFor: response.userid, presenceState: 'available', @@ -2635,7 +2839,7 @@ connection.rtcConfiguration log('participant asked for availability'); } - if (response.presenceState == 'available') { + if (response.presenceState === 'available') { rtcMultiSession.presenceState = 'available'; connection.onstatechange({ @@ -2649,7 +2853,7 @@ connection.rtcConfiguration } } - if (response.donotJoin && response.messageFor == connection.userid) { + if (response.donotJoin && response.messageFor === connection.userid) { log(response.userid, 'is not joining your room.'); } @@ -2660,11 +2864,20 @@ connection.rtcConfiguration } }, callback: function(socket) { - socket && this.onopen(socket); + if (!socket) { + return; + } + + this.onopen(socket); }, onopen: function(socket) { - if (socket) defaultSocket = socket; - if (onSignalingReady) onSignalingReady(); + if (socket) { + defaultSocket = socket; + } + + if (onSignalingReady) { + onSignalingReady(); + } rtcMultiSession.defaultSocket = defaultSocket; @@ -2687,17 +2900,24 @@ connection.rtcConfiguration rtcMultiSession.defaultSocket = defaultSocket; - if (defaultSocket && onSignalingReady) setTimeout(onSignalingReady, 2000); + if (defaultSocket && onSignalingReady) { + setTimeout(onSignalingReady, 2000); + } if (connection.session.screen) { loadScreenFrame(); } - connection.getExternalIceServers && loadIceFrame(function(iceServers) { - connection.iceServers = connection.iceServers.concat(iceServers); - }); + if (connection.getExternalIceServers) { + loadIceFrame(function(iceServers) { + connection.iceServers = connection.iceServers.concat(iceServers); + }); + } + + if (connection.log === false) { + connection.skipLogs(); + } - if (connection.log == false) connection.skipLogs(); if (connection.onlog) { log = warn = error = function() { var log = {}; @@ -2716,21 +2936,30 @@ connection.rtcConfiguration var userMaxParticipantsAllowed = 0; // if user has set a custom max participant setting, remember it - if (connection.maxParticipantsAllowed != 256) { + 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) { + 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) { + if (userMaxParticipantsAllowed && connection.maxParticipantsAllowed !== 1) { connection.maxParticipantsAllowed = userMaxParticipantsAllowed; } } @@ -2753,16 +2982,19 @@ connection.rtcConfiguration defaultSocket.send(connection.sessionDescription); } - if (!connection.transmitRoomOnce && !rtcMultiSession.isOwnerLeaving) + if (!connection.transmitRoomOnce && !rtcMultiSession.isOwnerLeaving) { setTimeout(transmit, connection.interval || 3000); + } } // todo: test and fix next line. - if (!args.dontTransmit /* || connection.transmitRoomOnce */ ) transmit(); + if (!args.dontTransmit /* || connection.transmitRoomOnce */ ) { + transmit(); + } }; - function joinSession(_config, skipOnStateChange) { - if (rtcMultiSession.donotJoin && rtcMultiSession.donotJoin == _config.sessionid) { + function joinSession(_config) { + if (rtcMultiSession.donotJoin && rtcMultiSession.donotJoin === _config.sessionid) { return; } @@ -2803,7 +3035,9 @@ connection.rtcConfiguration if (!isEmpty(offers)) { log(toStr(offers)); - } else log('Seems data-only connection.'); + } else { + log('Seems data-only connection.'); + } connection.onstatechange({ userid: _config.userid, @@ -2829,11 +3063,12 @@ connection.rtcConfiguration // join existing session this.joinSession = function(_config) { - if (!defaultSocket) + if (!defaultSocket) { return setTimeout(function() { warn('Default-Socket is not yet initialized.'); rtcMultiSession.joinSession(_config); }, 1000); + } _config = _config || {}; participants = {}; @@ -2862,11 +3097,11 @@ connection.rtcConfiguration contactInitiator(); function checker() { - if (rtcMultiSession.presenceState == 'checking') { + if (rtcMultiSession.presenceState === 'checking') { warn('Unable to reach initiator. Trying again...'); contactInitiator(); setTimeout(function() { - if (rtcMultiSession.presenceState == 'checking') { + if (rtcMultiSession.presenceState === 'checking') { connection.onstatechange({ userid: _config.userid, extra: _config.extra || {}, @@ -2888,7 +3123,9 @@ connection.rtcConfiguration rtcMultiSession.donotJoin = sessionid; var session = connection.sessionDescriptions[sessionid]; - if (!session) return; + if (!session) { + return; + } defaultSocket.send({ donotJoin: true, @@ -2912,7 +3149,7 @@ connection.rtcConfiguration } if (_channel) { - if (_channel.readyState == 'open') { + if (_channel.readyState === 'open') { _channel.send(message); } return; @@ -2920,7 +3157,7 @@ connection.rtcConfiguration for (var dataChannel in connection.channels) { var channel = connection.channels[dataChannel].channel; - if (channel.readyState == 'open') { + if (channel.readyState === 'open') { channel.send(message); } } @@ -2943,12 +3180,12 @@ connection.rtcConfiguration } if (e.socket) { - if (e.socket.userid != connection.userid) { + if (e.socket.userid !== connection.userid) { addStream(connection.peers[e.socket.userid]); } } else { for (var peer in connection.peers) { - if (peer != connection.userid) { + if (peer !== connection.userid) { addStream(connection.peers[peer]); } } @@ -2988,7 +3225,7 @@ connection.rtcConfiguration // because Firefox has no support of renegotiation yet; // so both chrome and firefox should redial instead of renegotiate! - if (isFirefox || _peer.userinfo.browser == 'firefox') { + if (isFirefox || _peer.userinfo.browser === 'firefox') { return _peer.redial(); } @@ -3024,8 +3261,13 @@ connection.rtcConfiguration }; function acceptRequest(response) { - if (!rtcMultiSession.requestsFrom) rtcMultiSession.requestsFrom = {}; - if (rtcMultiSession.requestsFrom[response.userid]) return; + if (!rtcMultiSession.requestsFrom) { + rtcMultiSession.requestsFrom = {}; + } + + if (rtcMultiSession.requestsFrom[response.userid]) { + return; + } var obj = { userid: response.userid, @@ -3062,7 +3304,9 @@ connection.rtcConfiguration // www.RTCMultiConnection.org/docs/onRequest/ if (connection.onRequest && connection.isInitiator) { connection.onRequest(obj); - } else _accept(obj); + } else { + _accept(obj); + } } function _accept(e) { @@ -3092,9 +3336,15 @@ connection.rtcConfiguration // 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]; + if (arguments[0]) { + e.userid = arguments[0]; + } + if (arguments[1]) { + e.extra = arguments[1]; + } + if (arguments[2]) { + e.channel = arguments[2]; + } } connection.captureUserMedia(function() { @@ -3143,13 +3393,245 @@ connection.rtcConfiguration 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 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); + } + + if (hints.video !== false) { + // 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; + } + + 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.video && !hints.video.mozMediaSource && hints.video.optional && hints.video.mandatory) { + if (!hints.video.optional.length && isEmpty(hints.video.mandatory)) { + hints.video = true; + } + } + } + + // 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.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 (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.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); + }); + } + } var RTCPeerConnection = window.mozRTCPeerConnection || window.webkitRTCPeerConnection; var RTCSessionDescription = window.mozRTCSessionDescription || window.RTCSessionDescription; var RTCIceCandidate = window.mozRTCIceCandidate || window.RTCIceCandidate; @@ -3166,16 +3648,18 @@ connection.rtcConfiguration this.attachMediaStreams(); if (isFirefox && this.session.data) { - if (this.session.data && type == 'offer') { + if (this.session.data && type === 'offer') { this.createDataChannel(); } this.getLocalDescription(type); - if (this.session.data && type == 'answer') { + if (this.session.data && type === 'answer') { this.createDataChannel(); } - } else self.getLocalDescription(type); + } else { + self.getLocalDescription(type); + } return this; }, @@ -3189,12 +3673,14 @@ connection.rtcConfiguration var self = this; - if (createType == 'answer') { + if (createType === 'answer') { this.setRemoteDescription(this.offerDescription, createDescription); - } else createDescription(); + } else { + createDescription(); + } function createDescription() { - self.connection[createType == 'offer' ? 'createOffer' : 'createAnswer'](function(sessionDescription) { + self.connection[createType === 'offer' ? 'createOffer' : 'createAnswer'](function(sessionDescription) { sessionDescription.sdp = self.serializeSdp(sessionDescription.sdp, createType); self.connection.setLocalDescription(sessionDescription); @@ -3202,7 +3688,7 @@ connection.rtcConfiguration self.onSessionDescription(sessionDescription, self.streaminfo); } - if (sessionDescription.type == 'offer') { + if (sessionDescription.type === 'offer') { log('offer sdp', sessionDescription.sdp); } @@ -3214,13 +3700,8 @@ connection.rtcConfiguration // it is "connection.processSdp=function(sdp){return sdp;}" sdp = this.processSdp(sdp); - if (isFirefox) return sdp; - - if (this.renegotiate) { - // sdp = sdp.replace(/a=rtpmap:.* rtx.*\r\n/gi, ''); - // sdp = sdp.replace(/a=fmtp:.* apt=.*\r\n/gi, ''); - // sdp = sdp.replace(/a=rtcp-fb.*\r\n/gi, ''); - // sdp = sdp.replace(/a=candidate:.*\r\n/gi, ''); + if (isFirefox) { + return sdp; } if (this.session.inactive && !this.holdMLine) { @@ -3235,7 +3716,7 @@ connection.rtcConfiguration } sdp = this.setBandwidth(sdp); - if (this.holdMLine == 'both') { + if (this.holdMLine === 'both') { if (this.hold) { this.prevSDP = sdp; sdp = sdp.replace(/a=sendonly|a=recvonly|a=sendrecv/g, 'a=inactive'); @@ -3251,27 +3732,27 @@ connection.rtcConfiguration } } } - } else if (this.holdMLine == 'audio' || this.holdMLine == 'video') { + } else if (this.holdMLine === 'audio' || this.holdMLine === 'video') { sdp = sdp.split('m='); var audio = ''; var video = ''; - if (sdp[1] && sdp[1].indexOf('audio') == 0) { + if (sdp[1] && sdp[1].indexOf('audio') === 0) { audio = 'm=' + sdp[1]; } - if (sdp[2] && sdp[2].indexOf('audio') == 0) { + if (sdp[2] && sdp[2].indexOf('audio') === 0) { audio = 'm=' + sdp[2]; } - if (sdp[1] && sdp[1].indexOf('video') == 0) { + if (sdp[1] && sdp[1].indexOf('video') === 0) { video = 'm=' + sdp[1]; } - if (sdp[2] && sdp[2].indexOf('video') == 0) { + if (sdp[2] && sdp[2].indexOf('video') === 0) { video = 'm=' + sdp[2]; } - if (this.holdMLine == 'audio') { + if (this.holdMLine === 'audio') { if (this.hold) { this.prevSDP = sdp[0] + audio + video; sdp = sdp[0] + audio.replace(/a=sendonly|a=recvonly|a=sendrecv/g, 'a=inactive') + video; @@ -3280,7 +3761,7 @@ connection.rtcConfiguration } } - if (this.holdMLine == 'video') { + if (this.holdMLine === 'video') { if (this.hold) { this.prevSDP = sdp[0] + audio + video; sdp = sdp[0] + audio + video.replace(/a=sendonly|a=recvonly|a=sendrecv/g, 'a=inactive'); @@ -3297,7 +3778,7 @@ connection.rtcConfiguration // From RFC 4145, SDP setup attribute values. // http://goo.gl/xETJEp && http://goo.gl/3Wgcau - if (createType == 'offer') { + if (createType === 'offer') { sdp = sdp.replace(/a=setup:passive|a=setup:active|a=setup:holdconn/g, 'a=setup:actpass'); } else { sdp = sdp.replace(/a=setup:actpass|a=setup:passive|a=setup:holdconn/g, 'a=setup:active'); @@ -3312,7 +3793,7 @@ connection.rtcConfiguration }, init: function() { this.setConstraints(); - this.connection = new RTCPeerConnection(this.iceServers, this.optionalArgument); + this.connection = new RTCPeerConnection(this.rtcConfiguration, this.optionalArgument); if (this.session.data) { log('invoked: createDataChannel'); @@ -3328,7 +3809,9 @@ connection.rtcConfiguration return; } - if (!self.trickleIce) return; + if (!self.trickleIce) { + return; + } self.onicecandidate(event.candidate); }; @@ -3337,7 +3820,7 @@ connection.rtcConfiguration if (self.returnedSDP) { self.returnedSDP = false; return; - }; + } self.returnedSDP = true; self.onSessionDescription(self.connection.localDescription, self.streaminfo); @@ -3354,7 +3837,10 @@ connection.rtcConfiguration }; this.connection.onsignalingstatechange = function() { - self.connection && self.oniceconnectionstatechange({ + if (!self.connection) { + return; + } + self.oniceconnectionstatechange({ iceConnectionState: self.connection.iceConnectionState, iceGatheringState: self.connection.iceGatheringState, signalingState: self.connection.signalingState @@ -3362,17 +3848,20 @@ connection.rtcConfiguration }; this.connection.oniceconnectionstatechange = function() { - if (!self.connection) return; - + if (!self.connection) { + return; + } self.oniceconnectionstatechange({ iceConnectionState: self.connection.iceConnectionState, iceGatheringState: self.connection.iceGatheringState, signalingState: self.connection.signalingState }); - if (self.trickleIce) return; + if (self.trickleIce) { + return; + } - if (self.connection.iceGatheringState == 'complete') { + if (self.connection.iceGatheringState === 'complete') { log('iceGatheringState', self.connection.iceGatheringState); returnSDP(); } @@ -3381,7 +3870,9 @@ connection.rtcConfiguration var self = this; }, setBandwidth: function(sdp) { - if (isMobileDevice || isFirefox || !this.bandwidth) return sdp; + if (isMobileDevice || isFirefox || !this.bandwidth) { + return sdp; + } var bandwidth = this.bandwidth; @@ -3429,13 +3920,7 @@ connection.rtcConfiguration } }; - if (this.constraints.mandatory) { - log('sdp-mandatory-constraints', toStr(this.constraints.mandatory)); - } - - if (this.constraints.optional) { - log('sdp-optional-constraints', toStr(this.constraints.optional)); - } + log('sdp-constraints', toStr(this.constraints)); this.optionalArgument = { optional: this.optionalArgument.optional || [], @@ -3450,15 +3935,19 @@ connection.rtcConfiguration log('optional-argument', toStr(this.optionalArgument)); - if (!isNull(this.iceServers)) { + if (!isNull(this.rtcConfiguration) && !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 (!isNull(iceCandidates.reflexive)) { + stun = iceCandidates.reflexive; + } + if (!isNull(iceCandidates.relay)) { + turn = iceCandidates.relay; + } if (!host && !stun && turn) { this.rtcConfiguration.iceTransports = 'relay'; @@ -3466,18 +3955,20 @@ connection.rtcConfiguration this.rtcConfiguration.iceTransports = 'none'; } - this.iceServers = { + this.rtcConfiguration = { iceServers: this.iceServers, iceTransports: this.rtcConfiguration.iceTransports }; - } else this.iceServers = null; + } else { + this.rtcConfiguration = null; + } - log('rtc-configuration', toStr(this.iceServers)); + log('rtc-configuration', toStr(this.rtcConfiguration)); }, onSdpError: function(e) { var message = toStr(e); - if (message && message.indexOf('RTP/SAVPF Expects at least 4 fields') != -1) { + 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); @@ -3489,9 +3980,13 @@ connection.rtcConfiguration error(toStr(err)); }, setRemoteDescription: function(sessionDescription, onSdpSuccess) { - if (!sessionDescription) throw 'Remote session description should NOT be NULL.'; + if (!sessionDescription) { + throw 'Remote session description should NOT be NULL.'; + } - if (!this.connection) return; + if (!this.connection) { + return; + } log('setting remote description', sessionDescription.type, sessionDescription.sdp); @@ -3500,7 +3995,7 @@ connection.rtcConfiguration new RTCSessionDescription(sessionDescription), onSdpSuccess || this.onSdpSuccess, function(error) { - if (error.search(/STATE_SENTINITIATE|STATE_INPROGRESS/gi) == -1) { + if (error.search(/STATE_SENTINITIATE|STATE_INPROGRESS/gi) === -1) { self.onSdpError(error); } } @@ -3509,10 +4004,12 @@ connection.rtcConfiguration addIceCandidate: function(candidate) { var self = this; if (isPluginRTC) { - RTCIceCandidate(candidate, function(iceCandidate) { + new RTCIceCandidate(candidate, function(iceCandidate) { onAddIceCandidate(iceCandidate); }); - } else onAddIceCandidate(new RTCIceCandidate(candidate)); + } else { + onAddIceCandidate(new RTCIceCandidate(candidate)); + } function onAddIceCandidate(iceCandidate) { self.connection.addIceCandidate(iceCandidate, function() { @@ -3524,17 +4021,23 @@ connection.rtcConfiguration }, createDataChannel: function(channelIdentifier) { // skip 2nd invocation of createDataChannel - if (this.channels && this.channels.length) return; + if (this.channels && this.channels.length) { + return; + } var self = this; - if (!this.channels) this.channels = []; + 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 (this.dataChannelDict) { + dataChannelDict = this.dataChannelDict; + } if (isChrome && !this.preferSCTP) { dataChannelDict.reliable = false; // Deprecated! @@ -3542,13 +4045,13 @@ connection.rtcConfiguration log('dataChannelDict', toStr(dataChannelDict)); - if (this.type == 'answer' || isFirefox) { + if (this.type === 'answer' || isFirefox) { this.connection.ondatachannel = function(event) { self.setChannelEvents(event.channel); }; } - if ((isChrome && this.type == 'offer') || isFirefox) { + if ((isChrome && this.type === 'offer') || isFirefox) { this.setChannelEvents( this.connection.createDataChannel(channelIdentifier || 'channel', dataChannelDict) ); @@ -3571,24 +4074,26 @@ connection.rtcConfiguration channel.onopen = function() { channel.push = channel.send; channel.send = function(data) { - if (self.connection.iceConnectionState == 'disconnected') { + if (self.connection.iceConnectionState === 'disconnected') { return; } - if (channel.readyState.search(/closing|closed/g) != -1) { + if (channel.readyState.search(/closing|closed/g) !== -1) { return; } - if (channel.readyState.search(/connecting|open/g) == -1) { + if (channel.readyState.search(/connecting|open/g) === -1) { return; } - if (channel.readyState == 'connecting') { + 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.'; + } else { + throw 'Number of times exceeded to wait for WebRTC data connection to be opened.'; + } }, 1000); } try { @@ -3596,7 +4101,9 @@ connection.rtcConfiguration } 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.'; + if (numberOfTimes >= 20) { + throw 'Number of times exceeded to resend data packets over WebRTC data channels.'; + } setTimeout(function() { channel.send(data); }, 100); @@ -3639,7 +4146,7 @@ connection.rtcConfiguration this.streaminfo = ''; var streams = this.connection.getLocalStreams(); for (var i = 0; i < streams.length; i++) { - if (i == 0) { + if (i === 0) { this.streaminfo = JSON.stringify({ streamid: streams[i].streamid || '', isScreen: !!streams[i].isScreen, @@ -3705,252 +4212,78 @@ connection.rtcConfiguration } }; } + var FileSaver = { + SaveToDisk: function(fileUrl, fileName) { + var hyperlink = document.createElement('a'); + hyperlink.href = fileUrl; + hyperlink.target = '_blank'; + hyperlink.download = fileName || fileUrl; - 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; - } + var mouseEvent = new MouseEvent('click', { + view: window, + bubbles: true, + cancelable: true + }); - // connection.mediaConstraints.video = false; - if (typeof videoConstraints == 'boolean' && hints.video) { - hints.video = videoConstraints; - } + hyperlink.dispatchEvent(mouseEvent); - // connection.mediaConstraints.audio.mandatory = {prop:true}; - var audioMandatoryConstraints = audioConstraints.mandatory; - if (!isEmpty(audioMandatoryConstraints)) { - hints.audio.mandatory = merge(hints.audio.mandatory, audioMandatoryConstraints); + // (window.URL || window.webkitURL).revokeObjectURL(hyperlink.href); } + }; - // 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)); - } + function TextReceiver(connection) { + var content = {}; - if (mandatory.minWidth >= 1280 && mandatory.minHeight >= 720) { - warn('Enjoy HD video! min/' + mandatory.minWidth + ':' + mandatory.minHeight + ', max/' + mandatory.maxWidth + ':' + mandatory.maxHeight); - } + function receive(data, userid, extra) { + // uuid is used to uniquely identify sending instance + var uuid = data.uuid; + if (!content[uuid]) { + content[uuid] = []; } - 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]; + content[uuid].push(data.message); + if (data.last) { + var message = content[uuid].join(''); + if (data.isobject) { + message = JSON.parse(message); } - }); - hints.video.optional = swap(hints.video.optional); + // latency detection + var receivingTime = new Date().getTime(); + var latency = receivingTime - data.sendingTime; - hints.video.optional.push({ - sourceId: connection._mediaSources.video - }); - } + var e = { + data: message, + userid: userid, + extra: extra, + latency: latency + }; - 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]; + 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); } - }); - - 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.webkitURL.createObjectURL(stream); - video.play(); + delete content[uuid]; } - - 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); - }); - } + return { + receive: receive + }; } - - var FileSaver = { - SaveToDisk: function(fileUrl, fileName) { - var hyperlink = document.createElement('a'); - hyperlink.href = fileUrl; - hyperlink.target = '_blank'; - hyperlink.download = fileName || fileUrl; - - var mouseEvent = new MouseEvent('click', { - view: window, - bubbles: true, - cancelable: true - }); - - hyperlink.dispatchEvent(mouseEvent); - - // (window.URL || window.webkitURL).revokeObjectURL(hyperlink.href); - } - }; - var TextSender = { send: function(config) { var connection = config.connection; @@ -3989,9 +4322,9 @@ connection.rtcConfiguration data.packets = parseInt(text.length / packetSize); } - if (text.length > packetSize) + if (text.length > packetSize) { data.message = text.slice(0, packetSize); - else { + } else { data.message = text; data.last = true; data.isobject = isobject; @@ -4009,59 +4342,12 @@ connection.rtcConfiguration } } }; - - 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 - }; - } - var webAudioMediaStreamSources = []; function convertToAudioStream(mediaStream) { - if (!mediaStream) throw 'MediaStream is mandatory.'; + if (!mediaStream) { + throw 'MediaStream is mandatory.'; + } if (mediaStream.getVideoTracks && !mediaStream.getVideoTracks().length) { return mediaStream; @@ -4089,14 +4375,14 @@ connection.rtcConfiguration 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']); + 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) { + if (window.crypto && window.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++) { @@ -4125,19 +4411,19 @@ connection.rtcConfiguration } function isNull(obj) { - return typeof obj == 'undefined'; + return typeof obj === 'undefined'; } function isString(obj) { - return typeof obj == 'string'; + return typeof obj === 'string'; } function isEmpty(session) { - var length = 0; - for (var s in session) { - length++; + var stringified = JSON.stringify(session); + if (stringified === '{}' || !stringified.split(',').length) { + return true; } - return length == 0; + return false; } // this method converts array-buffer into string @@ -4151,7 +4437,9 @@ connection.rtcConfiguration // this method converts string into array-buffer function str2ab(str) { - if (!isString(str)) str = JSON.stringify(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); @@ -4164,9 +4452,11 @@ connection.rtcConfiguration function swap(arr) { var swapped = [], length = arr.length; - for (var i = 0; i < length; i++) - if (arr[i] && arr[i] !== true) + for (var i = 0; i < length; i++) { + if (arr[i] && arr[i] !== true) { swapped.push(arr[i]); + } + } return swapped; } @@ -4182,38 +4472,46 @@ connection.rtcConfiguration warn: function() {} }; - function log() { + var log = function() { console.log(arguments); - } + }; - function error() { + var error = function() { console.error(arguments); - } + }; - function warn() { + var warn = function() { 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); + log = console.log.bind(console); + error = console.error.bind(console); + warn = console.warn.bind(console); } function toStr(obj) { return JSON.stringify(obj, function(key, value) { + key = key; if (value && value.sdp) { log(value.sdp.type, '\t', value.sdp.sdp); return ''; - } else return value; + } else { + return value; + } }, '\t'); } function getLength(obj) { - var length = 0; - for (var o in obj) - if (o) length++; - return length; + if (typeof obj !== 'object') { + throw 'Invalid data-type: ' + (typeof obj) + '; expected: object'; + } + + var stringified = JSON.stringify(obj); + if (stringified === '{}' || !stringified.split(',').length) { + return 0; + } + return stringified.split(',').length; } // Get HTMLAudioElement/HTMLVideoElement accordingly @@ -4227,7 +4525,7 @@ connection.rtcConfiguration body.insertBefore(mediaElement, body.firstChild); setTimeout(function() { - Plugin.attachMediaStream(mediaElement, stream) + Plugin.attachMediaStream(mediaElement, stream); }, 1000); return Plugin.attachMediaStream(mediaElement, stream); @@ -4242,9 +4540,11 @@ connection.rtcConfiguration // http://goo.gl/WZ5nFl // Firefox don't yet support onended for any stream (remote/local) - isFirefox && mediaElement.addEventListener('ended', function() { - stream.onended(); - }, false); + if (isFirefox) { + mediaElement.addEventListener('ended', function() { + stream.onended(); + }, false); + } mediaElement.play(); @@ -4254,9 +4554,13 @@ connection.rtcConfiguration var onStreamEndedHandlerFiredFor = {}; function onStreamEndedHandler(streamedObject, connection) { - if (streamedObject.mediaElement && !streamedObject.mediaElement.parentNode) return; + if (streamedObject.mediaElement && !streamedObject.mediaElement.parentNode) { + return; + } - if (onStreamEndedHandlerFiredFor[streamedObject.streamid]) return; + if (onStreamEndedHandlerFiredFor[streamedObject.streamid]) { + return; + } onStreamEndedHandlerFiredFor[streamedObject.streamid] = streamedObject; connection.onstreamended(streamedObject); } @@ -4264,9 +4568,13 @@ connection.rtcConfiguration var onLeaveHandlerFiredFor = {}; function onLeaveHandler(event, connection) { - if (onLeaveHandlerFiredFor[event.userid]) return; + if (onLeaveHandlerFiredFor[event.userid]) { + return; + } onLeaveHandlerFiredFor[event.userid] = event; connection.onleave(event); + + connection.numberOfConnectedUsers--; } function takeSnapshot(args) { @@ -4282,14 +4590,20 @@ connection.rtcConfiguration 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.callback) { + return; + } + args.callback(connection.snapshots[userid]); } - if (args.mediaElement) return _takeSnapshot(args.mediaElement); + 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) { + if (stream.userid === userid && stream.stream && stream.stream.getVideoTracks && stream.stream.getVideoTracks().length) { _takeSnapshot(stream.mediaElement); continue; } @@ -4306,8 +4620,13 @@ connection.rtcConfiguration } function merge(mergein, mergeto) { - if (!mergein) mergein = {}; - if (!mergeto) return mergein; + if (!mergein) { + mergein = {}; + } + + if (!mergeto) { + return mergein; + } for (var item in mergeto) { mergein[item] = mergeto[item]; @@ -4320,7 +4639,9 @@ connection.rtcConfiguration script.src = src; script.onload = function() { log('loaded resource:', src); - if (onload) onload(); + if (onload) { + onload(); + } }; document.documentElement.appendChild(script); } @@ -4337,14 +4658,18 @@ connection.rtcConfiguration if (isString(element)) { element = document.querySelector(element); - if (!element) element = document.getElementById(element); + if (!element) { + element = document.getElementById(element); + } + } + if (!element) { + throw 'HTML DOM Element is not accessible!'; } - 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, { + window.html2canvas(element, { onrendered: function(canvas) { args.callback(canvas.toDataURL()); } @@ -4364,7 +4689,7 @@ connection.rtcConfiguration return chunk; } - var fileBufferReader = new FileBufferReader(); + var fileBufferReader = new window.FileBufferReader(); fileBufferReader.onProgress = function(chunk) { connection.onFileProgress(_private(chunk), chunk.uuid); }; @@ -4383,12 +4708,17 @@ connection.rtcConfiguration var screenFrame, loadedScreenFrame; function loadScreenFrame(skip) { - if (DetectRTC.screen.extensionid != ReservedExtensionID) { + if (DetectRTC.screen.extensionid !== ReservedExtensionID) { return; } - if (loadedScreenFrame) return; - if (!skip) return loadScreenFrame(true); + if (loadedScreenFrame) { + return; + } + + if (!skip) { + return loadScreenFrame(true); + } loadedScreenFrame = true; @@ -4414,11 +4744,15 @@ connection.rtcConfiguration }; } - var iceFrame, loadedIceFrame; + var loadedIceFrame; function loadIceFrame(callback, skip) { - if (loadedIceFrame) return; - if (!skip) return loadIceFrame(callback, true); + if (loadedIceFrame) { + return; + } + if (!skip) { + return loadIceFrame(callback, true); + } loadedIceFrame = true; @@ -4429,7 +4763,9 @@ connection.rtcConfiguration listenEventHandler('message', iFrameLoaderCallback); function iFrameLoaderCallback(event) { - if (!event.data || !event.data.iceServers) return; + if (!event.data || !event.data.iceServers) { + return; + } callback(event.data.iceServers); // this event listener is no more needed @@ -4465,28 +4801,34 @@ connection.rtcConfiguration // implementation from #68 if (session.type) { - if (session.type == 'remote' && root.type != 'remote') return; - if (session.type == 'local' && root.type != 'local') return; + 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) { + if (root.type === 'local' && session.audio && !!stream.getAudioTracks) { var audioTracks = stream.getAudioTracks()[0]; - if (audioTracks) + if (audioTracks) { audioTracks.enabled = !enabled; + } } - if (root.type == 'local' && (session.video || session.screen) && !!stream.getVideoTracks) { + if (root.type === 'local' && (session.video || session.screen) && !!stream.getVideoTracks) { var videoTracks = stream.getVideoTracks()[0]; - if (videoTracks) + if (videoTracks) { videoTracks.enabled = !enabled; + } } root.sockets.forEach(function(socket) { - if (root.type == 'local') { + if (root.type === 'local') { socket.send({ streamid: root.streamid, mute: !!enabled, @@ -4495,7 +4837,7 @@ connection.rtcConfiguration }); } - if (root.type == 'remote') { + if (root.type === 'remote') { socket.send({ promptMuteUnmute: true, streamid: root.streamid, @@ -4506,42 +4848,119 @@ connection.rtcConfiguration } }); - if (root.type == 'remote') return; + 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 firefoxScreenCapturingWarning = '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; + + var MediaStreamTrack = window.MediaStreamTrack; + + // 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); + } + + 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; - // 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; + var options = {}; + var speechEvents = window.hark(stream, options); - fakeObject.isAudio = !!fakeObject.session.audio && !fakeObject.session.video; - fakeObject.isVideo = !!fakeObject.session.video; - fakeObject.isScreen = !!fakeObject.session.screen; + speechEvents.on('speaking', function() { + if (connection.onspeaking) { + connection.onspeaking(streamedObject); + } + }); - 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); - } + speechEvents.on('stopped_speaking', function() { + if (connection.onsilence) { + connection.onsilence(streamedObject); + } + }); - if (!enabled) { - stream.preMuted = {}; - root.rtcMultiConnection.onunmute(fakeObject); - } + speechEvents.on('volume_change', function(volume, threshold) { + if (connection.onvolumechange) { + connection.onvolumechange(merge({ + volume: volume, + threshold: threshold + }, streamedObject)); + } + }); } - 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'; + window.attachEventListener = function(video, type, listener, useCapture) { + video.addEventListener(type, listener, useCapture); + }; - // if application-developer deployed his own extension on Google App Store - var useCustomChromeExtensionForScreenCapturing = document.domain.indexOf('webrtc-experiment.com') != -1; + var Plugin = window.PluginRTC || {}; + window.onPluginRTCInitialized = function(pluginRTCObject) { + Plugin = pluginRTCObject; + MediaStreamTrack = Plugin.MediaStreamTrack; + RTCPeerConnection = Plugin.RTCPeerConnection; + RTCIceCandidate = Plugin.RTCIceCandidate; + RTCSessionDescription = Plugin.RTCSessionDescription; - var MediaStreamTrack = window.MediaStreamTrack; + 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'); + } // http://goo.gl/6ScAV9 var DetectRTC = {}; @@ -4559,8 +4978,8 @@ connection.rtcConfiguration // This method is useful only for Chrome! // Firefox seems having no support of enumerateDevices feature yet. - if (isPluginRTC || isFirefox) { - callback && callback(); + if ((isPluginRTC || isFirefox) && callback) { + callback(); return; } @@ -4579,7 +4998,9 @@ connection.rtcConfiguration DetectRTC.hasWebcam = true; } - callback && callback(); + if (callback) { + callback(); + } return; } @@ -4587,11 +5008,11 @@ connection.rtcConfiguration DetectRTC.MediaDevices = []; devices.forEach(function(device) { // if it is MediaStreamTrack.getSources - if (device.kind == 'audio') { + if (device.kind === 'audio') { device.kind = 'audioinput'; } - if (device.kind == 'video') { + if (device.kind === 'video') { device.kind = 'videoinput'; } @@ -4605,33 +5026,35 @@ connection.rtcConfiguration DetectRTC.MediaDevices.push(device); - if (device.kind == 'audioinput' || device.kind == 'audio') { + if (device.kind === 'audioinput' || device.kind === 'audio') { DetectRTC.hasMicrophone = true; } - if (device.kind == 'audiooutput') { + if (device.kind === 'audiooutput') { DetectRTC.hasSpeakers = true; } - if (device.kind == 'videoinput' || device.kind == 'video') { + if (device.kind === 'videoinput' || device.kind === 'video') { DetectRTC.hasWebcam = true; } // there is no "videoouput" in the spec. }); - if (callback) callback(); + if (callback) { + callback(); + } }); } DetectRTC.isWebRTCSupported = !!window.webkitRTCPeerConnection || !!window.mozRTCPeerConnection; DetectRTC.isAudioContextSupported = (!!window.AudioContext || !!window.webkitAudioContext) && !!AudioContext.prototype.createMediaStreamSource; - DetectRTC.isScreenCapturingSupported = isChrome && chromeVersion >= 26 && (isNodeWebkit ? true : location.protocol == 'https:'); + DetectRTC.isScreenCapturingSupported = isChrome && chromeVersion >= 26 && (isNodeWebkit ? true : location.protocol === 'https:'); DetectRTC.isSctpDataChannelsSupported = !!navigator.mozGetUserMedia || (isChrome && chromeVersion >= 25); DetectRTC.isRtpDataChannelsSupported = isChrome && chromeVersion >= 31; // check for microphone/camera support! - CheckDeviceSupport(); + new CheckDeviceSupport(); DetectRTC.load = CheckDeviceSupport; var screenCallback; @@ -4640,15 +5063,19 @@ connection.rtcConfiguration chromeMediaSource: 'screen', extensionid: ReservedExtensionID, getSourceId: function(callback) { - if (!callback) throw '"callback" parameter is mandatory.'; + 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); + } else { + DetectRTC.screen.getChromeExtensionStatus(onstatus); + } function onstatus(status) { - if (status == 'installed-enabled') { + if (status === 'installed-enabled') { screenCallback = callback; window.postMessage('get-sourceId', '*'); return; @@ -4659,19 +5086,24 @@ connection.rtcConfiguration } }, onMessageCallback: function(data) { - if (!(isString(data) || !!data.sourceId)) return; + if (!(isString(data) || !!data.sourceId)) { + return; + } log('chrome message', data); // "cancel" button is clicked - if (data == 'PermissionDeniedError') { + if (data === 'PermissionDeniedError') { DetectRTC.screen.chromeMediaSource = 'PermissionDeniedError'; - if (screenCallback) return screenCallback('PermissionDeniedError'); - else throw new Error('PermissionDeniedError'); + if (screenCallback) { + return screenCallback('PermissionDeniedError'); + } else { + throw new Error('PermissionDeniedError'); + } } // extension notified his presence - if (data == 'rtcmulticonnection-extension-loaded') { + if (data === 'rtcmulticonnection-extension-loaded') { DetectRTC.screen.chromeMediaSource = 'desktop'; if (DetectRTC.screen.onScreenCapturingExtensionAvailable) { DetectRTC.screen.onScreenCapturingExtensionAvailable(); @@ -4684,7 +5116,9 @@ connection.rtcConfiguration // extension shared temp sourceId if (data.sourceId) { DetectRTC.screen.sourceId = data.sourceId; - if (screenCallback) screenCallback(DetectRTC.screen.sourceId); + if (screenCallback) { + screenCallback(DetectRTC.screen.sourceId); + } } }, getChromeExtensionStatus: function(extensionid, callback) { @@ -4693,9 +5127,11 @@ connection.rtcConfiguration callback(status); } - if (isFirefox) return _callback('not-chrome'); + if (isFirefox) { + return _callback('not-chrome'); + } - if (arguments.length != 2) { + if (arguments.length !== 2) { callback = extensionid; extensionid = this.extensionid; } @@ -4706,11 +5142,13 @@ connection.rtcConfiguration DetectRTC.screen.chromeMediaSource = 'screen'; window.postMessage('are-you-there', '*'); setTimeout(function() { - if (DetectRTC.screen.chromeMediaSource == 'screen') { + 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. */ + 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'); + } else { + _callback('installed-enabled'); + } }, 2000); }; image.onerror = function() { @@ -4720,86 +5158,14 @@ connection.rtcConfiguration }; })(); - // 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) { + if (event.origin !== window.location.origin) { return; } DetectRTC.screen.onMessageCallback(event.data); }); - 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'); - } - function setDefaults(connection) { // www.RTCMultiConnection.org/docs/userid/ connection.userid = getRandomString(); @@ -4865,7 +5231,7 @@ connection.rtcConfiguration // www.RTCMultiConnection.org/docs/fakeDataChannels/ connection.fakeDataChannels = false; - connection.waitUntilRemoteStreamStartsFlowing = null; // NULL == true + connection.waitUntilRemoteStreamStartsFlowing = null; // NULL === true // auto leave on page unload connection.leaveOnPageUnload = true; @@ -4969,7 +5335,7 @@ connection.rtcConfiguration // this feature added to keep users privacy and // make sure HTTPs pages NEVER auto capture users media - // isChrome && location.protocol == 'https:' + // isChrome && location.protocol === 'https:' connection.preventSSLAutoAllowed = false; connection.autoReDialOnFailure = true; @@ -5075,24 +5441,36 @@ connection.rtcConfiguration // www.RTCMultiConnection.org/docs/onFileProgress/ connection.onFileProgress = function(chunk) { var helper = progressHelper[chunk.uuid]; - if (!helper) return; + 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 + ''; + 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); + 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; + if (progress.position === -1) { + return; + } + var position = +progress.position.toFixed(2).split('.')[1] || 100; label.innerHTML = position + '%'; } @@ -5128,7 +5506,9 @@ connection.rtcConfiguration connection.onSessionClosed = function(session) { if (session.isEjected) { warn(session.userid, 'ejected you.'); - } else warn('Session has been closed.', session); + } else { + warn('Session has been closed.', session); + } }; // www.RTCMultiConnection.org/docs/onmute/ @@ -5183,30 +5563,45 @@ connection.rtcConfiguration 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.screen && !stream.isScreen) { + return; + } + if (session.audio && !stream.isAudio) { + return; + } + if (session.video && !stream.isVideo) { + return; } - function _muteOrUnMute(stream, session, isMute) { - if (session.local && stream.type != 'local') return; - if (session.remote && stream.type != 'remote') return; + if (isMute) { + stream.mute(session); + } else { + stream.unmute(session); + } + } - if (session.isScreen && !stream.isScreen) return; - if (session.isAudio && !stream.isAudio) return; - if (session.isVideo && !stream.isVideo) return; + var stream; - if (isMute) stream.mute(session); - else stream.unmute(session); + if (session && !isString(session)) { + for (stream in this) { + if (connection._skip.indexOf(stream) === -1) { + _muteOrUnMute(this[stream], session, enabled); + } } return; } // implementation from #68 - for (var stream in this) { - if (connection._skip.indexOf(stream) == -1) { + for (stream in this) { + if (connection._skip.indexOf(stream) === -1) { this[stream]._private(session, enabled); } } @@ -5214,26 +5609,34 @@ connection.rtcConfiguration stop: function(type) { var _stream; for (var stream in this) { - if (connection._skip.indexOf(stream) == -1) { + if (connection._skip.indexOf(stream) === -1) { _stream = this[stream]; - if (!type) _stream.stop(); - - else if (isString(type)) { + if (!type) { + _stream.stop(); + } else if (isString(type)) { // connection.streams.stop('screen'); var config = {}; config[type] = true; _stopStream(_stream, config); - } else _stopStream(_stream, type); + } 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.userid && _stream.userid !== config.userid) { + return; + } - if (config.local && _stream.type != 'local') return; - if (config.remote && _stream.type != 'remote') return; + if (config.local && _stream.type !== 'local') { + return; + } + if (config.remote && _stream.type !== 'remote') { + return; + } if (config.screen && !!_stream.isScreen) { _stream.stop(); @@ -5256,29 +5659,37 @@ connection.rtcConfiguration remove: function(type) { var _stream; for (var stream in this) { - if (connection._skip.indexOf(stream) == -1) { + if (connection._skip.indexOf(stream) === -1) { _stream = this[stream]; - if (!type) _stopAndRemoveStream(_stream, { - local: true, - remote: true - }); - - else if (isString(type)) { + 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); + } 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.userid && _stream.userid !== config.userid) { + return; + } - if (config.local && _stream.type != 'local') return; - if (config.remote && _stream.type != 'remote') return; + if (config.local && _stream.type !== 'local') { + return; + } + if (config.remote && _stream.type !== 'remote') { + return; + } if (config.screen && !!_stream.isScreen) { endStream(_stream); @@ -5310,7 +5721,9 @@ connection.rtcConfiguration return this._selectStreams(args, true); }, _selectStreams: function(args, all) { - if (!args || isString(args) || isEmpty(args)) throw 'Invalid arguments.'; + 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)) { @@ -5323,7 +5736,7 @@ connection.rtcConfiguration 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 (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); } @@ -5375,7 +5788,9 @@ connection.rtcConfiguration // www.RTCMultiConnection.org/docs/media/ connection.media = { min: function(width, height) { - if (!connection.mediaConstraints.video) return; + if (!connection.mediaConstraints.video) { + return; + } if (!connection.mediaConstraints.video.mandatory) { connection.mediaConstraints.video.mandatory = {}; @@ -5384,7 +5799,9 @@ connection.rtcConfiguration connection.mediaConstraints.video.mandatory.minHeight = height; }, max: function(width, height) { - if (!connection.mediaConstraints.video) return; + if (!connection.mediaConstraints.video) { + return; + } if (!connection.mediaConstraints.video.mandatory) { connection.mediaConstraints.video.mandatory = {}; @@ -5404,14 +5821,14 @@ connection.rtcConfiguration var self = this; self.sockets.forEach(function(socket) { - if (self.type == 'local') { + if (self.type === 'local') { socket.send({ streamid: self.streamid, stopped: true }); } - if (self.type == 'remote') { + if (self.type === 'remote') { socket.send({ promptStreamStop: true, streamid: self.streamid @@ -5419,10 +5836,14 @@ connection.rtcConfiguration } }); - if (self.type == 'remote') return; + if (self.type === 'remote') { + return; + } var stream = self.stream; - if (stream) self.rtcMultiConnection.stopMediaStream(stream); + if (stream) { + self.rtcMultiConnection.stopMediaStream(stream); + } }; resultingObject.mute = function(session) { @@ -5435,22 +5856,28 @@ connection.rtcConfiguration this._private(session, false); }; - function muteOrUnmuteLocally(session, isPause, mediaElement) { - if (!mediaElement) return; + function muteOrUnmuteLocally(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(); + 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); + if (session && !isNull(session.sync) && session.sync === false) { + muteOrUnmuteLocally(enabled, this.mediaElement); return; } @@ -5474,8 +5901,8 @@ connection.rtcConfiguration if (isString(session)) { session = { - audio: session == 'audio', - video: session == 'video' + audio: session === 'audio', + video: session === 'video' }; } @@ -5492,24 +5919,24 @@ connection.rtcConfiguration if (isFirefox) { // firefox supports both audio/video recording in single webm file if (session.video) { - self.videoRecorder = RecordRTC(self.stream, { + self.videoRecorder = new window.RecordRTC(self.stream, { type: 'video' }); } else if (session.audio) { - self.audioRecorder = RecordRTC(self.stream, { + self.audioRecorder = new window.RecordRTC(self.stream, { type: 'audio' }); } } else if (isChrome) { // chrome supports recording in two separate files: WAV and WebM if (session.video) { - self.videoRecorder = RecordRTC(self.stream, { + self.videoRecorder = new window.RecordRTC(self.stream, { type: 'video' }); } if (session.audio) { - self.audioRecorder = RecordRTC(self.stream, { + self.audioRecorder = new window.RecordRTC(self.stream, { type: 'audio' }); } @@ -5519,7 +5946,9 @@ connection.rtcConfiguration self.audioRecorder.startRecording(); } - if (self.videoRecorder) self.videoRecorder.startRecording(); + if (self.videoRecorder) { + self.videoRecorder.startRecording(); + } }; resultingObject.stopRecording = function(callback, session) { @@ -5532,8 +5961,8 @@ connection.rtcConfiguration if (isString(session)) { session = { - audio: session == 'audio', - video: session == 'video' + audio: session === 'audio', + video: session === 'video' }; } @@ -5550,9 +5979,11 @@ connection.rtcConfiguration video: self.videoRecorder.getBlob() }); }); - } else callback({ - audio: self.audioRecorder.getBlob() - }); + } else { + callback({ + audio: self.audioRecorder.getBlob() + }); + } }); } else if (session.video && self.videoRecorder) { self.videoRecorder.stopRecording(function() { @@ -5604,17 +6035,27 @@ connection.rtcConfiguration }; 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); + 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]); + if (device1) { + select(this.devices[device1]); + } + + if (device2) { + select(this.devices[device2]); + } function select(device) { - if (!device) return; + if (!device) { + return; + } connection._mediaSources[device.kind] = device.id; } }; @@ -5633,11 +6074,15 @@ connection.rtcConfiguration connection.devices[device.deviceId] = device; }); - if (callback) callback(connection.devices); + if (callback) { + callback(connection.devices); + } }; connection.getMediaDevices = connection.enumerateDevices = function(callback) { - if (!callback) throw 'callback is mandatory.'; + if (!callback) { + throw 'callback is mandatory.'; + } connection.getDevices(function() { callback(connection.DetectRTC.MediaDevices); }); @@ -5660,12 +6105,14 @@ connection.rtcConfiguration // "drop" should detach all local streams for (var stream in connection.streams) { - if (connection._skip.indexOf(stream) == -1) { + if (connection._skip.indexOf(stream) === -1) { stream = connection.streams[stream]; - if (stream.type == 'local') { + if (stream.type === 'local') { connection.detachStreams.push(stream.streamid); onStreamEndedHandler(stream, connection); - } else onStreamEndedHandler(stream, connection); + } else { + onStreamEndedHandler(stream, connection); + } } } @@ -5692,7 +6139,7 @@ connection.rtcConfiguration callback(response.data.translations[0].translatedText); } - if (response.error && response.error.message == 'Daily Limit Exceeded') { + if (response.error && response.error.message === 'Daily Limit Exceeded') { warn('Text translation failed. Error message: "Daily Limit Exceeded."'); // returning original text @@ -5723,20 +6170,27 @@ connection.rtcConfiguration 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); + if (!!volumeChangeEventFired) { + return; + } + + volumeChangeEventFired = true; + + if (!connection.streams[streamid]) { + return; } + + 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); }; }; @@ -5751,14 +6205,14 @@ connection.rtcConfiguration connection.shareMediaFile = function(file, video, streamerid) { streamerid = streamerid || connection.token(); - if (!PreRecordedMediaStreamer) { + if (!window.PreRecordedMediaStreamer) { loadScript(connection.resources.PreRecordedMediaStreamer, function() { connection.shareMediaFile(file, video, streamerid); }); return streamerid; } - return PreRecordedMediaStreamer.shareMediaFile({ + return window.PreRecordedMediaStreamer.shareMediaFile({ file: file, video: video, streamerid: streamerid, @@ -5788,11 +6242,11 @@ connection.rtcConfiguration connection.onhold = function(track) { log('onhold', track); - if (track.kind != 'audio') { + if (track.kind !== 'audio') { track.mediaElement.pause(); track.mediaElement.setAttribute('poster', track.screenshot || connection.resources.muted); } - if (track.kind == 'audio') { + if (track.kind === 'audio') { track.mediaElement.muted = true; } }; @@ -5808,11 +6262,11 @@ connection.rtcConfiguration connection.onunhold = function(track) { log('onunhold', track); - if (track.kind != 'audio') { + if (track.kind !== 'audio') { track.mediaElement.play(); track.mediaElement.removeAttribute('poster'); } - if (track.kind != 'audio') { + if (track.kind !== 'audio') { track.mediaElement.muted = false; } }; @@ -5831,7 +6285,7 @@ connection.rtcConfiguration connection: connection, callback: function(screenshot) { // don't share repeated content - if (screenshot != lastScreenshot) { + if (screenshot !== lastScreenshot) { lastScreenshot = screenshot; for (var channel in connection.channels) { @@ -5843,7 +6297,10 @@ connection.rtcConfiguration } // "once" can be used to share single screenshot - !args.once && setTimeout(partOfScreenCapturer, args.interval || 200); + if (!!args.once) { + return; + } + setTimeout(partOfScreenCapturer, args.interval || 200); } }); } @@ -5886,7 +6343,9 @@ connection.rtcConfiguration }; connection.takeScreenshot = function(element, callback) { - if (!element || !callback) throw 'Invalid number of arguments.'; + if (!element || !callback) { + throw 'Invalid number of arguments.'; + } if (!window.html2canvas) { return loadScript(connection.resources.html2canvas, function() { @@ -5896,12 +6355,16 @@ connection.rtcConfiguration if (isString(element)) { element = document.querySelector(element); - if (!element) element = document.getElementById(element); + if (!element) { + element = document.getElementById(element); + } + } + if (!element) { + throw 'HTML Element is inaccessible!'; } - if (!element) throw 'HTML Element is inaccessible!'; // html2canvas.js is used to take screenshots - html2canvas(element, { + window.html2canvas(element, { onrendered: function(canvas) { callback(canvas.toDataURL()); } @@ -5935,18 +6398,24 @@ connection.rtcConfiguration }; connection.onfailed = function(event) { - if (!event.peer.numOfRetries) event.peer.numOfRetries = 0; + if (!event.peer.numOfRetries) { + event.peer.numOfRetries = 0; + } + event.peer.numOfRetries++; - if (isFirefox || event.targetuser.browser == 'firefox') { + if (event.peer.numOfRetries > 2) { + event.peer.numOfRetries = 0; + return; + } + + if (isFirefox || event.targetuser.browser === 'firefox') { error('ICE connectivity check is failed. Re-establishing peer connection.'); - event.peer.numOfRetries < 2 && event.peer.redial(); + event.peer.redial(); } else { error('ICE connectivity check is failed. Renegotiating peer connection.'); - event.peer.numOfRetries < 2 && event.peer.renegotiate(); + event.peer.renegotiate(); } - - if (event.peer.numOfRetries >= 2) event.peer.numOfRetries = 0; }; connection.onconnected = function(event) { @@ -5957,8 +6426,12 @@ connection.rtcConfiguration 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; + 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({ @@ -5967,7 +6440,7 @@ connection.rtcConfiguration // wait 5 seconds, if target peer didn't response, simply disconnect setTimeout(function() { - // iceConnectionState == 'disconnected' occurred out of low-bandwidth + // iceConnectionState === 'disconnected' occurred out of low-bandwidth // or internet connectivity issues if (connection.peers[event.userid].connected) { delete connection.peers[event.userid].connected; @@ -5990,10 +6463,14 @@ connection.rtcConfiguration }; connection.stopMediaStream = function(mediaStream) { - if (!mediaStream) throw 'MediaStream argument is mandatory.'; + if (!mediaStream) { + throw 'MediaStream argument is mandatory.'; + } if (connection.keepStreamsOpened) { - if (mediaStream.onended) mediaStream.onended(); + if (mediaStream.onended) { + mediaStream.onended(); + } return; } @@ -6005,7 +6482,9 @@ connection.rtcConfiguration if (isFirefox) { // Firefox don't yet support onended for any stream (remote/local) - if (mediaStream.onended) mediaStream.onended(); + if (mediaStream.onended) { + mediaStream.onended(); + } } // Latest firefox does support mediaStream.getAudioTrack but doesn't support stop on MediaStreamTrack @@ -6065,7 +6544,7 @@ connection.rtcConfiguration connection.resources.firebaseio = connection.resources.firebaseio.replace('//chat.', '//' + connection.firebase + '.'); } - var firebase = new Firebase(connection.resources.firebaseio + channel); + var firebase = new window.Firebase(connection.resources.firebaseio + channel); firebase.channel = channel; firebase.on('child_added', function(data) { config.onmessage(data.val()); @@ -6075,7 +6554,7 @@ connection.rtcConfiguration // 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') { + if (isNull(data[prop]) || typeof data[prop] === 'function') { data[prop] = false; } } @@ -6083,8 +6562,9 @@ connection.rtcConfiguration this.push(data); }; - if (!connection.socket) + if (!connection.socket) { connection.socket = firebase; + } firebase.onDisconnect().remove(); diff --git a/RTCMultiConnection/RTCMultiConnection.min.js b/RTCMultiConnection/RTCMultiConnection.min.js new file mode 100644 index 00000000..f77c5163 --- /dev/null +++ b/RTCMultiConnection/RTCMultiConnection.min.js @@ -0,0 +1,4 @@ +"use strict";!function(){function RTCMultiSession(connection,callbackForSignalingReady){function onDataChannelMessage(e){return e.data.checkingPresence&&connection.channels[e.userid]?void connection.channels[e.userid].send({presenceDetected:!0}):e.data.presenceDetected&&connection.peers[e.userid]?void(connection.peers[e.userid].connected=!0):void("text"===e.data.type?textReceiver.receive(e.data,e.userid,e.extra):connection.autoTranslateText?(e.original=e.data,connection.Translator.TranslateText(e.data,function(translatedText){e.data=translatedText,connection.onmessage(e)})):connection.onmessage(e))}function onNewSession(session){return connection.skipOnNewSession||(session.session||(session.session={}),session.extra||(session.extra={}),connection.sessionid&&session.sessionid!==connection.sessionid)?void 0:connection.onNewSession?(session.join=function(forceSession){if(!forceSession)return connection.join(session);for(var f in forceSession)session.session[f]=forceSession[f];var isDontCaptureUserMedia=connection.dontCaptureUserMedia;connection.dontCaptureUserMedia=!1,connection.captureUserMedia(function(){connection.dontCaptureUserMedia=!0,connection.join(session),connection.dontCaptureUserMedia=isDontCaptureUserMedia},forceSession)},session.extra||(session.extra={}),connection.onNewSession(session)):void connection.join(session)}function updateSocketForLocalStreams(socket){for(var i=0;i=60?socket.send({failedToReceiveRemoteVideo:!0,streamid:args.stream.streamid}):void setTimeout(function(){log("Waiting for incoming remote stream to be started flowing: "+args.numberOfTimes+" seconds."),waitUntilRemoteStreamStartsFlowing(args)},900):afterRemoteStreamStartedFlowing(args))}function initFakeChannel(){if(connection.fakeDataChannels&&!connection.channels[_config.userid]&&!connection.session.data){var fakeChannel={send:function(data){socket.send({fakeData:data})},readyState:"open"};connection.channels[_config.userid]={channel:fakeChannel,send:function(data){this.channel.send(data)}},peerConfig.onopen(fakeChannel)}}function afterRemoteStreamStartedFlowing(args){var mediaElement=args.mediaElement,session=args.session,stream=args.stream;stream.onended=function(){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};if(connection.streams[stream.streamid]=connection._getStream(streamedObject),connection.onstream(streamedObject),!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=!1,connection.onmute(fakeObject)}log("on:add:stream",streamedObject),onSessionOpened(),connection.onspeaking&&initHark({stream:stream,streamedObject:streamedObject,connection:connection})}function onChannelOpened(channel){_config.channel=channel,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});for(var q in connection.fileQueue)connection.send(connection.fileQueue[q],channel);isData(connection.session)&&onSessionOpened(),connection.partOfScreen&&connection.partOfScreen.sharing&&connection.peers[_config.userid].sharePartOfScreen(connection.partOfScreen)}function updateSocket(){socket.userid!==_config.userid&&(socket.userid=_config.userid,sockets[_config.socketIndex]=socket,connection.numberOfConnectedUsers++,connection.peers[_config.userid]={socket:socket,peer:peer,userid:_config.userid,extra:_config.extra,userinfo:_config.userinfo,addStream:function(session00){connection.addStream(session00,this.socket)},removeStream:function(streamid){return connection.streams[streamid]?(this.peer.connection.removeStream(connection.streams[streamid].stream),void this.renegotiate()):warn("No such stream exists. Stream-id:",streamid)},renegotiate:function(stream,session){connection.renegotiate(stream,session)},changeBandwidth:function(bandwidth){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}";this.peer.bandwidth=bandwidth,this.socket.send({changeBandwidth:!0,bandwidth:bandwidth})},sendCustomMessage:function(message){this.socket.send({customMessage:!0,message:message})},onCustomMessage:function(message){log('Received "private" message from',this.userid,isString(message)?message:toStr(message))},drop:function(dontSendMessage){for(var stream in connection.streams)-1===connection._skip.indexOf(stream)&&(stream=connection.streams[stream],stream.userid===connection.userid&&"local"===stream.type&&(this.peer.connection.removeStream(stream.stream),onStreamEndedHandler(stream,connection)),"remote"===stream.type&&stream.userid===this.userid&&onStreamEndedHandler(stream,connection));dontSendMessage||this.socket.send({drop:!0})},hold:function(holdMLine){return"answer"===peer.prevCreateType?void this.socket.send({unhold:!0,holdMLine:holdMLine||"both",takeAction:!0}):(this.socket.send({hold:!0,holdMLine:holdMLine||"both"}),this.peer.hold=!0,void this.fireHoldUnHoldEvents({kind:holdMLine,isHold:!0,userid:connection.userid,remoteUser:this.userid}))},unhold:function(holdMLine){return"answer"===peer.prevCreateType?void this.socket.send({unhold:!0,holdMLine:holdMLine||"both",takeAction:!0}):(this.socket.send({unhold:!0,holdMLine:holdMLine||"both"}),this.peer.hold=!1,void this.fireHoldUnHoldEvents({kind:holdMLine,isHold:!1,userid:connection.userid,remoteUser:this.userid}))},fireHoldUnHoldEvents:function(e){var isHold=e.isHold,kind=e.kind,userid=e.remoteUser||e.userid;for(var stream in connection.streams)-1===connection._skip.indexOf(stream)&&(stream=connection.streams[stream],stream.userid===userid&&(isHold&&connection.onhold(merge({kind:kind},stream)),isHold||connection.onunhold(merge({kind:kind},stream))))},redial:function(){for(var stream in connection.streams)-1===connection._skip.indexOf(stream)&&(stream=connection.streams[stream],stream.userid===this.userid&&"remote"===stream.type&&onStreamEndedHandler(stream,connection));log("ReDialing..."),socket.send({recreatePeer:!0}),peer=new PeerConnection,peer.create("offer",peerConfig)},sharePartOfScreen:function(args){function partOfScreenCapturer(){return that.stopPartOfScreenSharing?(that.stopPartOfScreenSharing=!1,void(connection.onpartofscreenstopped&&connection.onpartofscreenstopped())):that.pausePartOfScreenSharing?(connection.onpartofscreenpaused&&connection.onpartofscreenpaused(),setTimeout(partOfScreenCapturer,args.interval||200)):void capturePartOfScreen({element:args.element,connection:connection,callback:function(screenshot){if(!connection.channels[that.userid])throw"No such data channel exists.";screenshot!==lastScreenshot&&(lastScreenshot=screenshot,connection.channels[that.userid].send({screenshot:screenshot,isPartOfScreen:!0})),args.once||setTimeout(partOfScreenCapturer,args.interval||200)}})}var that=this,lastScreenshot="";partOfScreenCapturer()},getConnectionStats:function(callback,interval){function invoker(){RTCPeerConnection.prototype.getConnectionStats=window.getConnectionStats,peer.connection&&peer.connection.getConnectionStats(callback,interval)}if(!callback)throw"callback is mandatory.";window.getConnectionStats?invoker():loadScript(connection.resources.getConnectionStats,invoker)},takeSnapshot:function(callback){takeSnapshot({userid:this.userid,connection:connection,callback:callback})}})}function onSessionOpened(){if(connection.isInitiator&&getLength(participants)&&getLength(participants)<=connection.maxParticipantsAllowed&&(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}})),"chrome"===_config.userinfo.browser&&!_config.renegotiatedOnce)for(var rSession in connection.renegotiatedSessions)_config.renegotiatedOnce=!0,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&&response.userid!==connection.userid){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);"offer"===sdp.type&&(peerConfig.preferSCTP=!!response.preferSCTP,connection.fakeDataChannels=!!response.fakeDataChannels),initFakeChannel(),sdpInvoker(sdp,response.labels)}if(response.candidate&&peer&&peer.addIceCandidate(JSON.parse(response.candidate)),response.streamid&&(rtcMultiSession.streamids||(rtcMultiSession.streamids={}),rtcMultiSession.streamids[response.streamid]||(rtcMultiSession.streamids[response.streamid]=response.streamid,connection.onstreamid(response))),response.mute||response.unmute)if(response.promptMuteUnmute){if(!connection.privileges.canMuteRemoteStream)return void 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".'});connection.streams[response.streamid]&&(response.mute&&!connection.streams[response.streamid].muted&&connection.streams[response.streamid].mute(response.session),response.unmute&&connection.streams[response.streamid].muted&&connection.streams[response.streamid].unmute(response.session))}else{var streamObject={};connection.streams[response.streamid]&&(streamObject=connection.streams[response.streamid]);var session=response.session,fakeObject=merge({},streamObject);fakeObject.session=session,fakeObject.isAudio=!!fakeObject.session.audio&&!fakeObject.session.video,fakeObject.isVideo=!!fakeObject.session.video,fakeObject.isScreen=!!fakeObject.session.screen,response.mute&&connection.onmute(fakeObject||response),response.unmute&&connection.onunmute(fakeObject||response)}if(response.isVolumeChanged&&(log("Volume of stream: "+response.streamid+" has changed to: "+response.volume),connection.streams[response.streamid])){var mediaElement=connection.streams[response.streamid].mediaElement;mediaElement&&(mediaElement.volume=response.volume)}if(response.stopped&&connection.streams[response.streamid]&&onStreamEndedHandler(connection.streams[response.streamid],connection),response.promptStreamStop){if(!connection.privileges.canStopRemoteStream)return void 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".'});warn("Remote stream has been manually stopped!"),connection.streams[response.streamid]&&connection.streams[response.streamid].stop()}if(response.left){if(isFirefox){var userLeft=response.userid;for(var stream in connection.streams)stream=connection.streams[stream],stream.userid===userLeft&&(connection.stopMediaStream(stream),onStreamEndedHandler(stream,connection))}if(peer&&peer.connection&&("closed"!==peer.connection.signalingState&&-1===peer.connection.iceConnectionState.search(/disconnected|failed/gi)&&peer.connection.close(),peer.connection=null),participants[response.userid]&&delete participants[response.userid],response.closeEntireSession)return connection.onSessionClosed(response),void connection.leave();connection.remove(response.userid),onLeaveHandler({userid:response.userid,extra:response.extra||{},entireSessionClosed:!!response.closeEntireSession},connection)}if(response.playRoleOfBroadcaster&&(response.extra&&(connection.extra=merge(connection.extra,response.extra)),response.participants&&(participants=response.participants,participants[connection.userid]&&delete participants[connection.userid],sockets[0]&&sockets[0].userid===response.userid&&(delete sockets[0],sockets=swap(sockets)),socketObjects[response.userid]&&delete socketObjects[response.userid]),setTimeout(connection.playRoleOfInitiator,2e3)),response.changeBandwidth){if(!connection.peers[response.userid])throw"No such peer exists.";connection.peers[response.userid].peer.bandwidth=response.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.";connection.leave(),connection.onSessionClosed({userid:response.userid,extra:response.extra||_config.extra,isEjected:!0})}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(!0),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)return void connection.peers[response.userid][response.hold?"hold":"unhold"](response.holdMLine);connection.peers[response.userid].peer.hold=!!response.hold,connection.peers[response.userid].peer.holdMLine=response.holdMLine,socket.send({isRenegotiate:!0}),connection.peers[response.userid].fireHoldUnHoldEvents({kind:response.holdMLine,isHold:!!response.hold,userid:response.userid})}response.isRenegotiate&&connection.peers[response.userid].renegotiate(null,connection.peers[response.userid].peer.session),response.fakeData&&peerConfig.onmessage(response.fakeData),response.recreatePeer&&(peer=new PeerConnection),response.failedToReceiveRemoteVideo&&(log("Remote peer hasn't received stream: "+response.streamid+". Renegotiating..."),connection.peers[response.userid]&&connection.peers[response.userid].renegotiate()),response.redial&&connection.peers[response.userid]&&("disconnected"!==connection.peers[response.userid].peer.connection.iceConnectionState&&(_config.redialing=!1),"disconnected"!==connection.peers[response.userid].peer.connection.iceConnectionState||_config.redialing||(_config.redialing=!0,warn("Peer connection is closed.",toStr(connection.peers[response.userid].peer.connection),"ReDialing.."),connection.peers[response.userid].redial()))}}function sdpInvoker(sdp,labels){function createAnswer(){return isFirefox||"firefox"===_config.userinfo.browser?void(connection.peers[_config.userid]&&connection.peers[_config.userid].redial()):void peer.recreateAnswer(sdp,session,function(_sdp,streaminfo){sendsdp({sdp:_sdp,socket:socket,streaminfo:streaminfo}),connection.detachStreams=[]})}if("answer"===sdp.type)return peer.setRemoteDescription(sdp),void updateSocket();if(!_config.renegotiate&&"offer"===sdp.type)return peerConfig.offerDescription=sdp,peerConfig.session=connection.session,peer||(peer=new PeerConnection),peer.create("answer",peerConfig),void updateSocket();var session=_config.renegotiate;if(detachMediaStream(labels,peer.connection),session.oneway||isData(session))createAnswer(),delete _config.renegotiate;else{if(_config.capturing)return;_config.capturing=!0,connection.captureUserMedia(function(stream){_config.capturing=!1,(isChrome||isFirefox&&!peer.connection.getLocalStreams().length)&&peer.addStream(stream),connection.renegotiatedSessions[JSON.stringify(_config.renegotiate)]={session:_config.renegotiate,stream:stream},delete _config.renegotiate,createAnswer()},_config.renegotiate)}}var socketConfig={channel:_config.channel,onmessage:socketResponse,onopen:function(_socket){_socket&&(socket=_socket),isofferer&&!peer&&(peerConfig.session=connection.session,peer||(peer=new PeerConnection),peer.create("offer",peerConfig)),_config.socketIndex=socket.index=sockets.length,socketObjects[socketConfig.channel]=socket,sockets[_config.socketIndex]=socket,updateSocketForLocalStreams(socket),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);socket&&socketConfig.onopen(socket);var peer,isofferer=_config.isofferer,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,stun=iceCandidates.stun,turn=iceCandidates.turn;if(isNull(iceCandidates.reflexive)||(stun=iceCandidates.reflexive),isNull(iceCandidates.relay)||(turn=iceCandidates.relay),(iceCandidates.host||!candidate.candidate.match(/typ host/g))&&!(!turn&&candidate.candidate.match(/typ relay/g)||!stun&&candidate.candidate.match(/typ srflx/g))){var protocol=connection.iceProtocols;(protocol.udp||!candidate.candidate.match(/ udp /g))&&(protocol.tcp||!candidate.candidate.match(/ tcp /g))&&(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){var abToStr=ab2str(data);if(-1!==abToStr.indexOf('"userid":'))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;return void initFileBufferReader(connection,function(fbr){rtcMultiSession.fileBufferReader=fbr,that.onmessage(data)})}var fileBufferReader=rtcMultiSession.fileBufferReader;return void fileBufferReader.convertToObject(data,function(chunk){return chunk.maxChunks||chunk.readyForNextChunk?chunk.readyForNextChunk?void fileBufferReader.getNextChunk(chunk.uuid,function(nextChunk){rtcMultiSession.send(nextChunk)}):void fileBufferReader.addChunk(chunk,function(promptNextChunk){rtcMultiSession.send(promptNextChunk)}):void connection.onmessage({data:chunk,userid:_config.userid,extra:_config.extra})})}}},onaddstream:function(stream,session){function eventListener(){setTimeout(function(){mediaElement.muted=!1,afterRemoteStreamStartedFlowing({mediaElement:mediaElement,session:session,stream:stream,preMuted:preMuted})},3e3),mediaElement.removeEventListener("play",eventListener)}if(session=session||_config.renegotiate||connection.session,!isData(session)){session.screen&&(session.audio||session.video)&&(_config.gotAudioOrVideo?(session.audio=!1,session.video=!1):(_config.gotAudioOrVideo=!0,session.screen=!1));var preMuted={};if(_config.streaminfo){var streaminfo=_config.streaminfo.split("----"),strInfo=JSON.parse(streaminfo[streaminfo.length-1]);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:!0},session));return connection.setDefaultEventsForMediaElement&&connection.setDefaultEventsForMediaElement(mediaElement,stream.streamid),isPluginRTC||stream.getVideoTracks().length?void waitUntilRemoteStreamStartsFlowing({mediaElement:mediaElement,session:session,stream:stream,preMuted:preMuted}):mediaElement.addEventListener("play",eventListener,!1)}},onremovestream:function(stream){stream&&stream.streamid?(stream=connection.streams[stream.streamid],stream&&(log("on:stream:ended via on:remove:stream",stream),onStreamEndedHandler(stream,connection))):log("on:remove:stream",stream)},onclose:function(e){e.extra=_config.extra,e.userid=_config.userid,connection.onclose(e),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)),peer.connection&&"connected"===peer.connection.iceConnectionState&&"complete"===peer.connection.iceGatheringState&&"stable"===peer.connection.signalingState&&1===connection.numberOfConnectedUsers&&connection.onconnected({userid:_config.userid,extra:_config.extra,peer:connection.peers[_config.userid],targetuser:_config.userinfo}),!connection.isInitiator&&peer.connection&&"connected"===peer.connection.iceConnectionState&&"complete"===peer.connection.iceGatheringState&&"stable"===peer.connection.signalingState&&1===connection.numberOfConnectedUsers&&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."}),connection.peers[_config.userid]&&connection.peers[_config.userid].oniceconnectionstatechange&&connection.peers[_config.userid].oniceconnectionstatechange(event),connection.peers[_config.userid]&&"failed"===connection.peers[_config.userid].peer.connection.iceConnectionState&&connection.onfailed({userid:_config.userid,extra:_config.extra,peer:connection.peers[_config.userid],targetuser:_config.userinfo}),connection.peers[_config.userid]&&"disconnected"===connection.peers[_config.userid].peer.connection.iceConnectionState&&(peer.connection.renegotiate||connection.ondisconnected({userid:_config.userid,extra:_config.extra,peer:connection.peers[_config.userid],targetuser:_config.userinfo}),peer.connection.renegotiate=!1),connection.autoReDialOnFailure&&connection.peers[_config.userid]&&("disconnected"!==connection.peers[_config.userid].peer.connection.iceConnectionState&&(_config.redialing=!1),"disconnected"!==connection.peers[_config.userid].peer.connection.iceConnectionState||_config.redialing||(_config.redialing=!0,warn("Peer connection is closed.",toStr(connection.peers[_config.userid].peer.connection),"ReDialing.."),connection.peers[_config.userid].socket.send({redial:!0}),connection.streams.remove({remote:!0,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};connection.playRoleOfInitiator=function(){connection.dontCaptureUserMedia=!0,connection.open(),sockets=swap(sockets),connection.dontCaptureUserMedia=!1},connection.askToShareParticipants=function(){defaultSocket&&defaultSocket.send({askToShareParticipants:!0})},connection.shareParticipants=function(args){var message={joinUsers:participants,userid:connection.userid,extra:connection.extra};args&&(args.dontShareWith&&(message.dontShareWith=args.dontShareWith),args.shareWith&&(message.shareWith=args.shareWith)),defaultSocket.send(message)}}function detachMediaStream(labels,peer){if(labels)for(var i=0;i1&&isString(arguments[0])&&(e={},arguments[0]&&(e.userid=arguments[0]),arguments[1]&&(e.extra=arguments[1]),arguments[2]&&(e.channel=arguments[2])),connection.captureUserMedia(function(){_accept(e)})};var isRMSDeleted=!1;this.disconnect=function(){if(this.isOwnerLeaving=!0,!connection.keepStreamsOpened){for(var streamid in connection.localStreams)connection.localStreams[streamid].stop();connection.localStreams={},currentUserMediaRequest={streams:[],mutex:!1,queueRequests:[]}}connection.isInitiator&&defaultSocket.send({isDisconnectSockets:!0}),connection.refresh(),rtcMultiSession.defaultSocket=defaultSocket=null,isRMSDeleted=!0,connection.ondisconnected({userid:connection.userid,extra:connection.extra,peer:connection.peers[connection.userid],isSocketsDisconnected:!0}),connection.close(),window.removeEventListener("beforeunload",rtcMultiSession.leaveHandler),window.removeEventListener("keyup",rtcMultiSession.leaveHandler),log("Disconnected your sockets, peers, streams and everything except RTCMultiConnection object.")}}function getUserMedia(options){function streaming(stream,returnBack,streamid){streamid||(streamid=getRandomString()),connection.localStreams[streamid]=stream;var video=options.video;video&&(video[isFirefox?"mozSrcObject":"src"]=isFirefox?stream:window.webkitURL.createObjectURL(stream),video.play()),options.onsuccess(stream,returnBack,idInstance,streamid),currentUserMediaRequest.streams[idInstance]={stream:stream,streamid:streamid},currentUserMediaRequest.mutex=!1,currentUserMediaRequest.queueRequests.length&&getUserMedia(currentUserMediaRequest.queueRequests.shift())}if(isPluginRTC)return Plugin.getUserMedia?Plugin.getUserMedia(options.constraints||{audio:!0,video:!0},options.onsuccess,options.onerror):void setTimeout(function(){getUserMedia(options)},1e3);if(currentUserMediaRequest.mutex===!0)return void currentUserMediaRequest.queueRequests.push(options);currentUserMediaRequest.mutex=!0;var connection=options.connection,mediaConstraints=options.mediaConstraints||{},videoConstraints="boolean"==typeof mediaConstraints.video?mediaConstraints.video:mediaConstraints.video||mediaConstraints,audioConstraints="boolean"==typeof mediaConstraints.audio?mediaConstraints.audio:mediaConstraints.audio||defaultConstraints,n=navigator,hints=options.constraints||{audio:defaultConstraints,video:defaultConstraints};hints.video&&hints.video.mozMediaSource&&(videoConstraints={}),hints.video===!0&&(hints.video=defaultConstraints),hints.audio===!0&&(hints.audio=defaultConstraints),"boolean"==typeof audioConstraints&&hints.audio&&(hints.audio=audioConstraints),"boolean"==typeof videoConstraints&&hints.video&&(hints.video=videoConstraints);var audioMandatoryConstraints=audioConstraints.mandatory;if(isEmpty(audioMandatoryConstraints)||(hints.audio.mandatory=merge(hints.audio.mandatory,audioMandatoryConstraints)),hints.video!==!1){var videoMandatoryConstraints=videoConstraints.mandatory;if(videoMandatoryConstraints){var mandatory={};if(videoMandatoryConstraints.minWidth&&(mandatory.minWidth=videoMandatoryConstraints.minWidth),videoMandatoryConstraints.minHeight&&(mandatory.minHeight=videoMandatoryConstraints.minHeight),videoMandatoryConstraints.maxWidth&&(mandatory.maxWidth=videoMandatoryConstraints.maxWidth),videoMandatoryConstraints.maxHeight&&(mandatory.maxHeight=videoMandatoryConstraints.maxHeight),videoMandatoryConstraints.minAspectRatio&&(mandatory.minAspectRatio=videoMandatoryConstraints.minAspectRatio),videoMandatoryConstraints.maxFrameRate&&(mandatory.maxFrameRate=videoMandatoryConstraints.maxFrameRate),videoMandatoryConstraints.minFrameRate&&(mandatory.minFrameRate=videoMandatoryConstraints.minFrameRate),mandatory.minWidth&&mandatory.minHeight){var allowed=["1920:1080","1280:720","960:720","640:360","640:480","320:240","320:180"];(-1===allowed.indexOf(mandatory.minWidth+":"+mandatory.minHeight)||-1===allowed.indexOf(mandatory.maxWidth+":"+mandatory.maxHeight))&&error('The min/max width/height constraints you passed "seems" NOT supported.',toStr(mandatory)),(mandatory.minWidth>mandatory.maxWidth||mandatory.minHeight>mandatory.maxHeight)&&error("Minimum value must not exceed maximum value.",toStr(mandatory)),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)}videoMandatoryConstraints&&(hints.video.mandatory=merge(hints.video.mandatory,videoMandatoryConstraints)),videoConstraints.optional&&videoConstraints.optional instanceof Array&&videoConstraints.optional.length&&(hints.video.optional=hints.video.optional?hints.video.optional.concat(videoConstraints.optional):videoConstraints.optional),hints.video.mandatory&&!isEmpty(hints.video.mandatory)&&connection._mediaSources.video&&(hints.video.optional.forEach(function(video,index){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})),hints.video&&!hints.video.mozMediaSource&&hints.video.optional&&hints.video.mandatory&&!hints.video.optional.length&&isEmpty(hints.video.mandatory)&&(hints.video=!0)}audioConstraints.optional&&audioConstraints.optional instanceof Array&&audioConstraints.optional.length&&(hints.audio.optional=hints.audio.optional?hints.audio.optional.concat(audioConstraints.optional):audioConstraints.optional),hints.audio.mandatory&&!isEmpty(hints.audio.mandatory)&&connection._mediaSources.audio&&(hints.audio.optional.forEach(function(audio,index){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})),isMobileDevice&&(hints={audio:!!hints.audio,video:!!hints.video}),log("invoked getUserMedia with constraints:",toStr(hints));var idInstance=JSON.stringify(hints);currentUserMediaRequest.streams[idInstance]?streaming(currentUserMediaRequest.streams[idInstance].stream,!0,currentUserMediaRequest.streams[idInstance].streamid):(n.getMedia=n.webkitGetUserMedia||n.mozGetUserMedia,n.getMedia(hints,streaming,function(error){options.onerror(error,hints)}))}function PeerConnection(){return{create:function(type,options){merge(this,options);var self=this;return this.type=type,this.init(),this.attachMediaStreams(),isFirefox&&this.session.data?(this.session.data&&"offer"===type&&this.createDataChannel(),this.getLocalDescription(type),this.session.data&&"answer"===type&&this.createDataChannel()):self.getLocalDescription(type),this},getLocalDescription:function(createType){function createDescription(){self.connection["offer"===createType?"createOffer":"createAnswer"](function(sessionDescription){sessionDescription.sdp=self.serializeSdp(sessionDescription.sdp,createType),self.connection.setLocalDescription(sessionDescription),self.trickleIce&&self.onSessionDescription(sessionDescription,self.streaminfo),"offer"===sessionDescription.type&&log("offer sdp",sessionDescription.sdp),self.prevCreateType=createType},self.onSdpError,self.constraints)}log("(getLocalDescription) peer createType is",createType),this.session.inactive&&isNull(this.rtcMultiConnection.waitUntilRemoteStreamStartsFlowing)&&(this.rtcMultiConnection.waitUntilRemoteStreamStartsFlowing=!1);var self=this;"answer"===createType?this.setRemoteDescription(this.offerDescription,createDescription):createDescription()},serializeSdp:function(sdp,createType){if(sdp=this.processSdp(sdp),isFirefox)return sdp;if(this.session.inactive&&!this.holdMLine&&(this.hold=!0,(this.session.screen||this.session.video)&&this.session.audio?this.holdMLine="both":this.session.screen||this.session.video?this.holdMLine="video":this.session.audio&&(this.holdMLine="audio")),sdp=this.setBandwidth(sdp),"both"===this.holdMLine){if(this.hold)this.prevSDP=sdp,sdp=sdp.replace(/a=sendonly|a=recvonly|a=sendrecv/g,"a=inactive");else if(this.prevSDP&&!this.session.inactive&&(sdp=this.prevSDP,35>=chromeVersion))return sdp}else if("audio"===this.holdMLine||"video"===this.holdMLine){sdp=sdp.split("m=");var audio="",video="";sdp[1]&&0===sdp[1].indexOf("audio")&&(audio="m="+sdp[1]),sdp[2]&&0===sdp[2].indexOf("audio")&&(audio="m="+sdp[2]),sdp[1]&&0===sdp[1].indexOf("video")&&(video="m="+sdp[1]),sdp[2]&&0===sdp[2].indexOf("video")&&(video="m="+sdp[2]),"audio"===this.holdMLine&&(this.hold?(this.prevSDP=sdp[0]+audio+video,sdp=sdp[0]+audio.replace(/a=sendonly|a=recvonly|a=sendrecv/g,"a=inactive")+video):this.prevSDP&&(sdp=this.prevSDP)),"video"===this.holdMLine&&(this.hold?(this.prevSDP=sdp[0]+audio+video,sdp=sdp[0]+audio+video.replace(/a=sendonly|a=recvonly|a=sendrecv/g,"a=inactive")):this.prevSDP&&(sdp=this.prevSDP))}return!this.hold&&this.session.inactive&&(sdp="offer"===createType?sdp.replace(/a=setup:passive|a=setup:active|a=setup:holdconn/g,"a=setup:actpass"):sdp.replace(/a=setup:actpass|a=setup:passive|a=setup:holdconn/g,"a=setup:active"),sdp=sdp.replace(/a=inactive/g,"a=sendrecv")),sdp},init:function(){function returnSDP(){return self.returnedSDP?void(self.returnedSDP=!1):(self.returnedSDP=!0,void self.onSessionDescription(self.connection.localDescription,self.streaminfo))}this.setConstraints(),this.connection=new RTCPeerConnection(this.rtcConfiguration,this.optionalArgument),this.session.data&&(log("invoked: createDataChannel"),this.createDataChannel()),this.connection.onicecandidate=function(event){return event.candidate?void(self.trickleIce&&self.onicecandidate(event.candidate)):void(self.trickleIce||returnSDP())},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(){self.connection&&(self.oniceconnectionstatechange({iceConnectionState:self.connection.iceConnectionState,iceGatheringState:self.connection.iceGatheringState,signalingState:self.connection.signalingState}),self.trickleIce||"complete"===self.connection.iceGatheringState&&(log("iceGatheringState",self.connection.iceGatheringState),returnSDP()))};var self=this},setBandwidth:function(sdp){if(isMobileDevice||isFirefox||!this.bandwidth)return sdp;var bandwidth=this.bandwidth;return this.session.screen&&(bandwidth.screen?bandwidth.screen<300&&warn("It seems that you are using wrong bandwidth value for screen. Screen sharing is expected to fail."):warn("It seems that you are not using bandwidth for screen. Screen sharing is expected to fail.")),bandwidth.screen&&this.session.screen&&(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")),(bandwidth.audio||bandwidth.video||bandwidth.data)&&(sdp=sdp.replace(/b=AS([^\r\n]+\r\n)/g,"")),bandwidth.audio&&(sdp=sdp.replace(/a=mid:audio\r\n/g,"a=mid:audio\r\nb=AS:"+bandwidth.audio+"\r\n")),bandwidth.video&&(sdp=sdp.replace(/a=mid:video\r\n/g,"a=mid:video\r\nb=AS:"+(this.session.screen?bandwidth.screen:bandwidth.video)+"\r\n")),bandwidth.data&&!this.preferSCTP&&(sdp=sdp.replace(/a=mid:data\r\n/g,"a=mid:data\r\nb=AS:"+bandwidth.data+"\r\n")),sdp},setConstraints:function(){if(this.constraints={optional:this.sdpConstraints.optional||isChrome?[{VoiceActivityDetection:!1}]:[],mandatory:this.sdpConstraints.mandatory||{OfferToReceiveAudio:!!this.session.audio,OfferToReceiveVideo:!!this.session.video||!!this.session.screen}},log("sdp-constraints",toStr(this.constraints)),this.optionalArgument={optional:this.optionalArgument.optional||[],mandatory:this.optionalArgument.mandatory||{}},this.preferSCTP||this.optionalArgument.optional.push({RtpDataChannels:!0}),log("optional-argument",toStr(this.optionalArgument)),isNull(this.rtcConfiguration)||isNull(this.iceServers))this.rtcConfiguration=null;else{var iceCandidates=this.rtcMultiConnection.candidates,stun=iceCandidates.stun,turn=iceCandidates.turn,host=iceCandidates.host;isNull(iceCandidates.reflexive)||(stun=iceCandidates.reflexive),isNull(iceCandidates.relay)||(turn=iceCandidates.relay),host||stun||!turn?host||stun||turn||(this.rtcConfiguration.iceTransports="none"):this.rtcConfiguration.iceTransports="relay",this.rtcConfiguration={iceServers:this.iceServers,iceTransports:this.rtcConfiguration.iceTransports}}log("rtc-configuration",toStr(this.rtcConfiguration))},onSdpError:function(e){var message=toStr(e);message&&-1!==message.indexOf("RTP/SAVPF Expects at least 4 fields")&&(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){log("setting remote description",sessionDescription.type,sessionDescription.sdp);var self=this;this.connection.setRemoteDescription(new RTCSessionDescription(sessionDescription),onSdpSuccess||this.onSdpSuccess,function(error){-1===error.search(/STATE_SENTINITIATE|STATE_INPROGRESS/gi)&&self.onSdpError(error)})}},addIceCandidate:function(candidate){function onAddIceCandidate(iceCandidate){self.connection.addIceCandidate(iceCandidate,function(){log("added:",candidate.sdpMid,candidate.candidate)},function(){error("onIceFailure",arguments,candidate.candidate)})}var self=this;isPluginRTC?new RTCIceCandidate(candidate,function(iceCandidate){onAddIceCandidate(iceCandidate)}):onAddIceCandidate(new RTCIceCandidate(candidate))},createDataChannel:function(channelIdentifier){if(!this.channels||!this.channels.length){var self=this;this.channels||(this.channels=[]);var dataChannelDict={};this.dataChannelDict&&(dataChannelDict=this.dataChannelDict),isChrome&&!this.preferSCTP&&(dataChannelDict.reliable=!1),log("dataChannelDict",toStr(dataChannelDict)),("answer"===this.type||isFirefox)&&(this.connection.ondatachannel=function(event){self.setChannelEvents(event.channel)}),(isChrome&&"offer"===this.type||isFirefox)&&this.setChannelEvents(this.connection.createDataChannel(channelIdentifier||"channel",dataChannelDict))}},setChannelEvents:function(channel){var self=this;channel.binaryType="arraybuffer",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("disconnected"!==self.connection.iceConnectionState&&-1===channel.readyState.search(/closing|closed/g)&&-1!==channel.readyState.search(/connecting|open/g)){if("connecting"===channel.readyState)return numberOfTimes++,setTimeout(function(){if(!(20>numberOfTimes))throw"Number of times exceeded to wait for WebRTC data connection to be opened.";channel.send(data)},1e3);try{channel.push(data)}catch(e){if(numberOfTimes++,warn("Data transmission failed. Re-transmitting..",numberOfTimes,toStr(e)),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){stream.streamid||isIE||(stream.streamid=getRandomString()),log("attaching stream:",stream.streamid,isPluginRTC?stream:toStr(stream)),this.connection.addStream(stream),this.sendStreamId(stream),this.getStreamInfo()},attachMediaStreams:function(){for(var streams=this.attachStreams,i=0;ii;i++)token+=a[i].toString(36);return token}return(Math.random()*(new Date).getTime()).toString(36).replace(/\./g,"")}function isData(session){return!session.audio&&!session.video&&!session.screen&&session.data}function isNull(obj){return"undefined"==typeof obj}function isString(obj){return"string"==typeof obj}function isEmpty(session){var stringified=JSON.stringify(session);return"{}"!==stringified&&stringified.split(",").length?!1:!0}function ab2str(buf){var result="";try{result=String.fromCharCode.apply(null,new Uint16Array(buf))}catch(e){}return result}function str2ab(str){isString(str)||(str=JSON.stringify(str));for(var buf=new ArrayBuffer(2*str.length),bufView=new Uint16Array(buf),i=0,strLen=str.length;strLen>i;i++)bufView[i]=str.charCodeAt(i);return buf}function swap(arr){for(var swapped=[],length=arr.length,i=0;length>i;i++)arr[i]&&arr[i]!==!0&&swapped.push(arr[i]);return swapped}function forEach(obj,callback){for(var item in obj)callback(obj[item],item)}function toStr(obj){return JSON.stringify(obj,function(key,value){return key=key,value&&value.sdp?(log(value.sdp.type," ",value.sdp.sdp),""):value}," ")}function getLength(obj){if("object"!=typeof obj)throw"Invalid data-type: "+typeof obj+"; expected: object";var stringified=JSON.stringify(obj);return"{}"!==stringified&&stringified.split(",").length?stringified.split(",").length:0}function createMediaElement(stream,session){var mediaElement=document.createElement(stream.isAudio?"audio":"video");if(mediaElement.id=stream.streamid,isPluginRTC){var body=document.body||document.documentElement;return body.insertBefore(mediaElement,body.firstChild),setTimeout(function(){Plugin.attachMediaStream(mediaElement,stream)},1e3),Plugin.attachMediaStream(mediaElement,stream)}return mediaElement[isFirefox?"mozSrcObject":"src"]=isFirefox?stream:window.webkitURL.createObjectURL(stream),mediaElement.controls=!0,mediaElement.autoplay=!!session.remote,mediaElement.muted=session.remote?!1:!0,isFirefox&&mediaElement.addEventListener("ended",function(){stream.onended()},!1),mediaElement.play(),mediaElement}function onStreamEndedHandler(streamedObject,connection){(!streamedObject.mediaElement||streamedObject.mediaElement.parentNode)&&(onStreamEndedHandlerFiredFor[streamedObject.streamid]||(onStreamEndedHandlerFiredFor[streamedObject.streamid]=streamedObject,connection.onstreamended(streamedObject)))}function onLeaveHandler(event,connection){onLeaveHandlerFiredFor[event.userid]||(onLeaveHandlerFiredFor[event.userid]=event,connection.onleave(event),connection.numberOfConnectedUsers--)}function takeSnapshot(args){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])}var userid=args.userid,connection=args.connection;if(args.mediaElement)return _takeSnapshot(args.mediaElement);for(var stream in connection.streams)stream=connection.streams[stream],stream.userid===userid&&stream.stream&&stream.stream.getVideoTracks&&stream.stream.getVideoTracks().length&&_takeSnapshot(stream.mediaElement)}function invokeMediaCaptured(connection){connection.onMediaCaptured&&(connection.onMediaCaptured(),delete connection.onMediaCaptured)}function merge(mergein,mergeto){if(mergein||(mergein={}),!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),onload&&onload() +},document.documentElement.appendChild(script)}function capturePartOfScreen(args){var connection=args.connection,element=args.element;if(!window.html2canvas)return loadScript(connection.resources.html2canvas,function(){capturePartOfScreen(args)});if(isString(element)&&(element=document.querySelector(element),element||(element=document.getElementById(element))),!element)throw"HTML DOM Element is not accessible!";window.html2canvas(element,{onrendered:function(canvas){args.callback(canvas.toDataURL())}})}function initFileBufferReader(connection,callback){function _private(chunk){return chunk.userid=chunk.extra.userid,chunk}if(!window.FileBufferReader)return void loadScript(connection.resources.FileBufferReader,function(){initFileBufferReader(connection,callback)});var fileBufferReader=new window.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)}function loadScreenFrame(skip){if(DetectRTC.screen.extensionid===ReservedExtensionID&&!loadedScreenFrame){if(!skip)return loadScreenFrame(!0);loadedScreenFrame=!0;var iframe=document.createElement("iframe");iframe.onload=function(){iframe.isLoaded=!0,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(){return iframe.isLoaded?void iframe.contentWindow.postMessage({captureSourceId:!0},"*"):void setTimeout(screenFrame.postMessage,100)}}}}function loadIceFrame(callback,skip){if(!loadedIceFrame){if(!skip)return loadIceFrame(callback,!0);loadedIceFrame=!0;var iframe=document.createElement("iframe");iframe.onload=function(){function iFrameLoaderCallback(event){event.data&&event.data.iceServers&&(callback(event.data.iceServers),window.removeEventListener("message",iFrameLoaderCallback))}iframe.isLoaded=!0,listenEventHandler("message",iFrameLoaderCallback),iframe.contentWindow.postMessage("get-ice-servers","*")},iframe.src="https://cdn.webrtc-experiment.com/getIceServers/",iframe.style.display="none",(document.body||document.documentElement).appendChild(iframe)}}function muteOrUnmute(e){var stream=e.stream,root=e.root,session=e.session||{},enabled=e.enabled;if(session.audio||session.video||(session=isString(session)?{audio:!0,video:!0}:merge(session,{audio:!0,video:!0})),session.type){if("remote"===session.type&&"remote"!==root.type)return;if("local"===session.type&&"local"!==root.type)return}if(log(enabled?"Muting":"UnMuting","session",toStr(session)),"local"===root.type&&session.audio&&stream.getAudioTracks){var audioTracks=stream.getAudioTracks()[0];audioTracks&&(audioTracks.enabled=!enabled)}if("local"===root.type&&(session.video||session.screen)&&stream.getVideoTracks){var videoTracks=stream.getVideoTracks()[0];videoTracks&&(videoTracks.enabled=!enabled)}if(root.sockets.forEach(function(socket){"local"===root.type&&socket.send({streamid:root.streamid,mute:!!enabled,unmute:!enabled,session:session}),"remote"===root.type&&socket.send({promptMuteUnmute:!0,streamid:root.streamid,mute:!!enabled,unmute:!enabled,session:session})}),"remote"!==root.type){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,enabled&&(stream.preMuted={audio:stream.getAudioTracks().length&&!stream.getAudioTracks()[0].enabled,video:stream.getVideoTracks().length&&!stream.getVideoTracks()[0].enabled},root.rtcMultiConnection.onmute(fakeObject)),enabled||(stream.preMuted={},root.rtcMultiConnection.onunmute(fakeObject))}}function listenEventHandler(eventName,eventHandler){window.removeEventListener(eventName,eventHandler),window.addEventListener(eventName,eventHandler,!1)}function initHark(args){if(!window.hark)return void loadScript(args.connection.resources.hark,function(){initHark(args)});var connection=args.connection,streamedObject=args.streamedObject,stream=args.stream,options={},speechEvents=window.hark(stream,options);speechEvents.on("speaking",function(){connection.onspeaking&&connection.onspeaking(streamedObject)}),speechEvents.on("stopped_speaking",function(){connection.onsilence&&connection.onsilence(streamedObject)}),speechEvents.on("volume_change",function(volume,threshold){connection.onvolumechange&&connection.onvolumechange(merge({volume:volume,threshold:threshold},streamedObject))})}function setDefaults(connection){function updateLabel(progress,label){if(-1!==progress.position){var position=+progress.position.toFixed(2).split(".")[1]||100;label.innerHTML=position+"%"}}connection.userid=getRandomString(),connection.session={audio:!0,video:!0},connection.maxParticipantsAllowed=256,connection.direction="many-to-many",connection.mediaConstraints={mandatory:{},optional:[],audio:{mandatory:{},optional:[]},video:{mandatory:{},optional:[]}},connection.candidates={host:!0,stun:!0,turn:!0},connection.sdpConstraints={},connection.sdpConstraints.mandatory={OfferToReceiveAudio:!0,OfferToReceiveVideo:!0},connection.privileges={canStopRemoteStream:!1,canMuteRemoteStream:!1},connection.iceProtocols={tcp:!0,udp:!0},connection.preferSCTP=isFirefox||chromeVersion>=32?!0:!1,connection.chunkInterval=isFirefox||chromeVersion>=32?100:500,connection.chunkSize=isFirefox||chromeVersion>=32?13e3:1e3,connection.fakeDataChannels=!1,connection.waitUntilRemoteStreamStartsFlowing=null,connection.leaveOnPageUnload=!0,connection.getExternalIceServers=isChrome,connection.UA={isFirefox:isFirefox,isChrome:isChrome,isMobileDevice:isMobileDevice,version:isChrome?chromeVersion:firefoxVersion,isNodeWebkit:isNodeWebkit,isSafari:isSafari,isIE:isIE,isOpera:isOpera},connection.fileQueue={},connection.renegotiatedSessions={},connection.channels={},connection.extra={},connection.bandwidth={screen:300},connection.caniuse={RTCPeerConnection:DetectRTC.isWebRTCSupported,getUserMedia:!!navigator.webkitGetUserMedia||!!navigator.mozGetUserMedia,AudioContext:DetectRTC.isAudioContextSupported,ScreenSharing:DetectRTC.isScreenCapturingSupported,RtpDataChannels:DetectRTC.isRtpDataChannelsSupported,SctpDataChannels:DetectRTC.isSctpDataChannelsSupported},connection.snapshots={},connection._mediaSources={},connection.devices={},connection.language="en",connection.autoTranslateText=!1,connection.googKey="AIzaSyCgB5hmFY74WYB-EoWkhr9cAGr6TiTHrEE",connection.localStreamids=[],connection.localStreams={},connection.preRecordedMedias={},connection.attachStreams=[],connection.detachStreams=[],connection.optionalArgument={optional:[{DtlsSrtpKeyAgreement:!0},{googImprovedWifiBwe:!0},{googScreencastMinBitrate:300}],mandatory:{}},connection.dataChannelDict={},connection.dontAttachStream=!1,connection.dontCaptureUserMedia=!1,connection.preventSSLAutoAllowed=!1,connection.autoReDialOnFailure=!0,connection.isInitiator=!1,connection.DetectRTC=DetectRTC,connection.trickleIce=!0,connection.sessionDescriptions={},connection.sessionDescription=null,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://chat.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"},connection.body=document.body||document.documentElement,connection.peers={},connection.firebase="chat",connection.numberOfSessions=0,connection.numberOfConnectedUsers=0,connection.enableFileSharing=!0,connection.autoSaveToDisk=!1,connection.processSdp=function(sdp){return sdp},connection.onmessage=function(e){log("onmessage",toStr(e))},connection.onopen=function(e){log("Data connection is opened between you and",e.userid)},connection.onerror=function(e){error(onerror,toStr(e))},connection.onclose=function(e){warn("onclose",toStr(e)),connection.streams.remove({userid:e.userid})};var progressHelper={};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},connection.onFileProgress=function(chunk){var helper=progressHelper[chunk.uuid];helper&&(helper.progress.value=chunk.currentPosition||chunk.maxChunks||helper.progress.max,updateLabel(helper.progress,helper.label))},connection.onFileEnd=function(file){progressHelper[file.uuid]&&(progressHelper[file.uuid].div.innerHTML=''+file.name+""),(connection.onFileSent||connection.onFileReceived)&&(connection.onFileSent&&connection.onFileSent(file,file.uuid),connection.onFileReceived&&connection.onFileReceived(file.name,file))},connection.onstream=function(e){connection.body.insertBefore(e.mediaElement,connection.body.firstChild)},connection.onstreamended=function(e){if(log("onStreamEndedHandler:",e),!e.mediaElement)return warn("Event.mediaElement is undefined",e);if(!e.mediaElement.parentNode){if(e.mediaElement=document.getElementById(e.streamid),!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)},connection.onSessionClosed=function(session){session.isEjected?warn(session.userid,"ejected you."):warn("Session has been closed.",session)},connection.onmute=function(e){e.isVideo&&e.mediaElement&&(e.mediaElement.pause(),e.mediaElement.setAttribute("poster",e.snapshot||connection.resources.muted)),e.isAudio&&e.mediaElement&&(e.mediaElement.muted=!0)},connection.onunmute=function(e){e.isVideo&&e.mediaElement&&(e.mediaElement.play(),e.mediaElement.removeAttribute("poster")),e.isAudio&&e.mediaElement&&(e.mediaElement.muted=!1)},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"],connection.streams={mute:function(session){this._private(session,!0)},unmute:function(session){this._private(session,!1)},_private:function(session,enabled){function _muteOrUnMute(stream,session,isMute){session.local&&"local"!==stream.type||session.remote&&"remote"!==stream.type||!(session.screen&&!stream.isScreen||session.audio&&!stream.isAudio||session.video&&!stream.isVideo||(isMute?!stream.mute(session):!stream.unmute(session)))}var stream;if(!session||isString(session))for(stream in this)-1===connection._skip.indexOf(stream)&&this[stream]._private(session,enabled);else for(stream in this)-1===connection._skip.indexOf(stream)&&_muteOrUnMute(this[stream],session,enabled)},stop:function(type){function _stopStream(_stream,config){config.userid&&_stream.userid!==config.userid||config.local&&"local"!==_stream.type||config.remote&&"remote"!==_stream.type||(config.screen&&_stream.isScreen&&_stream.stop(),config.audio&&_stream.isAudio&&_stream.stop(),config.video&&_stream.isVideo&&_stream.stop(),config.audio||config.video||config.screen||_stream.stop())}var _stream;for(var stream in this)if(-1===connection._skip.indexOf(stream))if(_stream=this[stream],type)if(isString(type)){var config={};config[type]=!0,_stopStream(_stream,config)}else _stopStream(_stream,type);else _stream.stop()},remove:function(type){function _stopAndRemoveStream(_stream,config){config.userid&&_stream.userid!==config.userid||config.local&&"local"!==_stream.type||config.remote&&"remote"!==_stream.type||(config.screen&&_stream.isScreen&&endStream(_stream),config.audio&&_stream.isAudio&&endStream(_stream),config.video&&_stream.isVideo&&endStream(_stream),config.audio||config.video||config.screen||endStream(_stream))}function endStream(_stream){onStreamEndedHandler(_stream,connection),delete connection.streams[_stream.streamid]}var _stream;for(var stream in this)if(-1===connection._skip.indexOf(stream))if(_stream=this[stream],type)if(isString(type)){var config={};config[type]=!0,_stopAndRemoveStream(_stream,config)}else _stopAndRemoveStream(_stream,type);else _stopAndRemoveStream(_stream,{local:!0,remote:!0})},selectFirst:function(args){return this._selectStreams(args,!1)},selectAll:function(args){return this._selectStreams(args,!0)},_selectStreams:function(args,all){if(!args||isString(args)||isEmpty(args))throw"Invalid arguments.";isNull(args.local)&&isNull(args.remote)&&isNull(args.userid)&&(args.local=args.remote=!0),args.isAudio||args.isVideo||args.isScreen||(args.isAudio=args.isVideo=args.isScreen=!0);var selectedStreams=[];for(var stream in this)-1===connection._skip.indexOf(stream)&&(stream=this[stream])&&(args.local&&"local"===stream.type||args.remote&&"remote"===stream.type||args.userid&&stream.userid===args.userid)&&(args.isVideo&&stream.isVideo&&selectedStreams.push(stream),args.isAudio&&stream.isAudio&&selectedStreams.push(stream),args.isScreen&&stream.isScreen&&selectedStreams.push(stream));return all?selectedStreams:selectedStreams[0]}};var iceServers=[];iceServers.push({url:"stun:stun.l.google.com:19302"}),iceServers.push({url:"stun:stun.anyfirewall.com:3478"}),iceServers.push({url:"turn:turn.bistri.com:80",credential:"homeo",username:"homeo"}),iceServers.push({url:"turn:turn.anyfirewall.com:443?transport=tcp",credential:"webrtc",username:"webrtc"}),connection.iceServers=iceServers,connection.rtcConfiguration={iceServers:null,iceTransports:"all",peerIdentity:!1},connection.media={min:function(width,height){connection.mediaConstraints.video&&(connection.mediaConstraints.video.mandatory||(connection.mediaConstraints.video.mandatory={}),connection.mediaConstraints.video.mandatory.minWidth=width,connection.mediaConstraints.video.mandatory.minHeight=height)},max:function(width,height){connection.mediaConstraints.video&&(connection.mediaConstraints.video.mandatory||(connection.mediaConstraints.video.mandatory={}),connection.mediaConstraints.video.mandatory.maxWidth=width,connection.mediaConstraints.video.mandatory.maxHeight=height)}},connection._getStream=function(event){function muteOrUnmuteLocally(isPause,mediaElement){if(mediaElement){var lastPauseState=mediaElement.onpause,lastPlayState=mediaElement.onplay;mediaElement.onpause=mediaElement.onplay=function(){},isPause?mediaElement.pause():mediaElement.play(),mediaElement.onpause=lastPauseState,mediaElement.onplay=lastPlayState}}var resultingObject=merge({sockets:event.socket?[event.socket]:[]},event);return resultingObject.stop=function(){var self=this;if(self.sockets.forEach(function(socket){"local"===self.type&&socket.send({streamid:self.streamid,stopped:!0}),"remote"===self.type&&socket.send({promptStreamStop:!0,streamid:self.streamid})}),"remote"!==self.type){var stream=self.stream;stream&&self.rtcMultiConnection.stopMediaStream(stream)}},resultingObject.mute=function(session){this.muted=!0,this._private(session,!0)},resultingObject.unmute=function(session){this.muted=!1,this._private(session,!1)},resultingObject._private=function(session,enabled){return session&&!isNull(session.sync)&&session.sync===!1?void muteOrUnmuteLocally(enabled,this.mediaElement):void muteOrUnmute({root:this,session:session,enabled:enabled,stream:this.stream})},resultingObject.startRecording=function(session){var self=this;return session||(session={audio:!0,video:!0}),isString(session)&&(session={audio:"audio"===session,video:"video"===session}),window.RecordRTC?(log("started recording session",session),self.videoRecorder=self.audioRecorder=null,isFirefox?session.video?self.videoRecorder=new window.RecordRTC(self.stream,{type:"video"}):session.audio&&(self.audioRecorder=new window.RecordRTC(self.stream,{type:"audio"})):isChrome&&(session.video&&(self.videoRecorder=new window.RecordRTC(self.stream,{type:"video"})),session.audio&&(self.audioRecorder=new window.RecordRTC(self.stream,{type:"audio"}))),self.audioRecorder&&self.audioRecorder.startRecording(),void(self.videoRecorder&&self.videoRecorder.startRecording())):loadScript(self.rtcMultiConnection.resources.RecordRTC,function(){self.startRecording(session)})},resultingObject.stopRecording=function(callback,session){session||(session={audio:!0,video:!0}),isString(session)&&(session={audio:"audio"===session,video:"video"===session}),log("stopped recording session",session);var self=this;session.audio&&self.audioRecorder?self.audioRecorder.stopRecording(function(){session.video&&self.videoRecorder?self.videoRecorder.stopRecording(function(){callback({audio:self.audioRecorder.getBlob(),video:self.videoRecorder.getBlob()})}):callback({audio:self.audioRecorder.getBlob()})}):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})},resultingObject.streamObject=resultingObject,resultingObject},connection.set=function(properties){for(var property in properties)this[property]=properties[property];return this},connection.onMediaError=function(event){error("name",event.name),error("constraintName",toStr(event.constraintName)),error("message",event.message),error("original session",event.session)},connection.takeSnapshot=function(userid,callback){takeSnapshot({userid:userid,connection:connection,callback:callback})},connection.saveToDisk=function(blob,fileName){blob.size&&blob.type?FileSaver.SaveToDisk(URL.createObjectURL(blob),fileName||blob.name||blob.type.replace("/","-")+blob.type.split("/")[1]):FileSaver.SaveToDisk(blob,fileName)},connection.selectDevices=function(device1,device2){function select(device){device&&(connection._mediaSources[device.kind]=device.id)}device1&&select(this.devices[device1]),device2&&select(this.devices[device2])},connection.getDevices=function(callback){return DetectRTC.MediaDevices.length?(DetectRTC.MediaDevices.forEach(function(device){connection.devices[device.deviceId]=device}),void(callback&&callback(connection.devices))):setTimeout(function(){connection.getDevices(callback)},1e3)},connection.getMediaDevices=connection.enumerateDevices=function(callback){if(!callback)throw"callback is mandatory.";connection.getDevices(function(){callback(connection.DetectRTC.MediaDevices)})},connection.onCustomMessage=function(message){log("Custom message",message)},connection.ondrop=function(droppedBy){log("Media connection is dropped by "+droppedBy)},connection.drop=function(config){config=config||{},connection.attachStreams=[];for(var stream in connection.streams)-1===connection._skip.indexOf(stream)&&(stream=connection.streams[stream],"local"===stream.type?(connection.detachStreams.push(stream.streamid),onStreamEndedHandler(stream,connection)):onStreamEndedHandler(stream,connection));connection.sendCustomMessage({drop:!0,dontRenegotiate:isNull(config.renegotiate)?!0:config.renegotiate})},connection.Translator={TranslateText:function(text,callback){var newScript=document.createElement("script");newScript.type="text/javascript";var sourceText=encodeURIComponent(text),randomNumber="method"+connection.token();window[randomNumber]=function(response){response.data&&response.data.translations[0]&&callback&&callback(response.data.translations[0].translatedText),response.error&&"Daily Limit Exceeded"===response.error.message&&(warn('Text translation failed. Error message: "Daily Limit Exceeded."'),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)}},connection.setDefaultEventsForMediaElement=function(mediaElement,streamid){mediaElement.onpause=function(){connection.streams[streamid]&&!connection.streams[streamid].muted&&connection.streams[streamid].mute()},mediaElement.onplay=function(){connection.streams[streamid]&&connection.streams[streamid].muted&&connection.streams[streamid].unmute()};var volumeChangeEventFired=!1;mediaElement.onvolumechange=function(){volumeChangeEventFired||(volumeChangeEventFired=!0,connection.streams[streamid]&&setTimeout(function(){var root=connection.streams[streamid];connection.streams[streamid].sockets.forEach(function(socket){socket.send({streamid:root.streamid,isVolumeChanged:!0,volume:mediaElement.volume})}),volumeChangeEventFired=!1},2e3))}},connection.onMediaFile=function(e){log("onMediaFile",e),connection.body.appendChild(e.mediaElement)},connection.shareMediaFile=function(file,video,streamerid){return streamerid=streamerid||connection.token(),window.PreRecordedMediaStreamer?window.PreRecordedMediaStreamer.shareMediaFile({file:file,video:video,streamerid:streamerid,connection:connection}):(loadScript(connection.resources.PreRecordedMediaStreamer,function(){connection.shareMediaFile(file,video,streamerid)}),streamerid)},connection.onpartofscreen=function(e){var image=document.createElement("img");image.src=e.screenshot,connection.body.appendChild(image)},connection.skipLogs=function(){log=error=warn=function(){}},connection.hold=function(mLine){for(var peer in connection.peers)connection.peers[peer].hold(mLine)},connection.onhold=function(track){log("onhold",track),"audio"!==track.kind&&(track.mediaElement.pause(),track.mediaElement.setAttribute("poster",track.screenshot||connection.resources.muted)),"audio"===track.kind&&(track.mediaElement.muted=!0)},connection.unhold=function(mLine){for(var peer in connection.peers)connection.peers[peer].unhold(mLine)},connection.onunhold=function(track){log("onunhold",track),"audio"!==track.kind&&(track.mediaElement.play(),track.mediaElement.removeAttribute("poster")),"audio"!==track.kind&&(track.mediaElement.muted=!1)},connection.sharePartOfScreen=function(args){function partOfScreenCapturer(){(!connection.partOfScreen||connection.partOfScreen.sharing)&&capturePartOfScreen({element:args.element,connection:connection,callback:function(screenshot){if(screenshot!==lastScreenshot){lastScreenshot=screenshot;for(var channel in connection.channels)connection.channels[channel].send({screenshot:screenshot,isPartOfScreen:!0})}args.once||setTimeout(partOfScreenCapturer,args.interval||200)}})}var lastScreenshot="";partOfScreenCapturer(),connection.partOfScreen=merge({sharing:!0},args)},connection.pausePartOfScreenSharing=function(){for(var peer in connection.peers)connection.peers[peer].pausePartOfScreenSharing=!0;connection.partOfScreen&&(connection.partOfScreen.sharing=!1)},connection.resumePartOfScreenSharing=function(){for(var peer in connection.peers)connection.peers[peer].pausePartOfScreenSharing=!1;connection.partOfScreen&&(connection.partOfScreen.sharing=!0)},connection.stopPartOfScreenSharing=function(){for(var peer in connection.peers)connection.peers[peer].stopPartOfScreenSharing=!0;connection.partOfScreen&&(connection.partOfScreen.sharing=!1)},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),element||(element=document.getElementById(element))),!element)throw"HTML Element is inaccessible!";window.html2canvas(element,{onrendered:function(canvas){callback(canvas.toDataURL())}})},connection.onScreenCapturingExtensionAvailable=function(){log("It seems that screen capturing extension is installed and available on your system!")},!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){return event.peer.numOfRetries||(event.peer.numOfRetries=0),event.peer.numOfRetries++,event.peer.numOfRetries>2?void(event.peer.numOfRetries=0):void(isFirefox||"firefox"===event.targetuser.browser?(error("ICE connectivity check is failed. Re-establishing peer connection."),event.peer.redial()):(error("ICE connectivity check is failed. Renegotiating peer connection."),event.peer.renegotiate()))},connection.onconnected=function(event){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),isEmpty(connection.channels)||connection.channels[event.userid]&&(connection.channels[event.userid].send({checkingPresence:!0}),setTimeout(function(){return connection.peers[event.userid].connected?void delete connection.peers[event.userid].connected:(connection.streams.remove({remote:!0,userid:event.userid}),void connection.remove(event.userid))},3e3))},connection.onstreamid=function(event){log("got remote streamid",event.streamid,"from",event.userid)},connection.stopMediaStream=function(mediaStream){if(!mediaStream)throw"MediaStream argument is mandatory.";if(connection.keepStreamsOpened)return void(mediaStream.onended&&mediaStream.onended());connection.localStreams[mediaStream.streamid]&&delete connection.localStreams[mediaStream.streamid],isFirefox&&mediaStream.onended&&mediaStream.onended();var checkForMediaStreamTrackStop=Boolean((mediaStream.getAudioTracks||mediaStream.getVideoTracks)&&(mediaStream.getAudioTracks()[0]&&!mediaStream.getAudioTracks()[0].stop||mediaStream.getVideoTracks()[0]&&!mediaStream.getVideoTracks()[0].stop));return!mediaStream.getAudioTracks||checkForMediaStreamTrackStop?void(mediaStream.stop&&mediaStream.stop()):(mediaStream.getAudioTracks().length&&mediaStream.getAudioTracks()[0].stop&&mediaStream.getAudioTracks().forEach(function(track){track.stop()}),void(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()},connection.openSignalingChannel=function(config){if(!window.Firebase)return loadScript(connection.resources.firebase,function(){connection.openSignalingChannel(config)});var channel=config.channel||connection.channel;connection.firebase&&(connection.resources.firebaseio=connection.resources.firebaseio.replace("//chat.","//"+connection.firebase+"."));var firebase=new window.Firebase(connection.resources.firebaseio+channel);firebase.channel=channel,firebase.on("child_added",function(data){config.onmessage(data.val())}),firebase.send=function(data){for(var prop in data)(isNull(data[prop])||"function"==typeof data[prop])&&(data[prop]=!1);this.push(data)},connection.socket||(connection.socket=firebase),firebase.onDisconnect().remove(),setTimeout(function(){config.callback(firebase)},1)},connection.Plugin=Plugin}window.RMCDefaultChannel=location.href.replace(/\/|:|#|\?|\$|\^|%|\.|`|~|!|@|\[|\||]|\|*. /g,"").split("\n").join("").split("\r").join(""),window.RTCMultiConnection=function(channel){function initRTCMultiSession(onSignalingReady){return screenFrame&&loadScreenFrame(),rtcMultiSession?onSignalingReady():void(rtcMultiSession=new RTCMultiSession(connection,onSignalingReady))}function joinSession(session,joinAs){if(isString(session)&&(connection.skipOnNewSession=!0),!rtcMultiSession)return log("Signaling channel is not ready. Connecting..."),void initRTCMultiSession(function(){log("Signaling channel is connected. Joining the session again..."),setTimeout(function(){joinSession(session,joinAs)},1e3)});if(isString(session)){if(!connection.sessionDescriptions[session])return setTimeout(function(){log("Session-Descriptions not found. Rechecking.."),joinSession(session,joinAs)},1e3);session=connection.sessionDescriptions[session]}if(joinAs)return captureUserMedia(function(){session.oneway=!0,joinSession(session)},joinAs);if(!session||!session.userid||!session.sessionid){error("missing arguments",arguments);var err='Invalid data passed over "connection.join" method.';throw connection.onstatechange({userid:"browser",extra:{},name:"Unexpected data detected.",reason:err}),err}connection.dontOverrideSession||(connection.session=session.session);var extra=connection.extra||session.extra||{};session.oneway||isData(session)?rtcMultiSession.joinSession(session,extra):captureUserMedia(function(){rtcMultiSession.joinSession(session,extra)})}function captureUserMedia(callback,_session,dontCheckChromExtension){function onIFrameCallback(event){if(event.data&&event.data.chromeMediaSourceId){window.removeEventListener("message",onIFrameCallback);var sourceId=event.data.chromeMediaSourceId;if(DetectRTC.screen.sourceId=sourceId,DetectRTC.screen.chromeMediaSource="desktop","PermissionDeniedError"===sourceId){var mediaStreamError={message:"https:"===location.protocol?"User denied to share content of his screen.":SCREEN_COMMON_FAILURE,name:"PermissionDeniedError",constraintName:screenConstraints,session:session};return currentUserMediaRequest.mutex=!1,DetectRTC.screen.sourceId=null,connection.onMediaError(mediaStreamError)}captureUserMedia(callback,_session)}event.data&&event.data.chromeExtensionStatus&&(warn("Screen capturing extension status is:",event.data.chromeExtensionStatus),DetectRTC.screen.chromeMediaSource="screen",captureUserMedia(callback,_session,!0))}function _captureUserMedia(forcedConstraints,forcedCallback,isRemoveVideoTracks,dontPreventSSLAutoAllowed){if(connection.onstatechange({userid:"browser",extra:{},name:"fetching-usermedia",reason:"About to capture user-media with constraints: "+toStr(forcedConstraints)}),connection.preventSSLAutoAllowed&&!dontPreventSSLAutoAllowed&&isChrome)return navigator.customGetUserMediaBar?void navigator.customGetUserMediaBar(forcedConstraints,function(){_captureUserMedia(forcedConstraints,forcedCallback,isRemoveVideoTracks,!0)},function(){connection.onMediaError({name:"PermissionDeniedError",message:"User denied permission.",constraintName:forcedConstraints,session:session})}):void loadScript(connection.resources.customGetUserMediaBar,function(){_captureUserMedia(forcedConstraints,forcedCallback,isRemoveVideoTracks,dontPreventSSLAutoAllowed)});var mediaConfig={onsuccess:function(stream,returnBack,idInstance,streamid){onStreamSuccessCallback(stream,returnBack,idInstance,streamid,forcedConstraints,forcedCallback,isRemoveVideoTracks,screenConstraints,constraints,session)},onerror:function(e,constraintUsed){var mediaStreamError;if(isFirefox&&"PERMISSION_DENIED"===e&&(e={message:"",name:"PermissionDeniedError",constraintName:constraintUsed,session:session}),isFirefox&&constraintUsed.video&&constraintUsed.video.mozMediaSource)return mediaStreamError={message:firefoxScreenCapturingWarning,name:e.name||"PermissionDeniedError",constraintName:constraintUsed,session:session},void connection.onMediaError(mediaStreamError); +if(isString(e))return connection.onMediaError({message:"Unknown Error",name:e,constraintName:constraintUsed,session:session});!e.name||"PermissionDeniedError"!==e.name&&"DevicesNotFoundError"!==e.name||(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.",e.message&&e.message.length&&(mediaStreamError+="\n "+e.message),mediaStreamError={message:mediaStreamError,name:e.name,constraintName:constraintUsed,session:session},connection.onMediaError(mediaStreamError),isChrome&&(session.audio||session.video)&&DetectRTC.load(function(){session.audio&&!DetectRTC.hasMicrophone&&(warn("It seems that you have no microphone attached to your device/system."),session.audio=session.audio=!1,session.video||(alert("It seems that you are capturing microphone and there is no device available or access is denied. Reloading..."),location.reload())),session.video&&!DetectRTC.hasWebcam&&(warn("It seems that you have no webcam attached to your device/system."),session.video=session.video=!1,session.audio||(alert("It seems that you are capturing webcam and there is no device available or access is denied. Reloading..."),location.reload())),DetectRTC.hasMicrophone||DetectRTC.hasWebcam?connection.getUserMediaPromptedOnce||(connection.getUserMediaPromptedOnce=!0,captureUserMedia(callback,session)):(alert("It seems that either both microphone/webcam are not available or access is denied. Reloading..."),location.reload())})),e.name&&"ConstraintNotSatisfiedError"===e.name&&(mediaStreamError="Either: ",mediaStreamError+="\n You are prompting unknown media resolutions.",mediaStreamError+="\n You are using invalid media constraints.",e.message&&e.message.length&&(mediaStreamError+="\n "+e.message),mediaStreamError={message:mediaStreamError,name:e.name,constraintName:constraintUsed,session:session},connection.onMediaError(mediaStreamError)),session.screen&&(isFirefox?error(firefoxScreenCapturingWarning):"https:"!==location.protocol?isNodeWebkit||"file:"!==location.protocol&&"http:"!==location.protocol||error("You cannot use HTTP or file protocol for screen capturing. You must either use HTTPs or chrome extension page or Node-Webkit page."):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=!1;var idInstance=JSON.stringify(constraintUsed);currentUserMediaRequest.streams[idInstance]&&delete currentUserMediaRequest.streams[idInstance]},mediaConstraints:connection.mediaConstraints||{}};mediaConfig.constraints=forcedConstraints||constraints,mediaConfig.connection=connection,getUserMedia(mediaConfig)}var session=_session||connection.session;if(isEmpty(session))return void(callback&&callback());if(connection.dontCaptureUserMedia)return callback();if(isData(session)||!connection.isInitiator&&session.oneway)return connection.attachStreams=[],callback();var constraints={audio:session.audio?{mandatory:{},optional:[{chromeRenderToAssociatedSink:!0}]}:!1,video:!!session.video};if(connection._mediaSources.audio&&constraints.audio.optional.push({sourceId:connection._mediaSources.audio}),connection._mediaSources.video&&(constraints.video={optional:[{sourceId:connection._mediaSources.video}]}),!session.screen&&!constraints.audio&&!constraints.video)return callback();var screenConstraints={audio:!1,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("https:"!==location.protocol)return error(SCREEN_COMMON_FAILURE);warn(firefoxScreenCapturingWarning),screenConstraints.video=merge(screenConstraints.video.mandatory,{mozMediaSource:"window",mediaSource:"window"}),constraints.audio&&(screenConstraints.audio=!0,constraints={}),delete screenConstraints.video.chromeMediaSource}if(session.screen){if(isChrome&&DetectRTC.screen.extensionid!==ReservedExtensionID&&(useCustomChromeExtensionForScreenCapturing=!0),isChrome&&!useCustomChromeExtensionForScreenCapturing&&!dontCheckChromExtension&&!DetectRTC.screen.sourceId)return listenEventHandler("message",onIFrameCallback),screenFrame||loadScreenFrame(),void screenFrame.postMessage();if(isChrome&&useCustomChromeExtensionForScreenCapturing&&!dontCheckChromExtension&&"screen"===DetectRTC.screen.chromeMediaSource&&DetectRTC.screen.extensionid)return DetectRTC.screen.extensionid===ReservedExtensionID&&-1===document.domain.indexOf("webrtc-experiment.com")?captureUserMedia(callback,_session,!0):(log("checking if chrome extension is installed."),void DetectRTC.screen.getChromeExtensionStatus(function(status){"installed-enabled"===status&&(DetectRTC.screen.chromeMediaSource="desktop"),captureUserMedia(callback,_session,!0),log("chrome extension is installed?","desktop"===DetectRTC.screen.chromeMediaSource)}));if(isChrome&&useCustomChromeExtensionForScreenCapturing&&"desktop"===DetectRTC.screen.chromeMediaSource&&!DetectRTC.screen.sourceId)return void DetectRTC.screen.getSourceId(function(sourceId){if("PermissionDeniedError"===sourceId){var mediaStreamError={message:"User denied to share content of his screen.",name:"PermissionDeniedError",constraintName:screenConstraints,session:session};return currentUserMediaRequest.mutex=!1,DetectRTC.screen.chromeMediaSource="desktop",connection.onMediaError(mediaStreamError)}return"No-Response"===sourceId?(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",captureUserMedia(callback,_session,!0)):void captureUserMedia(callback,_session,!0)});isChrome&&"desktop"===DetectRTC.screen.chromeMediaSource&&(screenConstraints.video.mandatory.chromeMediaSourceId=DetectRTC.screen.sourceId);var _isFirstSession=isFirstSession;_captureUserMedia(screenConstraints,constraints.audio||constraints.video?function(){_isFirstSession&&(isFirstSession=!0),_captureUserMedia(constraints,callback)}:callback)}else _captureUserMedia(constraints,callback,session.audio&&!session.video)}function onStreamSuccessCallback(stream,returnBack,idInstance,streamid,forcedConstraints,forcedCallback,isRemoveVideoTracks,screenConstraints,constraints,session){streamid||(streamid=getRandomString()),connection.onstatechange({userid:"browser",extra:{},name:"usermedia-fetched",reason:"Captured user media using constraints: "+toStr(forcedConstraints)}),isRemoveVideoTracks&&(stream=convertToAudioStream(stream)),connection.localStreamids.push(streamid),stream.onended=function(){streamedObject.mediaElement&&!streamedObject.mediaElement.parentNode&&document.getElementById(stream.streamid)&&(streamedObject.mediaElement=document.getElementById(stream.streamid)),connection.attachStreams.forEach(function(_stream,index){_stream===stream&&(delete connection.attachStreams[index],connection.attachStreams=swap(connection.attachStreams))}),onStreamEndedHandler(streamedObject,connection),connection.streams[streamid]&&connection.removeStream(streamid);var _stream=connection.streams[streamid];_stream&&_stream.sockets.length&&_stream.sockets.forEach(function(socket){socket.send({streamid:_stream.streamid,stopped:!0})}),currentUserMediaRequest.mutex=!1,currentUserMediaRequest.streams[idInstance]&&delete currentUserMediaRequest.streams[idInstance],DetectRTC.screen.sourceId=null},isIE||(stream.streamid=streamid,stream.isScreen=forcedConstraints===screenConstraints,stream.isVideo=forcedConstraints===constraints&&!!constraints.video,stream.isAudio=forcedConstraints===constraints&&!!constraints.audio&&!constraints.video,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=!0;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};isFirstSession&&connection.attachStreams.push(stream),isFirstSession=!1,connection.streams[streamid]=connection._getStream(streamedObject),returnBack||connection.onstream(streamedObject),connection.setDefaultEventsForMediaElement&&connection.setDefaultEventsForMediaElement(mediaElement,streamid),forcedCallback&&forcedCallback(stream,streamedObject),connection.onspeaking&&initHark({stream:stream,streamedObject:streamedObject,connection:connection})}var rtcMultiSession,connection=this;connection.channel=channel||RMCDefaultChannel,connection.isAcceptNewSession=!0,connection.open=function(args){connection.isAcceptNewSession=!1,connection.isInitiator=!0;var dontTransmit=!1;return args&&(isString(args)?connection.sessionid=args:(isNull(args.transmitRoomOnce)||(connection.transmitRoomOnce=args.transmitRoomOnce),isNull(args.dontTransmit)||(dontTransmit=args.dontTransmit),isNull(args.sessionid)||(connection.sessionid=args.sessionid))),connection.socket&&connection.socket.remove&&connection.socket.remove(),connection.sessionid||(connection.sessionid=connection.channel),connection.sessionDescription={sessionid:connection.sessionid,userid:connection.userid,session:connection.session,extra:connection.extra},connection.sessionDescriptions[connection.sessionDescription.sessionid]||(connection.numberOfSessions++,connection.sessionDescriptions[connection.sessionDescription.sessionid]=connection.sessionDescription),initRTCMultiSession(function(){rtcMultiSession.captureUserMediaOnDemand=args?!!args.captureUserMediaOnDemand:!1,args&&args.onMediaCaptured&&(connection.onMediaCaptured=args.onMediaCaptured),rtcMultiSession.captureUserMediaOnDemand||captureUserMedia(function(){rtcMultiSession.initSession({sessionDescription:connection.sessionDescription,dontTransmit:dontTransmit}),invokeMediaCaptured(connection)}),rtcMultiSession.captureUserMediaOnDemand&&rtcMultiSession.initSession({sessionDescription:connection.sessionDescription,dontTransmit:dontTransmit})}),connection.sessionDescription},connection.connect=function(sessionid){return sessionid&&(connection.sessionid=sessionid),initRTCMultiSession(function(){log("Signaling channel is ready.")}),this},connection.join=joinSession,connection.send=function(data,_channel){if(connection.numberOfConnectedUsers<=0)return void setTimeout(function(){connection.send(data,_channel)},1e3);if(!data)throw"No file, data or text message to share.";if(data instanceof Array&&!isNull(data[0].size)&&!isNull(data[0].type))for(var i=0;ipacketSize?data.message=text.slice(0,packetSize):(data.message=text,data.last=!0,data.isobject=isobject),channel.send(data,_channel),textToTransfer=text.slice(data.message.length),textToTransfer.length&&setTimeout(function(){sendText(null,textToTransfer)},connection.chunkInterval||100)}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||1e3,textToTransfer="",isobject=!1;isString(initialText)||(isobject=!0,initialText=JSON.stringify(initialText));var uuid=getRandomString(),sendingTime=(new Date).getTime();sendText(initialText)}},webAudioMediaStreamSources=[],isOpera=!!window.opera||navigator.userAgent.indexOf(" OPR/")>=0,isFirefox="undefined"!=typeof window.InstallTrigger,isSafari=Object.prototype.toString.call(window.HTMLElement).indexOf("Constructor")>0,isChrome=!!window.chrome&&!isOpera,isIE=!!document.documentMode,isPluginRTC=isSafari||isIE,isMobileDevice=!!navigator.userAgent.match(/Android|iPhone|iPad|iPod|BlackBerry|IEMobile/i),isNodeWebkit=!!(window.process&&"object"==typeof window.process&&window.process.versions&&window.process.versions["node-webkit"]);window.MediaStream=window.MediaStream||window.webkitMediaStream,window.AudioContext=window.AudioContext||window.webkitAudioContext;var chromeVersion=50,matchArray=navigator.userAgent.match(/Chrom(e|ium)\/([0-9]+)\./);isChrome&&matchArray&&matchArray[2]&&(chromeVersion=parseInt(matchArray[2],10));var firefoxVersion=50;matchArray=navigator.userAgent.match(/Firefox\/(.*)/),isFirefox&&matchArray&&matchArray[1]&&(firefoxVersion=parseInt(matchArray[1],10));var console=window.console||{log:function(){},error:function(){},warn:function(){}},log=function(){console.log(arguments)},error=function(){console.error(arguments)},warn=function(){console.warn(arguments)};(isChrome||isFirefox||isSafari)&&(log=console.log.bind(console),error=console.error.bind(console),warn=console.warn.bind(console));var screenFrame,loadedScreenFrame,loadedIceFrame,onStreamEndedHandlerFiredFor={},onLeaveHandlerFiredFor={},firefoxScreenCapturingWarning='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!',SCREEN_COMMON_FAILURE="HTTPs i.e. SSL-based URI is mandatory to use screen capturing.",ReservedExtensionID="ajhifddimkapgcifgcodmmfdlknahffk",useCustomChromeExtensionForScreenCapturing=-1!==document.domain.indexOf("webrtc-experiment.com"),MediaStreamTrack=window.MediaStreamTrack;window.addEventListener||(window.addEventListener=function(el,eventName,eventHandler){el.attachEvent&&el.attachEvent("on"+eventName,eventHandler)}),window.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.")},isEmpty(Plugin)||window.onPluginRTCInitialized(Plugin),isPluginRTC&&loadScript("https://cdn.webrtc-experiment.com/Plugin.EveryWhere.js");var DetectRTC={};!function(){function CheckDeviceSupport(callback){return(isPluginRTC||isFirefox)&&callback?void callback():(MediaStreamTrack&&MediaStreamTrack.getSources&&(navigator.getMediaDevices=MediaStreamTrack.getSources.bind(MediaStreamTrack)),navigator.getMediaDevices?void navigator.getMediaDevices(function(devices){DetectRTC.MediaDevices=[],devices.forEach(function(device){"audio"===device.kind&&(device.kind="audioinput"),"video"===device.kind&&(device.kind="videoinput"),device.deviceId||(device.deviceId=device.id),device.id||(device.id=device.deviceId),DetectRTC.MediaDevices.push(device),("audioinput"===device.kind||"audio"===device.kind)&&(DetectRTC.hasMicrophone=!0),"audiooutput"===device.kind&&(DetectRTC.hasSpeakers=!0),("videoinput"===device.kind||"video"===device.kind)&&(DetectRTC.hasWebcam=!0)}),callback&&callback()}):(log("navigator.getMediaDevices is undefined."),isChrome&&(DetectRTC.hasMicrophone=!0,DetectRTC.hasSpeakers=!0,DetectRTC.hasWebcam=!0),void(callback&&callback())))}DetectRTC.hasMicrophone=!1,DetectRTC.hasSpeakers=!1,DetectRTC.hasWebcam=!1,DetectRTC.MediaDevices=[],DetectRTC.isWebRTCSupported=!!window.webkitRTCPeerConnection||!!window.mozRTCPeerConnection,DetectRTC.isAudioContextSupported=!(!window.AudioContext&&!window.webkitAudioContext||!AudioContext.prototype.createMediaStreamSource),DetectRTC.isScreenCapturingSupported=isChrome&&chromeVersion>=26&&(isNodeWebkit?!0:"https:"===location.protocol),DetectRTC.isSctpDataChannelsSupported=!!navigator.mozGetUserMedia||isChrome&&chromeVersion>=25,DetectRTC.isRtpDataChannelsSupported=isChrome&&chromeVersion>=31,new CheckDeviceSupport,DetectRTC.load=CheckDeviceSupport;var screenCallback;DetectRTC.screen={chromeMediaSource:"screen",extensionid:ReservedExtensionID,getSourceId:function(callback){function onstatus(status){return"installed-enabled"===status?(screenCallback=callback,void window.postMessage("get-sourceId","*")):(DetectRTC.screen.chromeMediaSource="screen",void callback("No-Response"))}if(!callback)throw'"callback" parameter is mandatory.';DetectRTC.screen.status?onstatus(DetectRTC.screen.status):DetectRTC.screen.getChromeExtensionStatus(onstatus)},onMessageCallback:function(data){if(isString(data)||data.sourceId){if(log("chrome message",data),"PermissionDeniedError"===data){if(DetectRTC.screen.chromeMediaSource="PermissionDeniedError",screenCallback)return screenCallback("PermissionDeniedError");throw new Error("PermissionDeniedError")}"rtcmulticonnection-extension-loaded"===data&&(DetectRTC.screen.chromeMediaSource="desktop",DetectRTC.screen.onScreenCapturingExtensionAvailable&&(DetectRTC.screen.onScreenCapturingExtensionAvailable(),DetectRTC.screen.onScreenCapturingExtensionAvailable=null)),data.sourceId&&(DetectRTC.screen.sourceId=data.sourceId,screenCallback&&screenCallback(DetectRTC.screen.sourceId))}},getChromeExtensionStatus:function(extensionid,callback){function _callback(status){DetectRTC.screen.status=status,callback(status)}if(isFirefox)return _callback("not-chrome");2!==arguments.length&&(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(){_callback("screen"===DetectRTC.screen.chromeMediaSource?"desktop"===DetectRTC.screen.chromeMediaSource?"installed-enabled":"installed-disabled":"installed-enabled")},2e3)},image.onerror=function(){_callback("not-installed")}}}}(),window.addEventListener("message",function(event){event.origin===window.location.origin&&DetectRTC.screen.onMessageCallback(event.data)})}(); \ No newline at end of file diff --git a/RTCMultiConnection/bower.json b/RTCMultiConnection/bower.json new file mode 100644 index 00000000..eed0b8c2 --- /dev/null +++ b/RTCMultiConnection/bower.json @@ -0,0 +1,44 @@ +{ + "name": "rtcmulticonnection", + "description": "RTCMultiConnection is a WebRTC JavaScript wrapper library runs top over RTCPeerConnection API to provide multi-session establishment scenarios.", + "version": "2.2.4", + "authors": [ + { + "name": "Muaz Khan", + "email": "muazkh@gmail.com", + "homepage": "http://www.muazkhan.com/" + } + ], + "main": "./RTCMultiConnection.js", + "keywords": [ + "webrtc", + "rtcmulticonnection", + "webrtc-library", + "library", + "javascript", + "chrome", + "firefox", + "opera", + "ie", + "internet-explorer", + "android", + "rtcweb", + "rtcmulticonnection.js", + "multirtc", + "webrtc-experiment", + "javascript-library", + "muaz", + "muaz-khan" + ], + "repository": { + "type": "git", + "url": "https://github.com/muaz-khan/RTCMultiConnection.git" + }, + "homepage": "http://www.rtcmulticonnection.org", + "ignore": [ + "**/.*", + "node_modules", + "test", + "tests" + ] +} \ No newline at end of file diff --git a/RTCMultiConnection/demos/AppRTC-Look.html b/RTCMultiConnection/demos/AppRTC-Look.html index 56daae48..3e9d2f50 100644 --- a/RTCMultiConnection/demos/AppRTC-Look.html +++ b/RTCMultiConnection/demos/AppRTC-Look.html @@ -1,10 +1,10 @@ -AppRTC look for RTCMultiConnection Demo ® Muaz Khan +AppRTC look for RTCMultiConnection Demo ® Muaz Khan - + @@ -14,25 +14,25 @@ - - - - - - - - -
-
-

Multi-Broadcasters and Many Viewers using RTCMultiConnection.js!

-

- HOME - © - Muaz Khan - - . - @WebRTCWeb - - . - Github - - . - Latest issues - - . - What's New? -

-
- -
- - -
-

Your Role?



- -
- -
-
-
-

How it works?

-
    -
  1. There can be random number of broadcasters.
  2. -
  3. All broadcasters will be interconnected!
  4. -
  5. All broadcasters can see/talk with each other!
  6. -
  7. There can be random number of anonymous viewers.
  8. -
  9. All viewers can watch/listen videos from all broadcasters!
  10. -
-

Real-life scenarios?

-
    -
  1. 5 CEOs can setup a presentation with many employees!
  2. -
  3. 5 Teachers can teach many students in the same room!
  4. -
  5. Two ore more doctors can inspect two or more patients while LIVE teaching many students!
  6. -
-

How to use it?

-
    -
  1. 1st Tab: There MUST always be a room-moderator. Select "Room Moderator" and click "continue".
  2. -
  3. 2nd Tab: Select "Anonymous Viewer" if you want to anonymously watch/listen all videos.
  4. -
  5. 3rd Tab: Select "Broadcaster" if you want to setup two-way or multi-directional video chat with all other broadcasters.
  6. -
-
- - - -
-

Feedback

-
- -
- Enter your email too; if you want "direct" reply! -
- -
-

Latest Updates

-
-
-
- - - - - - - - - +--> + + + + + Multi-Broadcasters and Many Viewers using RTCMultiConnection ® Muaz Khan + + + + + + + + + + + + + + + + + +
+
+

Multi-Broadcasters and Many Viewers using RTCMultiConnection.js!

+

+ HOME + © + Muaz Khan . + @WebRTCWeb . + Github . + Latest issues . + What's New? +

+
+ +
+ + +
+

Your Role?

+
+
+ + +
+ +
+
+
+

How it works?

+
    +
  1. There can be random number of broadcasters.
  2. +
  3. All broadcasters will be interconnected!
  4. +
  5. All broadcasters can see/talk with each other!
  6. +
  7. There can be random number of anonymous viewers.
  8. +
  9. All viewers can watch/listen videos from all broadcasters!
  10. +
+

Real-life scenarios?

+
    +
  1. 5 CEOs can setup a presentation with many employees!
  2. +
  3. 5 Teachers can teach many students in the same room!
  4. +
  5. Two ore more doctors can inspect two or more patients while LIVE teaching many students!
  6. +
+

How to use it?

+
    +
  1. 1st Tab: There MUST always be a room-moderator. Select "Room Moderator" and click "continue".
  2. +
  3. 2nd Tab: Select "Anonymous Viewer" if you want to anonymously watch/listen all videos.
  4. +
  5. 3rd Tab: Select "Broadcaster" if you want to setup two-way or multi-directional video chat with all other broadcasters.
  6. +
+
+ + + +
+

Feedback

+
+ +
+ + Enter your email too; if you want "direct" reply! +
+ +
+

Latest Updates +

+
+
+
+ + + + + + + + + + \ No newline at end of file diff --git a/RTCMultiConnection/demos/OneWay-Screen-TwoWay-Audio.html b/RTCMultiConnection/demos/OneWay-Screen-TwoWay-Audio.html index 74637820..8c34b0ea 100644 --- a/RTCMultiConnection/demos/OneWay-Screen-TwoWay-Audio.html +++ b/RTCMultiConnection/demos/OneWay-Screen-TwoWay-Audio.html @@ -1,69 +1,71 @@ - - -OneWay Screen & Two-Way Audio using RTCMultiConnection - - - - -

OneWay Screen & Two-Way Audio using RTCMultiConnection



-
-
- - - - - -

This demo uses RTCMultiConnection.js to:

-
    -
  1. Share screen in one-way.
  2. -
  3. Share audio in two-way; among all users.
  4. -
-
-// initiator's session
-initiator.session = {
-    audio: true,
-    screen: true
-};
-
-participant.onNewSession = function (session) {
-    session.join(  { audio: true } );
-};
-
+--> + +OneWay Screen & Two-Way Audio using RTCMultiConnection + + + + +

OneWay Screen & Two-Way Audio using RTCMultiConnection +

+
+
+ +
+
+ + + + + +

This demo uses RTCMultiConnection.js to:

+
    +
  1. Share screen in one-way.
  2. +
  3. Share audio in two-way; among all users.
  4. +
+
+    // initiator's session initiator.session = { audio: true, screen: true }; participant.onNewSession = function (session) { session.join(
+    { audio: true } ); };
+
\ No newline at end of file diff --git a/RTCMultiConnection/demos/README.md b/RTCMultiConnection/demos/README.md index 33496386..fd9e5197 100644 --- a/RTCMultiConnection/demos/README.md +++ b/RTCMultiConnection/demos/README.md @@ -45,6 +45,8 @@ | takeSnapshot i.e. Take Snapshot of Local/Remote streams | [Demo](https://www.webrtc-experiment.com/RTCMultiConnection/takeSnapshot.html) | [Source](https://github.com/muaz-khan/RTCMultiConnection/tree/master/demos/takeSnapshot.html) | | Audio/Video/Screen sharing and recording | [Demo](https://www.webrtc-experiment.com/RTCMultiConnection/audio-video-screen-sharing-recording.html) | [Source](https://github.com/muaz-khan/RTCMultiConnection/tree/master/demos/audio-video-screen-sharing-recording.html) | | Broadcast Multiple-Cameras | [Demo](https://www.webrtc-experiment.com/RTCMultiConnection/Broadcast-Multiple-Cameras.html) | [Source](https://github.com/muaz-khan/RTCMultiConnection/tree/master/demos/Broadcast-Multiple-Cameras.html) | +| Remote Stream Forwarding | [Demo](https://www.webrtc-experiment.com/RTCMultiConnection/remote-stream-forwarding.html) | [Source](https://github.com/muaz-khan/RTCMultiConnection/tree/master/demos/remote-stream-forwarding.html) | +| WebRTC Scalable Broadcast | Socketio/Nodejs | [Source](https://github.com/muaz-khan/WebRTC-Scalable-Broadcast) | ## License diff --git a/RTCMultiConnection/demos/RTCMultiConnection-v1.3-and-socket.io.html b/RTCMultiConnection/demos/RTCMultiConnection-v1.3-and-socket.io.html index 2e43d674..b02ee07d 100644 --- a/RTCMultiConnection/demos/RTCMultiConnection-v1.3-and-socket.io.html +++ b/RTCMultiConnection/demos/RTCMultiConnection-v1.3-and-socket.io.html @@ -1,117 +1,118 @@ - - - - - - - - - - - - -
-

Local video container

-
-

Remote videos container

-
- - - - +--> + + + + + + + + + + + +
+

Local video container

+
+

Remote videos container

+
+ + + + \ No newline at end of file diff --git a/RTCMultiConnection/demos/RTCMultiConnection-v1.3-demo.html b/RTCMultiConnection/demos/RTCMultiConnection-v1.3-demo.html index 67f55696..d6e584c6 100644 --- a/RTCMultiConnection/demos/RTCMultiConnection-v1.3-demo.html +++ b/RTCMultiConnection/demos/RTCMultiConnection-v1.3-demo.html @@ -1,269 +1,265 @@ - - - - - - - RTCMultiConnection-v1.3.js testing demo! ® Muaz Khan - - - - - - - - - - - - - - - - - - - - - Goto "WebRTC Experiments & Demos" Homepage - -
-
-

How it works?

- -

Renegotiation means using pre-created peer connections to add additional streams by renegotiating offer/answer session descriptions.

-

In this demo; at first time; only data connection is opened. Later on, same peer connections are used to renegotiate audio, video or screen streams.

-

It is runtime insertion of streams!

-
-
-

Remeber, it is a testing demo for RTCMultiConnection-v1.3.js library. Documentation available here.

- - - - - - - - - - - -
-
- -
- - - - +--> + + + + + + RTCMultiConnection-v1.3.js testing demo! ® Muaz Khan + + + + + + + + + + + + + + + + + + + + + Goto "WebRTC Experiments & Demos" Homepage + +
+
+

How it works?

+ +

Renegotiation means using pre-created peer connections to add additional streams by renegotiating offer/answer session descriptions.

+

In this demo; at first time; only data connection is opened. Later on, same peer connections are used to renegotiate audio, video or screen streams.

+

It is runtime insertion of streams!

+
+
+

Remeber, it is a testing demo for RTCMultiConnection.js library. Documentation available here.

+ + + + + + + + + + + +
+
+ +
+ + + + \ No newline at end of file diff --git a/RTCMultiConnection/demos/RTCMultiConnection.sharePartOfScreen.html b/RTCMultiConnection/demos/RTCMultiConnection.sharePartOfScreen.html index 32cd99a1..8661b5b9 100644 --- a/RTCMultiConnection/demos/RTCMultiConnection.sharePartOfScreen.html +++ b/RTCMultiConnection/demos/RTCMultiConnection.sharePartOfScreen.html @@ -1,66 +1,79 @@ -RTCMultiConnection.sharePartOfScreen - +RTCMultiConnection.sharePartOfScreen + + @@ -74,18 +87,8 @@

Content is edit-able.


-document.getElementById('share-part-of-screen').onclick = function () {
-    connection.sharePartOfScreen({
-        element: '#share-me',
-        interval: 500
-    });
-};
-
-var image = document.getElementById('preview-image');
-connection.onpartofscreen = function (event) {
-    image.src = event.screenshot;
-};
-
+ document.getElementById('share-part-of-screen').onclick = function () { connection.sharePartOfScreen({ element: '#share-me', interval: 500 }); }; var image = document.getElementById('preview-image'); connection.onpartofscreen = function (event) { image.src = event.screenshot; }; +
@@ -94,7 +97,8 @@

Content is edit-able.

- + @@ -105,11 +109,11 @@

Content is edit-able.

data: true }; -connection.onopen = function () { +connection.onopen = function() { document.getElementById('share-part-of-screen').disabled = false; }; -document.getElementById('share-part-of-screen').onclick = function () { +document.getElementById('share-part-of-screen').onclick = function() { this.disabled = true; connection.sharePartOfScreen({ @@ -119,7 +123,7 @@

Content is edit-able.

}; var image = document.getElementById('preview-image'); -connection.onpartofscreen = function (event) { +connection.onpartofscreen = function(event) { this.disabled = true; image.src = event.screenshot; @@ -128,7 +132,7 @@

Content is edit-able.

connection.connect(); -document.getElementById('open-room').onclick = function () { +document.getElementById('open-room').onclick = function() { this.disabled = true; connection.open(); }; diff --git a/RTCMultiConnection/demos/RecordRTC-and-RTCMultiConnection.html b/RTCMultiConnection/demos/RecordRTC-and-RTCMultiConnection.html index 96811b6d..6a9dceb7 100644 --- a/RTCMultiConnection/demos/RecordRTC-and-RTCMultiConnection.html +++ b/RTCMultiConnection/demos/RecordRTC-and-RTCMultiConnection.html @@ -1,411 +1,388 @@ - - - - - RecordRTC & RTCMultiConnection ® Muaz Khan - - - - - - - - - - - - - - - - - -
-
-

- RecordRTC & - RTCMultiConnection - ® Muaz Khan -

-

- HOME - © - Muaz Khan - - . - @WebRTCWeb - - . - Github - - . - Latest issues - - . - What's New? -

-
- -
- - -
-
- - Private ?? #123456789 - - - - - -
- - -
- - -
-
- - - -
-
    -
  1. You can record video from both local and remote media streams (chrome,firefox,opera)
  2. -
  3. You can record audio from local stream (chrome,firefox,opera)
  4. -
  5. You can record "remote-audio" only on firefox
  6. -
  7. Many streams can be added; renegotiated; and recorded (simultaneously)!
  8. -
-
-
-

You can invoke audio/video recorder like this:

- -
-connection.streams['stream-id'].startRecording({
-    audio: true,
-    video: true
-});
-
-
-
-

You can skip argument:

-
-// to record both audio and video
-connection.streams['stream-id'].startRecording();
-
-
-
-

Stop recording and get blob.

-
    -
  1. - A JavaScritp object is returned with audio/video blobs e.g. blob.audio or blob.video. -
  2. -
- -
-connection.streams['stream-id'].stopRecording(function (blob) {
-    var mediaElement = document.createElement('audio');
-    mediaElement.src = URL.createObjectURL(blob.audio);
-    document.documentElement.appendChild(h2);
-});
-
-
-
-

You can force to stop a specific stream:

- -
-connection.streams['stream-id'].stopRecording(onBlobs, {
-    audio: true // stop audio recorder and get audio blob
-});
-
-
-
-

A simple working example:

- -
-connection.onstream = function (e) {
-    // e.type == 'remote' || 'local'
-	
-    connection.streams[e.streamid].startRecording({
-        video: true
-    });
-
-    // record 10 sec audio/video
-    var recordingInterval = 10 * 10000;
-
-    setTimeout(function () {
-        connection.streams[e.streamid].stopRecording(function (blob) {
-            var mediaElement = document.createElement('video');
-            mediaElement.src = URL.createObjectURL(blob.video);
-            document.documentElement.appendChild(h2);
-        });
-    }, recordingInterval)
-}
-
-
- -
-

Feedback

-
- -
- Enter your email too; if you want "direct" reply! -
- -
-

Latest Updates

-
-
-
- - - - - - - - - +--> + + + + + RecordRTC & RTCMultiConnection ® Muaz Khan + + + + + + + + + + + + + + + + + +
+
+

+ RecordRTC & + RTCMultiConnection ® Muaz Khan +

+

+ HOME + © + Muaz Khan . + @WebRTCWeb . + Github . + Latest issues . + What's New? +

+
+ +
+ + +
+
+ + Private ?? + + + #123456789 + + + + + + + +
+ + +
+ + +
+
+ + + +
+
    +
  1. You can record video from both local and remote media streams (chrome,firefox,opera)
  2. +
  3. You can record audio from local stream (chrome,firefox,opera)
  4. +
  5. You can record "remote-audio" only on firefox
  6. +
  7. Many streams can be added; renegotiated; and recorded (simultaneously)!
  8. +
+
+
+

You can invoke audio/video recorder like this:

+ +
+                connection.streams['stream-id'].startRecording({ audio: true, video: true });
+            
+
+
+

You can skip argument:

+
+                // to record both audio and video connection.streams['stream-id'].
+                startRecording();
+            
+
+
+

Stop recording and get blob.

+
    +
  1. + A JavaScritp object is returned with audio/video blobs e.g. blob.audio or blob.video. +
  2. +
+ +
+                connection.streams['stream-id'].stopRecording(function (blob) { var mediaElement = document.createElement('audio'); mediaElement.src = URL.createObjectURL(blob.audio); document.documentElement.appendChild(h2); });
+            
+
+
+

You can force to stop a specific stream:

+ +
+                connection.streams['stream-id'].stopRecording(onBlobs, { audio: true // stop audio recorder and get audio blob });
+            
+
+
+

A simple working example:

+ +
+                connection.onstream = function (e) { // e.type == 'remote' || 'local' connection.streams[e.streamid].
+
+                startRecording({ video: true }); // record 10 sec audio/video var recordingInterval = 10 * 10000; setTimeout(function () { connection.streams[e.streamid].
+                stopRecording(function (blob) { var mediaElement = document.createElement('video'); mediaElement.src = URL.createObjectURL(blob.video); document.documentElement.appendChild(h2); }); }, recordingInterval) }
+            
+
+ +
+

Feedback

+
+ +
+ + Enter your email too; if you want "direct" reply! +
+ +
+

Latest Updates +

+
+
+
+ + + + + + + + + + \ No newline at end of file diff --git a/RTCMultiConnection/demos/Renegotiation.html b/RTCMultiConnection/demos/Renegotiation.html index bb47b371..1ff67101 100644 --- a/RTCMultiConnection/demos/Renegotiation.html +++ b/RTCMultiConnection/demos/Renegotiation.html @@ -1,287 +1,282 @@ - - - - - - Renegotiation using RTCMultiConnection ® Muaz Khan - - - - - - - - - - - - - - - - - - - - - - Goto "WebRTC Experiments & Demos" Homepage -
-
-
-
-

- How it works?

-

- Renegotiation means using pre-created peer connections to add additional streams - by renegotiating offer/answer session descriptions.

-

- In this demo; at first time; only data connection is opened. Later on, same peer - connections are used to renegotiate audio, video or screen streams.

-

- It is runtime insertion of streams!

-
-
-

- Remeber, it is a testing demo for RTCMultiConnection-v1.4.js library. Documentation available here.

- - - -
-
- -
- - - +--> + + + + + + Renegotiation using RTCMultiConnection ® Muaz Khan + + + + + + + + + + + + + + + + + + + + + + + Goto "WebRTC Experiments & Demos" Homepage +
+
+
+
+

+ How it works?

+

+ Renegotiation means using pre-created peer connections to add additional streams by renegotiating offer/answer session descriptions.

+

+ In this demo; at first time; only data connection is opened. Later on, same peer connections are used to renegotiate audio, video or screen streams.

+

+ It is runtime insertion of streams!

+
+
+

+ Remeber, it is a testing demo for RTCMultiConnection-v1.4.js library. Documentation available here.

+ + + +
+
+ +
+ + + + \ No newline at end of file diff --git a/RTCMultiConnection/demos/admin-guest.html b/RTCMultiConnection/demos/admin-guest.html index ad0344eb..048bf51d 100644 --- a/RTCMultiConnection/demos/admin-guest.html +++ b/RTCMultiConnection/demos/admin-guest.html @@ -1,397 +1,397 @@ - - - - - - Admin/Guest audio/video calling using RTCMultiConnection ® Muaz Khan - - - - - - - - - - - - - - -

- Admin/Guest audio/video calling using / RTCMultiConnection -

- -

- HOME - © - Muaz Khan - - . - @WebRTCWeb - - . - Github - - . - Latest issues - - . - What's New? -

- -
-
- - -
-
- -

...

-
- - -
-
- -
-

Feedback

-
- -
- -
- - - - - - - - - +--> + + + + + + Admin/Guest audio/video calling using RTCMultiConnection ® Muaz Khan + + + + + + + + + + + + + + + +

+ Admin/Guest audio/video calling using / RTCMultiConnection +

+ +

+ HOME + © + Muaz Khan . + @WebRTCWeb . + Github . + Latest issues . + What's New? +

+ +
+
+ + +
+
+ +

...

+
+ + +
+
+ +
+

Feedback

+
+ +
+ +
+ + + + + + + + + + \ No newline at end of file diff --git a/RTCMultiConnection/demos/all-in-one.html b/RTCMultiConnection/demos/all-in-one.html index 76bba14a..cc1a8dab 100644 --- a/RTCMultiConnection/demos/all-in-one.html +++ b/RTCMultiConnection/demos/all-in-one.html @@ -1,529 +1,475 @@ - - - - - RTCMultiConnection All-in-One Test ® Muaz Khan - - - - - - - - - - - - - - - - - - -
-
-

- RTCMultiConnection - All-in-One test - ® - Muaz Khan -

-

- HOME - © - Muaz Khan - - . - @WebRTCWeb - - . - Github - - . - Latest issues - - . - What's New? -

-
- -
- - -
-
- - -
- -

- Select SessionType and Direction-of-Flow! -

- -
- - - - -
- - -
- - -
-
- -
-

WebRTC DataChannel

- - - - - -
-

Text Chat

- -
- -
-

Share Files

- - -
-
-
- - - -
-
    -
  1. Mesh networking model is implemented to open multiple (1:1) interconnected peer connections
  2. -
  3. Maximum peer connections limit per page is 256 (on chrome) i.e. 256 users can connect together!
  4. -
-
- -
-

How to use RTCMultiConnection?

- -
-// https://cdn.webrtc-experiment.com/RTCMultiConnection.js
-
-
- -
-

Common Code

- -
-var MODERATOR_CHANNEL_ID = 'ABCDEF'; // channel-id
-var MODERATOR_SESSION_ID = 'XYZ';    // room-id
-var MODERATOR_ID         = 'JKL';    // user-id
-var MODERATOR_SESSION    = {         // media-type
-    audio: true,
-    video: true
-};
-var MODERATOR_EXTRA      = {};       // empty extra-data
-
-
- -
-

Code for Room Moderator (i.e. Initiator)

- -
-var moderator     = new RTCMultiConnection(MODERATOR_CHANNEL_ID);
-moderator.session = MODERATOR_SESSION;
-moderator.userid  = MODERATOR_ID;
-moderator.extra   = MODERATOR_EXTRA;
-moderator.open({
-    dontTransmit: true,
-    sessionid:    MODERATOR_SESSION_ID
-});
-
-
-
- -
-

Code for Room Participants

- -
-var participants = new RTCMultiConnection(MODERATOR_CHANNEL_ID);
-participants.join({
-    sessionid: MODERATOR_SESSION_ID,
-    userid:    MODERATOR_ID,
-    extra:     MODERATOR_EXTRA,
-    session:   MODERATOR_SESSION
-});
-
-
- -
-

(optional) Handle how to get streams

- -
-// same code can be used for participants
-// (it is optional) 
-moderator.onstreamid = function(event) {
-    // got a clue of incoming remote stream
-    // didn't get remote stream yet
-    
-    var incoming_stream_id = event.streamid;
-    
-    YOUR_PREVIEW_IMAGE.show();
-    
-    // or
-    YOUR_PREVIEW_VIDEO.show();
-};
-
-// same code can be used for participants
-// it is useful
-moderator.onstream = function(event) {
-    // got local or remote stream
-    // if(event.type == 'local')  {}
-    // if(event.type == 'remote') {}
-    
-    document.body.appendChild(event.mediaElement);
-    
-    // or YOUR_VIDEO.src = event.blobURL;
-    // or YOUR_VIDEO.src = URL.createObjectURL(event.stream);
-};
-
-// same code can be used for participants
-// it is useful but optional
-moderator.onstreamended = function(event) {
-    event.mediaElement.parentNode.removeChild(event.mediaElement);
-};
-
-
- -
-

Feedback

-
- -
- Enter your email too; if you want "direct" reply! -
- -
-

Latest Updates

-
-
-
- - - - - - - - - +--> + + + + + RTCMultiConnection All-in-One Test ® Muaz Khan + + + + + + + + + + + + + + + + + + +
+
+

+ RTCMultiConnection All-in-One test ® + Muaz Khan +

+

+ HOME + © + Muaz Khan . + @WebRTCWeb . + Github . + Latest issues . + What's New? +

+
+ +
+ + +
+
+ + +
+ +

+ Select SessionType and Direction-of-Flow! +

+ +
+ + + + +
+ + +
+ + +
+
+ +
+

WebRTC DataChannel

+ + + + + +
+

Text Chat

+ +
+ +
+

Share Files

+ + +
+
+
+ + + +
+
    +
  1. Mesh networking model is implemented to open multiple (1:1) interconnected peer connections
  2. +
  3. Maximum peer connections limit per page is + 256 (on chrome) i.e. 256 users can connect together!
  4. +
+
+ +
+

How to use RTCMultiConnection?

+ +
+                // https://cdn.webrtc-experiment.com/RTCMultiConnection.js
+            
+
+ +
+

Common Code

+ +
+                var MODERATOR_CHANNEL_ID = 'ABCDEF'; // channel-id var MODERATOR_SESSION_ID = 'XYZ'; // room-id var MODERATOR_ID = 'JKL'; // user-id var MODERATOR_SESSION = { // media-type audio: true, video: true }; var MODERATOR_EXTRA = {}; // empty extra-data
+            
+
+ +
+

Code for Room Moderator (i.e. Initiator)

+ +
+                var moderator = new RTCMultiConnection(MODERATOR_CHANNEL_ID); moderator.session = MODERATOR_SESSION; moderator.userid = MODERATOR_ID; moderator.extra = MODERATOR_EXTRA; moderator.open({ dontTransmit: true, sessionid: MODERATOR_SESSION_ID });
+
+            
+
+ +
+

Code for Room Participants

+ +
+                var participants = new RTCMultiConnection(MODERATOR_CHANNEL_ID); participants.join({ sessionid: MODERATOR_SESSION_ID, userid: MODERATOR_ID, extra: MODERATOR_EXTRA, session: MODERATOR_SESSION });
+            
+
+ +
+

(optional) Handle how to get streams

+ +
+                // same code can be used for participants // (it is optional) moderator.onstreamid = function(event) { // got a clue of incoming remote stream // didn't get remote stream yet var incoming_stream_id = event.streamid; YOUR_PREVIEW_IMAGE.show(); // or YOUR_PREVIEW_VIDEO.show(); }; // same code can be used for participants // it is useful moderator.onstream = function(event) { // got local or remote stream // if(event.type == 'local') {} // if(event.type == 'remote') {} document.body.appendChild(event.mediaElement); // or YOUR_VIDEO.src = event.blobURL; // or YOUR_VIDEO.src = URL.createObjectURL(event.stream); }; // same code can be used for participants // it is useful but optional moderator.onstreamended = function(event) { event.mediaElement.parentNode.removeChild(event.mediaElement); };
+            
+
+ +
+

Feedback

+
+ +
+ + Enter your email too; if you want "direct" reply! +
+ +
+

Latest Updates +

+
+
+
+ + + + + + + + + + \ No newline at end of file diff --git a/RTCMultiConnection/demos/audio-conferencing-data-sharing.html b/RTCMultiConnection/demos/audio-conferencing-data-sharing.html index e3093136..77516b0a 100644 --- a/RTCMultiConnection/demos/audio-conferencing-data-sharing.html +++ b/RTCMultiConnection/demos/audio-conferencing-data-sharing.html @@ -1,377 +1,385 @@ - - - - - - audio-conferencing + file-sharing + text-chat using RTCMultiConnection ® Muaz Khan - - - - - - - - - - - - - - - -
- ↑ WEBRTC EXPERIMENTS - - -

- audio-conferencing and data-sharing - RTCMultiConnection -

- -

- Copyright © 2013 Muaz - Khan<@muazkh>. -

-
-
-
- -
-

Open New Session:

- -
- - - - - - - - -
-

Local Media Stream

- -
-
-
-

Share Files

- - -
- -

Text Chat

- -
- -
-

Remote Media Streams

- -
-
- -
-
- -

Getting started with RTCMultiConnection

-
-<script src="https://www.webrtc-experiment.com/RTCMultiConnection-v1.2.js"></script>
-<script>
-    var room = new RTCMultiConnection('session-id');
-    room.session = 'audio + data';
-    if(room-owner) room.open();
-</script>
-
-
-
- -
-

Feedback

-
- -
- -
-
- - - - +--> + + + + + audio-conferencing + file-sharing + text-chat using RTCMultiConnection ® Muaz Khan + + + + + + + + + + + + + + + +
+ ↑ WEBRTC EXPERIMENTS + + +

+ audio-conferencing and data-sharing + RTCMultiConnection +

+ +

+ Copyright © 2013 Muaz + + + + Khan + <@muazkh + >. +

+
+
+
+ +
+

Open New Session:

+ +
+ + + + + + + + +
+

Local Media Stream

+ +
+
+
+

Share Files

+ + +
+ +

Text Chat

+ +
+ +
+

Remote Media Streams

+ +
+
+ +
+
+ +

Getting started with RTCMultiConnection +

+
+            <script src="https://www.webrtc-experiment.com/RTCMultiConnection-v1.2.js"></script> <script> var room = new
+            RTCMultiConnection('session-id'); room.session = 'audio + data'; if(room-owner) room.
+            open(); </script>
+        
+
+
+ +
+

Feedback

+
+ +
+ +
+
+ + + + + \ No newline at end of file diff --git a/RTCMultiConnection/demos/audio-video-screen-sharing-recording.html b/RTCMultiConnection/demos/audio-video-screen-sharing-recording.html index 7872a5e7..40ccc55f 100644 --- a/RTCMultiConnection/demos/audio-video-screen-sharing-recording.html +++ b/RTCMultiConnection/demos/audio-video-screen-sharing-recording.html @@ -1,5 +1,6 @@ -Audio/Video/Screen sharing and recording using RTCMultiConnection -

Audio/Video/Screen sharing and recording using RTCMultiConnection / Source Code

+Audio/Video/Screen sharing and recording using RTCMultiConnection +

Audio/Video/Screen sharing and recording using RTCMultiConnection / Source Code +


@@ -10,9 +11,15 @@

Audio/Video/Screen sharing and recording using - - - + + + + + + - - - - - - - -
-
-

- WebRTC AudioConferencing using - RTCMultiConnection -

-

- HOME - © - Muaz Khan - - . - @WebRTCWeb - - . - Github - - . - Latest issues - - . - What's New? -

-
- -
- - -
-
- - Private ?? #123456789 - - - - -
- - -
- - -
-
- - - -
-

Feedback

-
- -
- Enter your email too; if you want "direct" reply! -
- -
-

Latest Updates

-
-
-
- - - - - - - - - +--> + + + + + WebRTC AudioConferencing using RTCMultiConnection ® Muaz Khan + + + + + + + + + + + + + + + + + +
+
+

+ WebRTC AudioConferencing using + RTCMultiConnection +

+

+ HOME + © + Muaz Khan . + @WebRTCWeb . + Github . + Latest issues . + What's New? +

+
+ +
+ + +
+
+ + Private ?? + + + #123456789 + + + + + + +
+ + +
+ + +
+
+ + + +
+

Feedback

+
+ +
+ + Enter your email too; if you want "direct" reply! +
+ +
+

Latest Updates +

+
+
+
+ + + + + + + + + + \ No newline at end of file diff --git a/RTCMultiConnection/demos/bandwidth.html b/RTCMultiConnection/demos/bandwidth.html index 461e21d0..31c7a4dd 100644 --- a/RTCMultiConnection/demos/bandwidth.html +++ b/RTCMultiConnection/demos/bandwidth.html @@ -1,419 +1,422 @@ - - - - - - Customizing Bandwidth using RTCMultiConnection ® Muaz Khan - - - - - - - - - - - - - - - -
- ↑ - WEBRTC EXPERIMENTS - - -

Customizing Bandwidth using RTCMultiConnection

- -

Copyright © - Muaz Khan<@muazkh>. - -

- -
-

New Session:

- - -
- - - - - - - - -
-

Local video container

- -
-

Remote videos - container

- -
-
-
- - -
- -

You can customize bandwidth using URL parameters!

- -
- -

Go ahead and try RTCMultiConnection!

- -

Pass "session-id" only-over the constructor:

-
-connection = new RTCMultiConnection('session-id');
-connection.bandwidth = {
-    audio: 50,
-    video: 256
-};
-connection.open();
-
- -
-
-

What is RTCMultiConnection?

- -
-
RTCMultiConnection.js a javascript library supports features like audio/video conferencing; (one-way and one-to-many) broadcasting; screen sharing; data/text/file sharing (of any size); multi-and-manual sessions establishment; users ejection, rejection - and presence detection; and more. It automatically keeps session "active" all the time; even if initiator leaves. -
-
-
-

Feedback

- -
- -
- -
-
- - - - - +--> + + + + + Customizing Bandwidth using RTCMultiConnection ® Muaz Khan + + + + + + + + + + + + + + + +
+ ↑ + + + + WEBRTC EXPERIMENTS + + +

Customizing Bandwidth using RTCMultiConnection +

+ +

+ Copyright © + Muaz Khan + <@muazkh + >. + +

+ +
+

New Session:

+ + +
+ + + + + + + + +
+

Local video container

+ +
+

Remote videos container +

+ +
+
+
+ + +
+ +

You can customize bandwidth using URL parameters!

+ +
+ +

Go ahead and try RTCMultiConnection!

+ +

Pass "session-id" only-over the constructor:

+
+            connection = new RTCMultiConnection('session-id'); connection.bandwidth = { audio: 50, video: 256 }; connection.open();
+        
+ +
+
+

What is RTCMultiConnection?

+ +
+
RTCMultiConnection.js a javascript library supports features like audio/video conferencing; (one-way and one-to-many) broadcasting; screen sharing; data/text/file sharing (of any size); multi-and-manual sessions establishment; users ejection, rejection and presence detection; and more. It automatically keeps session "active" all the time; even if initiator leaves. +
+
+
+

Feedback

+ +
+ +
+ +
+
+ + + + + \ No newline at end of file diff --git a/RTCMultiConnection/demos/changeBandwidth.html b/RTCMultiConnection/demos/changeBandwidth.html index 272d513e..2338ef31 100644 --- a/RTCMultiConnection/demos/changeBandwidth.html +++ b/RTCMultiConnection/demos/changeBandwidth.html @@ -2,7 +2,8 @@ -
+ +
- \ No newline at end of file + \ No newline at end of file diff --git a/RTCMultiConnection/demos/chrome-to-firefox-screen-sharing.html b/RTCMultiConnection/demos/chrome-to-firefox-screen-sharing.html index 1877a350..952d8ed8 100644 --- a/RTCMultiConnection/demos/chrome-to-firefox-screen-sharing.html +++ b/RTCMultiConnection/demos/chrome-to-firefox-screen-sharing.html @@ -1,44 +1,51 @@ - -

Chrome-to-Firefox Screen Sharing Demo / using RTCMultiConnection

- -
- -
- - - - +--> +

Chrome-to-Firefox Screen Sharing Demo / using RTCMultiConnection +

+ +
+ +
+ + + + \ No newline at end of file diff --git a/RTCMultiConnection/demos/cropped-screen-sharing.html b/RTCMultiConnection/demos/cropped-screen-sharing.html index 2516e87b..b7c7613d 100644 --- a/RTCMultiConnection/demos/cropped-screen-sharing.html +++ b/RTCMultiConnection/demos/cropped-screen-sharing.html @@ -1,229 +1,233 @@ - + + WebRTC Cropped Screen Sharing using RTCMultiConnection - + - + - - -

WebRTC Cropped Screen Sharing using RTCMultiConnection

+ + + +

WebRTC Cropped Screen Sharing using RTCMultiConnection +

- This example explains how to crop remote stream. Cropping code is taken from this link. See how to test this demo?. + This example explains how to crop remote stream. Cropping code is taken from this link. See how to test this demo?.

- - + +
- - + +
- - + +
- - + +
- +

Cropped Screen:

- + - + - + + \ No newline at end of file diff --git a/RTCMultiConnection/demos/features.html b/RTCMultiConnection/demos/features.html index f9e3bba4..e18e3f70 100644 --- a/RTCMultiConnection/demos/features.html +++ b/RTCMultiConnection/demos/features.html @@ -1,137 +1,142 @@ - -RTCMultiConnection Features Demo -

RTCMultiConnection Features Demo / Goto Homepage

- -
- - - - - - - - -
- - - - - +--> +RTCMultiConnection Features Demo +

RTCMultiConnection Features Demo / Goto Homepage +

+ +
+ + + + + + + + +
+ + + + + \ No newline at end of file diff --git a/RTCMultiConnection/demos/getMediaDevices.html b/RTCMultiConnection/demos/getMediaDevices.html index c134f52b..e1ee4233 100644 --- a/RTCMultiConnection/demos/getMediaDevices.html +++ b/RTCMultiConnection/demos/getMediaDevices.html @@ -1,189 +1,180 @@ - - - navigator.getMediaDevices / navigator.enumerateDevices / MediaStreamTrack.getSources using RTCMultiConnection ® Muaz Khan - - - - - - - - - - + + + + + + + +
+
+

navigator.getMediaDevices / navigator.enumerateDevices / MediaStreamTrack.getSources using RTCMultiConnection.js!

+

+ HOME + © + Muaz Khan . + @WebRTCWeb . + Github . + Latest issues . + What's New? +

+
+ +
+ + +
+

Select Media Device

+
+
+ + +
+
+ +
+ +
+
- li { - border-bottom: 1px solid rgb(189, 189, 189); - border-left: 1px solid rgb(189, 189, 189); - padding: .5em; - } - - - - - - - -
-
-

navigator.getMediaDevices / navigator.enumerateDevices / MediaStreamTrack.getSources using RTCMultiConnection.js!

-

- HOME - © - Muaz Khan - - . - @WebRTCWeb - - . - Github - - . - Latest issues - - . - What's New? -

-
- -
- - -
-

Select Media Device



- -

-
- -
-
- - - -
-

How to get media devices?

-
-connection.enumerateDevices(function(devices) {
-    devices.forEach(function (device) {
-        // device.kind == 'audioinput' || 'audiooutput' || 'audio'
-        // device.kind == 'videoinput' || 'videooutput' || 'video'
-        // device.deviceId
-
-        // connection.selectDevices('deviceId');
-        // connection.captureUserMedia();
-    });
-});
-
-
- -
-

Reference Links

-
    -
  1. - http://www.rtcmulticonnection.org/docs/getDevices/ -
  2. - -
  3. - http://www.rtcmulticonnection.org/docs/selectDevices/ -
  4. -
-
- -
-

Feedback

-
- -
- Enter your email too; if you want "direct" reply! -
- -
-

Latest Updates

-
-
-
- - - - - - - - - + +
+

How to get media devices?

+
+                connection.enumerateDevices(function(devices) { devices.forEach(function (device) { // device.kind == 'audioinput' || 'audiooutput' || 'audio' // device.kind == 'videoinput' || 'videooutput' || 'video' // device.deviceId // connection.selectDevices('deviceId'); // connection.captureUserMedia(); }); });
+            
+
+ +
+

Reference Links

+
    +
  1. + http://www.rtcmulticonnection.org/docs/getDevices/ +
  2. + +
  3. + http://www.rtcmulticonnection.org/docs/selectDevices/ +
  4. +
+
+ +
+

Feedback

+
+ +
+ + Enter your email too; if you want "direct" reply! +
+ +
+

Latest Updates +

+
+
+
+ + + + + + + + + + \ No newline at end of file diff --git a/RTCMultiConnection/demos/group-file-sharing-plus-text-chat.html b/RTCMultiConnection/demos/group-file-sharing-plus-text-chat.html index f0ca2073..90cbeb27 100644 --- a/RTCMultiConnection/demos/group-file-sharing-plus-text-chat.html +++ b/RTCMultiConnection/demos/group-file-sharing-plus-text-chat.html @@ -1,296 +1,244 @@ - - - - - - File Sharing + Text Chat using RTCMultiConnection ® Muaz Khan - - - - - - - - - - - - - - - - - - - -
-
-

File Sharing + Text Chat using RTCMultiConnection -

- -

- © - Muaz Khan - - . - @WebRTCWeb - - . - Github - - . - Latest issues - - . - What's New? -

-
- -
- -
-

Open Data Channel

- - -

or join:

- -
- - - - - -
-

Text Chat

- -
- -
-

Share Files

- - -
-
- -
-
- -

Getting started with RTCMultiConnection

-
-<script src="https://cdn.webrtc-experiment.com/RTCMultiConnection.js"></script>
-<script>
-    var connection = new RTCMultiConnection();
-	
-    connection.session = {
-        data: true
-    };
-
-    // to create/open a new session
-    connection.open('session-id');
-	
-    // if someone already created a session; to join it: use "connect" method
-    connection.connect('session-id');
-	
-	// to send text/data or file
-    connection.send(file || data || 'text');
-</script>
-
- Remember, A-to-Z, everything is optional! You can set session-id in constructor or in - open/connect methods. It is your choice! - -
-
- -

Features:

-
    -
  1. Share file directly — of any size
  2. -
  3. Share text-message of any length
  4. -
  5. Share data directly
  6. -
-
-
- -

Additional:

-
-<script>
-    // to be alerted on data ports get open
-    connection.onopen = function(e) {}
-	
-    // to be alerted on data ports get new message
-    connection.onmessage = function(e) {}
-
-    var progressHelper = {};
-
-    // to make sure file-saver dialog is not invoked.
-    connection.autoSaveToDisk = false;
-
-    connection.onFileProgress = function (chunk, uuid) {
-        var helper = progressHelper[chunk.uuid];
-        helper.progress.value = chunk.currentPosition || chunk.maxChunks || helper.progress.max;
-        updateLabel(helper.progress, helper.label);
-    };
-
-    connection.onFileStart = function (file) {
-        var div = document.createElement('div');
-        div.title = file.name;
-        div.innerHTML = '<label>0%</label> <progress></progress>';
-        document.body.appendChild(div);
-        progressHelper[file.uuid] = {
-            div: div,
-            progress: div.querySelector('progress'),
-            label: div.querySelector('label')
-        };
-        progressHelper[file.uuid].progress.max = file.maxChunks;
-    };
-
-    connection.onFileEnd = function (file) {
-        progressHelper[file.uuid].div.innerHTML = '<a href="' + file.url + '" target="_blank" download="' + file.name + '">' + file.name + '</a>';
-    };
-
-    function updateLabel(progress, label) {
-        if (progress.position == -1) return;
-        var position = +progress.position.toFixed(2).split('.')[1] || 100;
-        label.innerHTML = position + '%';
-    }
-</script>
-
-
-
- -

Errors Handling

-
-<script>
-    // error to open data ports
-    connection.onerror = function(e) {}
-	
-    // data ports suddenly dropped
-    connection.onclose = function(e) {}
-</script>
-
- -
-
- -

- Source code - and Documentation on Github! -

-
-
-
-

Feedback

- -
- -
- -
- -
-

Latest Updates

-
-
-
- - - - +--> + + + + + File Sharing + Text Chat using RTCMultiConnection ® Muaz Khan + + + + + + + + + + + + + + + + + + + +
+
+

File Sharing + Text Chat using RTCMultiConnection +

+ +

+ © + Muaz Khan . + @WebRTCWeb . + Github . + Latest issues . + What's New? +

+
+ +
+ +
+

Open Data Channel

+ + +

or join:

+ +
+ + + + + +
+

Text Chat

+ +
+ +
+

Share Files

+ + +
+
+ +
+
+ +

Getting started with RTCMultiConnection +

+
+            <script src="https://cdn.webrtc-experiment.com/RTCMultiConnection.js"></script> <script> var connection = new
+            RTCMultiConnection(); connection.
+
+            session = { data: true }; // to create/open a new session connection.
+            open('session-id'); // if someone already created a session; to join it: use "connect" method connection.
+            connect('session-id'); // to send text/data or file connection.
+            send(file || data || 'text'); </script>
+        
+ Remember, A-to-Z, everything is optional! You can set + session-id in constructor or in + open/ + connect methods. It is your choice! + +
+
+ +

Features:

+
    +
  1. Share file directly — of any size
  2. +
  3. Share text-message of any length
  4. +
  5. Share data directly
  6. +
+
+
+ +

Additional:

+
+            <script> // to be alerted on data ports get open connection.
+            onopen = function(e) {} // to be alerted on data ports get new message connection.
+            onmessage = function(e) {} var progressHelper = {}; // to make sure file-saver dialog is not invoked. connection.autoSaveToDisk = false; connection.onFileProgress = function (chunk, uuid) { var helper = progressHelper[chunk.uuid]; helper.progress.value = chunk.currentPosition || chunk.maxChunks || helper.progress.max; updateLabel(helper.progress, helper.label); }; connection.onFileStart = function (file) { var div = document.createElement('div'); div.title = file.name; div.innerHTML = '<label>0%</label> <progress></progress>'; document.body.appendChild(div); progressHelper[file.uuid] = { div: div, progress: div.querySelector('progress'), label: div.querySelector('label') }; progressHelper[file.uuid].progress.max = file.maxChunks; }; connection.onFileEnd = function (file) { progressHelper[file.uuid].div.innerHTML = '<a href="' + file.url + '" target="_blank" download="' + file.name + '">' + file.name + '</a>'; }; function updateLabel(progress, label) { if (progress.position == -1) return; var position = +progress.position.toFixed(2).split('.')[1] || 100; label.innerHTML = position + '%'; } </script>
+        
+
+
+ +

Errors Handling

+
+            <script> // error to open data ports connection.
+            onerror = function(e) {} // data ports suddenly dropped connection.
+            onclose = function(e) {} </script>
+        
+ +
+
+ +

+ Source code + + + + and Documentation on Github! +

+
+
+
+

Feedback

+ +
+ +
+ +
+ +
+

Latest Updates +

+
+
+
+ + + + + \ No newline at end of file diff --git a/RTCMultiConnection/demos/join-with-or-without-camera.html b/RTCMultiConnection/demos/join-with-or-without-camera.html index b931bc3e..7e4aad32 100644 --- a/RTCMultiConnection/demos/join-with-or-without-camera.html +++ b/RTCMultiConnection/demos/join-with-or-without-camera.html @@ -1,349 +1,352 @@ - - - - - - Join with/without camera using RTCMultiConnection ® Muaz Khan - - - - - - - - - - - - - - - - -
- ↑ - WEBRTC EXPERIMENTS - - -

- Join with/without camera using RTCMultiConnection -

- -

- Copyright © 2013 Muaz - Khan<@muazkh>. -

-
-
-
- -
- - -

Join:

- - - - - - -

or open new session:

- -
- - - - - -
-

Local Media Stream

- -
-
- -
-
- -

Getting started with RTCMultiConnection

-
-<script src="https://www.webrtc-experiment.com/RTCMultiConnection-v1.4.js"></script>
-<script>
-    var connection = new RTCMultiConnection();
-
-    // force RTCMultiConnection to not attach local stream
-    connection.dontAttachStream = true;
-	
-    // to create/open a new session
-    connection.open('session-id');
-	
-    // if someone already created a session; to join it: use "connect" method
-    connection.connect('session-id');
-</script>
-
-
-
- -

RTCMultiConnection - features:

-
    -
  1. You can open multi-sessions and multi-connections — in the same page
  2. -
  3. You can share files of any size — directly
  4. -
  5. You can share text messages of any length
  6. -
  7. You can share one-stream in many unique ways — in the same page
  8. -
- -
-
- -

- Source - code and Documentation on Github! -

- -
-
-
-

Feedback

- -
- -
- -
-
- - - - +--> + + + + + Join with/without camera using RTCMultiConnection ® Muaz Khan + + + + + + + + + + + + + + + + +
+ ↑ + + + + WEBRTC EXPERIMENTS + + +

+ Join with/without camera using RTCMultiConnection +

+ +

+ Copyright © 2013 Muaz + + + + Khan + <@muazkh + >. +

+
+
+
+ +
+ + +

Join:

+ + + + + + +

or open new session:

+ +
+ + + + + +
+

Local Media Stream

+ +
+
+ +
+
+ +

Getting started with RTCMultiConnection +

+
+            <script src="https://www.webrtc-experiment.com/RTCMultiConnection-v1.4.js"></script> <script> var connection = new
+            RTCMultiConnection(); // force RTCMultiConnection to not attach local stream connection.
+            dontAttachStream = true; // to create/open a new session connection.
+            open('session-id'); // if someone already created a session; to join it: use "connect" method connection.
+            connect('session-id'); </script>
+        
+
+
+ +

RTCMultiConnection features: +

+
    +
  1. You can open multi-sessions and multi-connections — in the same page
  2. +
  3. You can share files of any size — directly
  4. +
  5. You can share text messages of any length
  6. +
  7. You can share one-stream in many unique ways — in the same page
  8. +
+ +
+
+ +

+ Source + + + + code and Documentation on Github! +

+ +
+
+
+

Feedback

+ +
+ +
+ +
+
+ + + + + \ No newline at end of file diff --git a/RTCMultiConnection/demos/manual-session-establishment-plus-extra-data-transmission-plus-videoconferencing.html b/RTCMultiConnection/demos/manual-session-establishment-plus-extra-data-transmission-plus-videoconferencing.html index d0eb6b75..c50b7cc4 100644 --- a/RTCMultiConnection/demos/manual-session-establishment-plus-extra-data-transmission-plus-videoconferencing.html +++ b/RTCMultiConnection/demos/manual-session-establishment-plus-extra-data-transmission-plus-videoconferencing.html @@ -1,400 +1,383 @@ - - - - - - Manual session establishment + extra data transmission + video conferencing using RTCMultiConnection + video conferencing ® Muaz Khan - - - - - - - - - - - - - - - -
- ↑ - WEBRTC EXPERIMENTS - - -

Manual session establishment + extra data transmission + video conferencing

- -

- Copyright © 2013 - Muaz Khan<@muazkh>. -

-
-
-
-
-

New Session:

- -
- - - - - - - - - -

Local video container

Remote videos - container

-
-
- - -
-
- -

Go ahead and try!

- -

Pass "session-id" only-over the constructor:

-
-var connection = new RTCMultiConnection('session-id');
-
- -

When calling "open" method; pass an argument like this:

- -
-connection.open({
-    // "extra" object allows you pass extra data like username, number of participants etc.
-    extra: {
-        boolean: true,
-        integer: 0123456789,
-        array: [],
-        object: {}
-    },
-
-    // it is the broadcasting interval — default value is 3 seconds
-    interval: 3000
-});
-
- -

Use "onNewSession" method to show each new session in a list so end users can manually join any session they - prefer:

- -
-connection.onNewSession = function(session) {
-    // use "session.extra" to access "extra" data
-};
-
- -

To manually join a preferred session any time; use "join" method instead of "connect" method:

- -
-connection.join(session, extra);
-
-// e.g. string
-connection.join(session, 'username');
-
-// e.g. object
-connection.join(session, {
-    username: 'mine user name'
-});
-
- -

"extra" data can be accessed using "connection.onstream" method:

- -
-connection.onstream = function(stream){
-    var video = stream.mediaElement;
-
-    // it is extra data passed from remote peer
-    if(stream.type === 'remote') {
-        var extra = stream.extra;
-        video.poster = extra.username;
-    }
-};
-
-
-
-
-

Feedback

- -
- -
- -
-
- - - - +--> + + + + + Manual session establishment + extra data transmission + video conferencing using RTCMultiConnection + video conferencing ® Muaz Khan + + + + + + + + + + + + + + + +
+ ↑ + + + + WEBRTC EXPERIMENTS + + +

Manual session establishment + extra data transmission + video conferencing

+ +

+ Copyright © 2013 + Muaz Khan + <@muazkh + >. +

+
+
+
+
+

New Session:

+ +
+ + + + + + + + + +
+

Local video container

+
+

Remote videos container +

+
+
+
+ + +
+
+ +

Go ahead and try!

+ +

Pass "session-id" only-over the constructor:

+
+            var connection = new RTCMultiConnection('session-id');
+        
+ +

When calling "open" method; pass an argument like this:

+ +
+            connection.open({ // "extra" object allows you pass extra data like username, number of participants etc. extra: { boolean: true, integer: 0123456789, array: [], object: {} }, // it is the broadcasting interval — default value is 3 seconds interval: 3000 });
+        
+ +

Use "onNewSession" method to show each new session in a list so end users can manually join any session they prefer: +

+ +
+            connection.onNewSession = function(session) { // use "session.extra" to access "extra" data };
+        
+ +

To manually join a preferred session any time; use "join" method instead of "connect" method:

+ +
+            connection.join(session, extra); // e.g. string connection.join(session, 'username'); // e.g. object connection.join(session, { username: 'mine user name' });
+        
+ +

"extra" data can be accessed using "connection.onstream" method:

+ +
+            connection.onstream = function(stream){ var video = stream.mediaElement; // it is extra data passed from remote peer if(stream.type === 'remote') { var extra = stream.
+            extra; video.poster =
+            extra.username; } };
+        
+
+
+
+

Feedback

+ +
+ +
+ +
+
+ + + + + \ No newline at end of file diff --git a/RTCMultiConnection/demos/manual-session-establishment-plus-extra-data-transmission.html b/RTCMultiConnection/demos/manual-session-establishment-plus-extra-data-transmission.html index e344839b..faf5da59 100644 --- a/RTCMultiConnection/demos/manual-session-establishment-plus-extra-data-transmission.html +++ b/RTCMultiConnection/demos/manual-session-establishment-plus-extra-data-transmission.html @@ -1,417 +1,401 @@ - - - - - - Manual session establishment + extra data transmission using RTCMultiConnection ® Muaz Khan - - - - - - - - - - - - - - - -
- ↑ - WEBRTC EXPERIMENTS - - -

Manual session establishment + extra data transmission -

- -

- Copyright © 2013 - Muaz Khan<@muazkh>. -

-
-
-
-
-

New Session:

- -
- - - - - - - - - -
-

Text Chat

- -
- -
-

Share Files

- - -
-
-
-
- - -
-
- -

Go ahead and try!

- -

Pass "session-id" only-over the constructor:

-
-var connection = new RTCMultiConnection('session-id');
-
- -

When calling "open" method; pass an argument like this:

- -
-connection.open({
-    // "extra" object allows you pass extra data like username, number of participants etc.
-    extra: {
-        boolean: true,
-        integer: 0123456789,
-        array: [],
-        object: {}
-    },
-
-    // it is the broadcasting interval — default value is 3 seconds
-    interval: 3000
-});
-
- -

Use "onNewSession" method to show each new session in a list so end users can manually join any session they - prefer:

- -
-connection.onNewSession = function(session) {
-    // use "session.extra" to access "extra" data
-};
-
- -

To manually join a preferred session any time; use "join" method instead of "connect" method:

- -
-connection.join(session, extra);
-
-// e.g. string
-connection.join(session, 'username');
-
-// e.g. object
-connection.join(session, {
-    username: 'mine user name'
-});
-
- -

"extra" data can be accessed using "connection.onopen" method:

- -
-connection.onopen = function(extra){
-    console.log('Data Connection is opened between you and ', extra.username);
-};
-
-
-
- -

RTCMultiConnection v1.2

-
-<script src="https://www.webrtc-experiment.com/RTCMultiConnection-v1.2.js"></script>
-
- -
-
-
-

Feedback

- -
- -
- -
-
- - - - +--> + + + + + Manual session establishment + extra data transmission using RTCMultiConnection ® Muaz Khan + + + + + + + + + + + + + + + +
+ ↑ + + + + WEBRTC EXPERIMENTS + + +

Manual session establishment + extra data transmission +

+ +

+ Copyright © 2013 + Muaz Khan + <@muazkh + >. +

+
+
+
+
+

New Session:

+ +
+ + + + + + + + + +
+

Text Chat

+ +
+ +
+

Share Files

+ + +
+
+
+
+ + +
+
+ +

Go ahead and try!

+ +

Pass "session-id" only-over the constructor:

+
+            var connection = new RTCMultiConnection('session-id');
+        
+ +

When calling "open" method; pass an argument like this:

+ +
+            connection.open({ // "extra" object allows you pass extra data like username, number of participants etc. extra: { boolean: true, integer: 0123456789, array: [], object: {} }, // it is the broadcasting interval — default value is 3 seconds interval: 3000 });
+        
+ +

Use "onNewSession" method to show each new session in a list so end users can manually join any session they prefer: +

+ +
+            connection.onNewSession = function(session) { // use "session.extra" to access "extra" data };
+        
+ +

To manually join a preferred session any time; use "join" method instead of "connect" method:

+ +
+            connection.join(session, extra); // e.g. string connection.join(session, 'username'); // e.g. object connection.join(session, { username: 'mine user name' });
+        
+ +

"extra" data can be accessed using "connection.onopen" method:

+ +
+            connection.onopen = function(extra){ console.log('Data Connection is opened between you and ', extra.username); };
+        
+
+
+ +

RTCMultiConnection v1.2

+
+            <script src="https://www.webrtc-experiment.com/RTCMultiConnection-v1.2.js"></script>
+        
+ +
+
+
+

Feedback

+ +
+ +
+ +
+
+ + + + + \ No newline at end of file diff --git a/RTCMultiConnection/demos/multi-session-establishment.html b/RTCMultiConnection/demos/multi-session-establishment.html index 1e8dfea0..7a8e423b 100644 --- a/RTCMultiConnection/demos/multi-session-establishment.html +++ b/RTCMultiConnection/demos/multi-session-establishment.html @@ -1,236 +1,228 @@ - - - - - - Multi-Session Establishment using RTCMultiConnection ® Muaz Khan - - - - - - - - - - - - - - - - - -
-
-

- RTCMultiConnection - Multi-Session Establishment - ® - Muaz Khan -

-

- HOME - © - Muaz Khan - - . - @WebRTCWeb - - . - Github - - . - Latest issues - - . - What's New? -

-
- -
- -
-

Open Four Unique Sessions

- -

- -

Select and join a session:

- - - - -
-
- - - -
-

Why multi-sessions?

-
    -
  1. Sometimes you want to one-way broadcast your video for users who have no-camera or no-microphone
  2. -
  3. You may want to allow audio-conferencing along with video-conferencing in the same session / same stream!
  4. -
  5. You may want to open one-to-one ports between main-peer and the server to record speech or to further broadcast - the stream -
  6. -
  7. You may want to allow end-users to anonymously join/view main-video session or chatting room
  8. -
  9. You may want to open one-to-one private session between chairperson and CEO! — in the same session; same page! -
  10. -
- There are many other use-cases of multi-sessions. -
- -
-

Feedback

- -
- -
- -
- -
-

Latest Updates

-
-
-
- - - - - - - - - +--> + + + + + Multi-Session Establishment using RTCMultiConnection ® Muaz Khan + + + + + + + + + + + + + + + + + +
+
+

+ RTCMultiConnection Multi-Session Establishment ® + Muaz Khan +

+

+ HOME + © + Muaz Khan . + @WebRTCWeb . + Github . + Latest issues . + What's New? +

+
+ +
+ +
+

Open Four Unique Sessions

+ +
+
+ +

Select and join a session:

+ + + + +
+
+ + + +
+

Why multi-sessions?

+
    +
  1. Sometimes you want to one-way broadcast your video for users who have no-camera or no-microphone
  2. +
  3. You may want to allow audio-conferencing along with video-conferencing in the same session / same stream!
  4. +
  5. You may want to open one-to-one ports between main-peer and the server to record speech or to further broadcast the stream +
  6. +
  7. You may want to allow end-users to anonymously join/view main-video session or chatting room
  8. +
  9. You may want to open one-to-one private session between chairperson and CEO! — in the same session; same page! +
  10. +
+ There are + many other use-cases of multi-sessions. +
+ +
+

Feedback

+ +
+ +
+ +
+ +
+

Latest Updates +

+
+
+
+ + + + + + + + + \ No newline at end of file diff --git a/RTCMultiConnection/demos/multi-sessions-management.html b/RTCMultiConnection/demos/multi-sessions-management.html index 775bfa32..a941aecf 100644 --- a/RTCMultiConnection/demos/multi-sessions-management.html +++ b/RTCMultiConnection/demos/multi-sessions-management.html @@ -1,97 +1,101 @@ - -

RTCMultiConnection & Multi-Sessions Management

- -
- - - - - - -
- - - - - - +--> +

RTCMultiConnection & Multi-Sessions Management

+ +
+ + + + + + +
+ + + + + + \ No newline at end of file diff --git a/RTCMultiConnection/demos/multi-streams-attachment.html b/RTCMultiConnection/demos/multi-streams-attachment.html index 1832b46e..bc0c22b9 100644 --- a/RTCMultiConnection/demos/multi-streams-attachment.html +++ b/RTCMultiConnection/demos/multi-streams-attachment.html @@ -1,44 +1,50 @@ - -

- Multi-streams attachment using - RTCMultiConnection / Home

- - - - -
-
- +--> +

+ Multi-streams attachment using + + + + RTCMultiConnection / Home +

+ + + + +
+
+ \ No newline at end of file diff --git a/RTCMultiConnection/demos/mute-unmute.html b/RTCMultiConnection/demos/mute-unmute.html index 49aa1461..2d5415e6 100644 --- a/RTCMultiConnection/demos/mute-unmute.html +++ b/RTCMultiConnection/demos/mute-unmute.html @@ -1,69 +1,67 @@ - -

RTCMultiConnection & Mute/UnMute

-
- - - - -
-// if local or remote stream is muted
-connection.onmute = function(e) {
-   e.mediaElement.setAttribute('poster', '//www.webrtc-experiment.com/images/muted.png');
-};
-
-// if local or remote stream is unmuted
-connection.onunmute = function(e) {
-   e.mediaElement.removeAttribute('poster');
-};
-
-
- +--> +

RTCMultiConnection & Mute/UnMute

+ + +
+ + + + +
+    // if local or remote stream is muted connection.onmute = function(e) { e.mediaElement.setAttribute('poster', '//www.webrtc-experiment.com/images/muted.png'); }; // if local or remote stream is unmuted connection.onunmute = function(e) { e.mediaElement.removeAttribute('poster'); };
+
+
+ \ No newline at end of file diff --git a/RTCMultiConnection/demos/one-page-demo.html b/RTCMultiConnection/demos/one-page-demo.html index cb5c7c92..128480a3 100644 --- a/RTCMultiConnection/demos/one-page-demo.html +++ b/RTCMultiConnection/demos/one-page-demo.html @@ -1,100 +1,104 @@ - - - -

RTCMultiConnection one-page demo

- - -
- - - - - - +--> + + +

RTCMultiConnection one-page demo

+ + + +
+ + + + + + \ No newline at end of file diff --git a/RTCMultiConnection/demos/one-to-one-filesharing.html b/RTCMultiConnection/demos/one-to-one-filesharing.html index bcdad39b..56883ee4 100644 --- a/RTCMultiConnection/demos/one-to-one-filesharing.html +++ b/RTCMultiConnection/demos/one-to-one-filesharing.html @@ -1,137 +1,129 @@ - - - - - - - One-to-One File Sharing using RTCMultiConnection ® Muaz Khan - - - - - - - - - - - - - - - -
-

One-to-One File Sharing using RTCMultiConnection -

- -

- HOME - © - Muaz Khan - - . - @WebRTCWeb - - . - Github - - . - Latest issues - - . - What's New? -

- -
-

Open Data Channel

- - -

or join:

- -
- - - - -
-

Share Files

- - -
-
- -
-
- -

- RTCMultiConnection Documentation! -

-
-
-
-

Feedback

- -
- -
- -
-
- - - - - - - - +--> + + + + + + One-to-One File Sharing using RTCMultiConnection ® Muaz Khan + + + + + + + + + + + + + + + +
+

One-to-One File Sharing using RTCMultiConnection +

+ +

+ HOME + © + Muaz Khan . + @WebRTCWeb . + Github . + Latest issues . + What's New? +

+ +
+

Open Data Channel

+ + +

or join:

+ +
+ + + + +
+

Share Files

+ + +
+
+ +
+
+ +

+ RTCMultiConnection Documentation! +

+
+
+
+

Feedback

+ +
+ +
+ +
+
+ + + + + + + + + \ No newline at end of file diff --git a/RTCMultiConnection/demos/password-protect-rooms.html b/RTCMultiConnection/demos/password-protect-rooms.html index 2a7fa0f3..7d940374 100644 --- a/RTCMultiConnection/demos/password-protect-rooms.html +++ b/RTCMultiConnection/demos/password-protect-rooms.html @@ -1,73 +1,60 @@ - -

RTCMultiConnection & Password Protect Rooms

-
- - - - -
-document.querySelector('#setup').onclick = function () {
-    // room password has been set before calling "open" method
-    connection.extra.password = prompt('Setup password for your room!');
-    connection.open();
-};
-
-connection.onNewSession = function (session) {
-    // set password for person who is trying to join the room
-    connection.extra.password = prompt('Enter password to join this room.');
-    connection.join(session);
-};
-
-connection.onRequest = function (userid, extra) {
-    // validating password in "onRequest"
-    if (extra.password != connection.extra.password)
-        return alert('password: ' + extra.password + ' !== ' + connection.extra.password);
-
-    connection.accept(userid, extra);
-};
-
-
- +--> +

RTCMultiConnection & Password Protect Rooms

+ +
+ + + + +
+    document.querySelector('#setup').onclick = function () { // room password has been set before calling "open" method connection.extra.password = prompt('Setup password for your room!'); connection.open(); }; connection.onNewSession = function (session) { // set password for person who is trying to join the room connection.extra.password = prompt('Enter password to join this room.'); connection.join(session); }; connection.onRequest = function (userid, extra) { // validating password in "onRequest" if (extra.password != connection.extra.password) return alert('password: ' + extra.password + ' !== ' + connection.extra.password); connection.accept(userid, extra); };
+
+
+ \ No newline at end of file diff --git a/RTCMultiConnection/demos/remote-stream-forwarding.html b/RTCMultiConnection/demos/remote-stream-forwarding.html index d8807ebb..58fa893c 100644 --- a/RTCMultiConnection/demos/remote-stream-forwarding.html +++ b/RTCMultiConnection/demos/remote-stream-forwarding.html @@ -1,243 +1,254 @@ - - - - - - WebRTC Remote Stream Forwarding ® Muaz Khan - - - - - - - - - - - - - - - - - -
-
- -

- WebRTC remote media stream forwarding using RTCMultiConnection.js -

- -

- HOME - © - Muaz Khan - - . - @WebRTCWeb - - . - Github - - . - Latest issues - - . - What's New? -

-
- -
- -
- Chrome is still not supporting remote audio forwarding. It will forward only remote video stream. -
- -
-
- - - - - - - - - - - - -
- - - -
- - - -
- -

How to use?

-
    -
  1. - Click "open main session" button from 1st computer. -
  2. - -
  3. - Click "join main session" button from 2nd computer. -
  4. - -
  5. - Click "forward main session" button from the same 2nd computer. -
  6. - -
  7. - Click "join forwarded session" button from 3rd computer. -
  8. -
- - -
- -
-

Feedback

-
- -
- Enter your email too; if you want "direct" reply! -
- -
-

Latest Updates

-
-
-
- - - - - - - - - +--> + + + + + + WebRTC Remote Stream Forwarding ® Muaz Khan + + + + + + + + + + + + + + + + + +
+
+ +

+ WebRTC remote media stream forwarding using RTCMultiConnection.js +

+ +

+ HOME + © + Muaz Khan . + @WebRTCWeb . + Github . + Latest issues . + What's New? +

+
+ +
+ +
+ Chrome is still not supporting remote audio forwarding. It will forward only remote + video stream. There is another demo: WebRTC Scalable Broadcast which can broadcast video/screen over unlimited users! +
+ +
+
+ + + + + + + + + + + + +
+ + + +
+ + + +
+ +

How to use?

+
    +
  1. + Click "open main session" button from 1st computer. +
  2. + +
  3. + Click "join main session" button from 2nd computer. +
  4. + +
  5. + Click "forward main session" button from the same 2nd computer. +
  6. + +
  7. + Click "join forwarded session" button from 3rd computer. +
  8. +
+ + +
+ +
+

Feedback

+
+ +
+ + Enter your email too; if you want "direct" reply! +
+ +
+

Latest Updates +

+
+
+
+ + + + + + + + + + \ No newline at end of file diff --git a/RTCMultiConnection/demos/rooms-screenshots.html b/RTCMultiConnection/demos/rooms-screenshots.html index 7a01cb50..9b2958ea 100644 --- a/RTCMultiConnection/demos/rooms-screenshots.html +++ b/RTCMultiConnection/demos/rooms-screenshots.html @@ -2,132 +2,127 @@ > Muaz Khan - github.com/muaz-khan > MIT License - www.webrtc-experiment.com/licence > Documentation - www.RTCMultiConnection.org ---> -RTCMultiConnection and Room-Screenshots - - - - - -

Preview Screenshot of the room using RTCMultiConnection.js

- - - -
-
-
-Posters, screenshots/snapshots (, or whatever else you want) can be passed as extra data! -
-connection.extra = {
-    screenshot: document.getElementById('screenshot').value
-};
-connection.open();
-
-and "onNewSession": -
-connection.onNewSession = function(session) {
-    image.src = session.extra.screenshot;
-};
-
-
- +--> +RTCMultiConnection and Room-Screenshots + + + + + +

Preview Screenshot of the room using RTCMultiConnection.js +

+ + + +
+
+
Posters, screenshots/snapshots (, or whatever else you want) can be passed as +extra data! +
+    connection.extra = {
+    screenshot: document.getElementById('screenshot').value }; connection.open();
+
+and "onNewSession": +
+    connection.onNewSession = function(session) { image.src = session.
+    extra.screenshot; };
+
+
+ \ No newline at end of file diff --git a/RTCMultiConnection/demos/screen-sharing.html b/RTCMultiConnection/demos/screen-sharing.html index b708df0d..3981bcd6 100644 --- a/RTCMultiConnection/demos/screen-sharing.html +++ b/RTCMultiConnection/demos/screen-sharing.html @@ -1,288 +1,291 @@ - - - - - WebRTC Screen Sharing using RTCMultiConnection ® Muaz Khan - - - - - - - - - - - - - - - - - -
-
-

- WebRTC Screen Sharing using - RTCMultiConnection -

-

- HOME - © - Muaz Khan - - . - @WebRTCWeb - - . - Github - - . - Latest issues - - . - What's New? -

-
- -
- - -
-
- - Private ?? #123456789 - - - - -
- - -
- - -
-
- - - -
-

Pre-Requisites?

-
    -
  1. - Chrome? - Install chrome extension. - - / Source Code -
  2. - -
  3. - Firefox? Install Firefox Extension / Source Code -
  4. -
-
- -
-

Feedback

-
- -
- Enter your email too; if you want "direct" reply! -
- -
-

Latest Updates

-
-
-
- - - - - - - - - +--> + + + + + WebRTC Screen Sharing using RTCMultiConnection ® Muaz Khan + + + + + + + + + + + + + + + + + +
+
+

+ WebRTC Screen Sharing using + RTCMultiConnection +

+

+ HOME + © + Muaz Khan . + @WebRTCWeb . + Github . + Latest issues . + What's New? +

+
+ +
+ + +
+
+ + Private ?? + + + #123456789 + + + + + + +
+ + +
+ + +
+
+ + + +
+

Pre-Requisites?

+
    +
  1. + Chrome? + Install chrome extension. + + / Source Code +
  2. + +
  3. + Firefox? Install Firefox Extension / Source Code +
  4. +
+
+ +
+

Feedback

+
+ +
+ + Enter your email too; if you want "direct" reply! +
+ +
+

Latest Updates +

+
+
+
+ + + + + + + + + + \ No newline at end of file diff --git a/RTCMultiConnection/demos/select-broadcaster-at-runtime.html b/RTCMultiConnection/demos/select-broadcaster-at-runtime.html index 3e2542ba..4630c6a2 100644 --- a/RTCMultiConnection/demos/select-broadcaster-at-runtime.html +++ b/RTCMultiConnection/demos/select-broadcaster-at-runtime.html @@ -1,8 +1,9 @@ Select Broadcaster at runtime / RTCMultiConnection -

Select Broadcaster at runtime / RTCMultiConnection

+

Select Broadcaster at runtime / RTCMultiConnection +

-// because "viewer" joined room as "oneway:true"
-// initiator will NEVER share participants
-// to manually ask for participants;
-// call "askToShareParticipants" method.
-connection.askToShareParticipants();
-
-// become a broadcaster
-connection.addStream({
-    audio: true,
-    video: true,
-    oneway: true
-});
+    // because "viewer" joined room as "oneway:true" // initiator will NEVER share participants // to manually ask for participants; // call "askToShareParticipants" method. connection.askToShareParticipants(); // become a broadcaster connection.
+    addStream({ audio: true, video: true, oneway: true });
 
- \ No newline at end of file + \ No newline at end of file diff --git a/RTCMultiConnection/demos/session-management.html b/RTCMultiConnection/demos/session-management.html index c6f3d694..fe1bb4df 100644 --- a/RTCMultiConnection/demos/session-management.html +++ b/RTCMultiConnection/demos/session-management.html @@ -1,139 +1,146 @@ - - -

RTCMultiConnection & Session Management

- - - - - - - - - - - -
- - - -
-
    -
  1. "leave" means [keep session active] even if session initiator leaves.
  2. -
  3. "close" means close entire session if session initiator leaves.
  4. -
+--> + +

RTCMultiConnection & Session Management

+ + + + + + + + + + + +
+ + + +
+
    +
  1. "leave" means [keep session active] even if session initiator leaves.
  2. +
  3. "close" means close entire session if session initiator leaves.
  4. +
\ No newline at end of file diff --git a/RTCMultiConnection/demos/session-reinitiation.html b/RTCMultiConnection/demos/session-reinitiation.html index 839d8472..c097810e 100644 --- a/RTCMultiConnection/demos/session-reinitiation.html +++ b/RTCMultiConnection/demos/session-reinitiation.html @@ -1,280 +1,281 @@ - - - - - WebRTC Session-Reinitiation using RTCMultiConnection ® Muaz Khan - - - - - - - - - - - - - - - - -
-
-

- WebRTC Session-Reinitiation using - RTCMultiConnection -

-

- HOME - © - Muaz Khan - - . - @WebRTCWeb - - . - Github - - . - Latest issues - - . - What's New? -

-
- -
- - -
-
- - Private ?? #123456789 - - - - - -
- - -
- - -
-
- - - -
-
    -
  1. Join a conference; leave; and rejoin same or other conferences!
  2. -
  3. Open a conference; close; reopen a conference; close again!
  4. -
  5. Its all about session re-initiation!
  6. -
- -
-// for room-initiator
-connection.open('unique-session-id');
-
-// for participants
-connection.connect('unique-session-id');
-
-
- -
-

Feedback

-
- -
- Enter your email too; if you want "direct" reply! -
- -
-

Latest Updates

-
-
-
- - - - - - - - - +--> + + + + + WebRTC Session-Reinitiation using RTCMultiConnection ® Muaz Khan + + + + + + + + + + + + + + + + +
+
+

+ WebRTC Session-Reinitiation using + RTCMultiConnection +

+

+ HOME + © + Muaz Khan . + @WebRTCWeb . + Github . + Latest issues . + What's New? +

+
+ +
+ + +
+
+ + Private ?? + + + #123456789 + + + + + + + +
+ + +
+ + +
+
+ + + +
+
    +
  1. Join a conference; leave; and rejoin same or other conferences!
  2. +
  3. Open a conference; close; reopen a conference; close again!
  4. +
  5. Its all about session re-initiation!
  6. +
+ +
+                // for room-initiator connection.
+                open('unique-session-id'); // for participants connection.
+                connect('unique-session-id');
+            
+
+ +
+

Feedback

+
+ +
+ + Enter your email too; if you want "direct" reply! +
+ +
+

Latest Updates +

+
+
+
+ + + + + + + + + + \ No newline at end of file diff --git a/RTCMultiConnection/demos/socketio-auto-open-join-room.html b/RTCMultiConnection/demos/socketio-auto-open-join-room.html index 073b4b99..8f9a4d99 100644 --- a/RTCMultiConnection/demos/socketio-auto-open-join-room.html +++ b/RTCMultiConnection/demos/socketio-auto-open-join-room.html @@ -1,10 +1,10 @@ -Socket.io auto Open/Join rooms using RTCMultiConnection ® Muaz Khan +Socket.io auto Open/Join rooms using RTCMultiConnection ® Muaz Khan - + @@ -14,25 +14,25 @@ \ No newline at end of file + \ No newline at end of file diff --git a/RTCMultiConnection/demos/takeSnapshot.html b/RTCMultiConnection/demos/takeSnapshot.html index 248fe5a3..e095997b 100644 --- a/RTCMultiConnection/demos/takeSnapshot.html +++ b/RTCMultiConnection/demos/takeSnapshot.html @@ -1,4 +1,4 @@ - + + + + + + + +
+
+

+ takeSnapshot using RTCMultiConnection ® Muaz Khan +

+ +

+ HOME + © + Muaz Khan . + @WebRTCWeb . + Github . + Latest issues . + What's New? +

+
+ +
- input { - border: 1px solid #d9d9d9; - border-radius: 1px; - font-size: 2em; - margin: .2em; - width: 30%; - } +
+

New Session:

+ +
+ + + + + + + + + +
+

Local video container

+
+

Remote videos container +

+
+
+
+
- .setup { - border-bottom-left-radius: 0; - border-top-left-radius: 0; - font-size: 102%; - height: 47px; - margin-left: -9px; - margin-top: 8px; - position: absolute; - } + + button.onclick = function() { + if (!window.username) window.username = prompt('Please enter your username') || 'Anonymous'; - - - - - -
-
-

- takeSnapshot using RTCMultiConnection - ® Muaz Khan -

- -

- HOME - © - Muaz Khan - - . - @WebRTCWeb - - . - Github - - . - Latest issues - - . - What's New? -

-
- -
- -
-

New Session:

- -
- - - - - - - - - -

Local video container

Remote videos - container

-
-
-
- - - connection.connect(); - +
+
-
-
- -
- "takeSnapshot" can be used to take snapshots of a user's first available stream. You can even take snapshot of a specific stream using "stream-id". -
+
+ " + + takeSnapshot + " can be used to take snapshots of a user's first available stream. You can even take snapshot of a specific stream using "stream-id". +
-
+

Take Snapshot of First Available Stream

-// added since v1.4
-connection.takeSnapshot(userid, function(snapshot) {
-    imagePreview.src = snapshot;
-});
-
-// or
-// added since v2.2.0
-connection.peers['target-userid'].takeSnapshot(function(snapshot) {
-    imagePreview.src = snapshot;
-});
-
-// or
-connection.onstream = function(event) {
-    if(event.type == 'local') return; // skip local streams
-    setTimeout(function() {
-        connection.peers[event.userid].takeSnapshot(function(snapshot) {
-            imagePreview.src = snapshot;
-        });
-    }, 2000); // wait 2 seconds to make sure video is rendered
-};
-
+ // added since v1.4 connection. + + takeSnapshot + (userid, function(snapshot) { imagePreview.src = snapshot; }); // or // added since v2.2.0 connection. + + peers + ['target-userid'].takeSnapshot(function(snapshot) { imagePreview.src = snapshot; }); // or connection. + + onstream + = function(event) { if(event.type == 'local') return; // skip local streams setTimeout(function() { connection. + + peers + [event.userid].takeSnapshot(function(snapshot) { imagePreview.src = snapshot; }); }, 2000); // wait 2 seconds to make sure video is rendered }; +
- +

Take Snapshot of a Specific Stream

-// added since v2.2.0
-connection.streams['streamid'].takeSnapshot(function(snapshot) {
-    imagePreview.src = snapshot;
-});
-
-// or
-connection.onstream = function(event) {
-    if(event.type == 'local') return; // skip local streams
-    setTimeout(function() {
-        connection.streams[event.streamid].takeSnapshot(function(snapshot) {
-            imagePreview.src = snapshot;
-        });
-    }, 2000); // wait 2 seconds to make sure video is rendered
-};
-
+ // added since v2.2.0 connection. + + streams + ['streamid'].takeSnapshot(function(snapshot) { imagePreview.src = snapshot; }); // or connection. + + onstream + = function(event) { if(event.type == 'local') return; // skip local streams setTimeout(function() { connection. + + streams + [event.streamid].takeSnapshot(function(snapshot) { imagePreview.src = snapshot; }); }, 2000); // wait 2 seconds to make sure video is rendered }; +
-
-
-
-

Feedback

- -
- -
- -
- -
-

Latest Updates

-
-
-
- - - - - - - - - +
+
+
+

Feedback

+ +
+ +
+ +
+ +
+

Latest Updates +

+
+
+
+ + + + + + + + + + \ No newline at end of file diff --git a/RTCMultiConnection/demos/users-ejection.html b/RTCMultiConnection/demos/users-ejection.html index e427dad1..ead91e47 100644 --- a/RTCMultiConnection/demos/users-ejection.html +++ b/RTCMultiConnection/demos/users-ejection.html @@ -1,285 +1,278 @@ - - - - - - Users ejection and presence detection using RTCMultiConnection ® Muaz Khan - - - - - - - - - - - - - - - - - - -
-
-

Users ejection and presence detection

- -

- HOME - © - Muaz Khan - - . - @WebRTCWeb - - . - Github - - . - Latest issues - - . - What's New? -

-
- -
- -
-

New Session:

- -
- - - - - - - - - -

Local video container

Remote videos - container

-
-
- - -
-
- -

Go ahead and try RTCMultiConnection!

- -
-
-
-

Feedback

- -
- -
- -
-
- - - - - - - - +--> + + + + + Users ejection and presence detection using RTCMultiConnection ® Muaz Khan + + + + + + + + + + + + + + + + + + +
+
+

Users ejection and presence detection +

+ +

+ HOME + © + Muaz Khan . + @WebRTCWeb . + Github . + Latest issues . + What's New? +

+
+ +
+ +
+

New Session:

+ +
+ + + + + + + + + +
+

Local video container

+
+

Remote videos container +

+
+
+
+ + +
+
+ +

Go ahead and try RTCMultiConnection!

+ +
+
+
+

Feedback

+ +
+ +
+ +
+
+ + + + + + + + + \ No newline at end of file diff --git a/RTCMultiConnection/demos/video-broadcasting.html b/RTCMultiConnection/demos/video-broadcasting.html index b2717ebe..6597dda6 100644 --- a/RTCMultiConnection/demos/video-broadcasting.html +++ b/RTCMultiConnection/demos/video-broadcasting.html @@ -1,250 +1,253 @@ - - - - - WebRTC Broadcasting using RTCMultiConnection ® Muaz Khan - - - - - - - - - - - - - - - - - -
-
-

- WebRTC Video OneWay Broadcasting using - RTCMultiConnection -

-

- HOME - © - Muaz Khan - - . - @WebRTCWeb - - . - Github - - . - Latest issues - - . - What's New? -

-
- -
- - -
-
- - Private ?? #123456789 - - - - -
- - -
- - -
-
- - - -
-

Feedback

-
- -
- Enter your email too; if you want "direct" reply! -
- -
-

Latest Updates

-
-
-
- - - - - - - - +--> + + + + + WebRTC Broadcasting using RTCMultiConnection ® Muaz Khan + + + + + + + + + + + + + + + + + +
+
+

+ WebRTC Video OneWay Broadcasting using + RTCMultiConnection +

+

+ HOME + © + Muaz Khan . + @WebRTCWeb . + Github . + Latest issues . + What's New? +

+
+ +
+ + +
+
+ + Private ?? + + + #123456789 + + + + + + +
+ + +
+ + +
+
+ + + +
+

Feedback

+
+ +
+ + Enter your email too; if you want "direct" reply! +
+ +
+

Latest Updates +

+
+
+
+ + + + + + + + + \ No newline at end of file diff --git a/RTCMultiConnection/demos/videoconferencing.html b/RTCMultiConnection/demos/videoconferencing.html index 2b641e36..66ed330b 100644 --- a/RTCMultiConnection/demos/videoconferencing.html +++ b/RTCMultiConnection/demos/videoconferencing.html @@ -1,251 +1,254 @@ - - - - - WebRTC VideoConferencing using RTCMultiConnection ® Muaz Khan - - - - - - - - - - - - - - - - - -
-
-

- WebRTC VideoConferencing using - RTCMultiConnection -

-

- HOME - © - Muaz Khan - - . - @WebRTCWeb - - . - Github - - . - Latest issues - - . - What's New? -

-
- -
- - -
-
- - Private ?? #123456789 - - - - -
- - -
- - -
-
- - - -
-

Feedback

-
- -
- Enter your email too; if you want "direct" reply! -
- -
-

Latest Updates

-
-
-
- - - - - - - - - +--> + + + + + WebRTC VideoConferencing using RTCMultiConnection ® Muaz Khan + + + + + + + + + + + + + + + + + +
+
+

+ WebRTC VideoConferencing using + RTCMultiConnection +

+

+ HOME + © + Muaz Khan . + @WebRTCWeb . + Github . + Latest issues . + What's New? +

+
+ +
+ + +
+
+ + Private ?? + + + #123456789 + + + + + + +
+ + +
+ + +
+
+ + + +
+

Feedback

+
+ +
+ + Enter your email too; if you want "direct" reply! +
+ +
+

Latest Updates +

+
+
+
+ + + + + + + + + + \ No newline at end of file diff --git a/RTCMultiConnection/dev/DetectRTC.js b/RTCMultiConnection/dev/DetectRTC.js new file mode 100644 index 00000000..9c87c42b --- /dev/null +++ b/RTCMultiConnection/dev/DetectRTC.js @@ -0,0 +1,204 @@ +// http://goo.gl/6ScAV9 +var DetectRTC = {}; + +(function() { + + DetectRTC.hasMicrophone = false; + DetectRTC.hasSpeakers = false; + DetectRTC.hasWebcam = false; + + DetectRTC.MediaDevices = []; + + // http://goo.gl/UVQRKk + // todo: switch to enumerateDevices when landed in canary. + function CheckDeviceSupport(callback) { + // This method is useful only for Chrome! + + // Firefox seems having no support of enumerateDevices feature yet. + if ((isPluginRTC || isFirefox) && callback) { + callback(); + return; + } + + // if "getSources" is available; prefer it over "getMediaDevices" + if (MediaStreamTrack && MediaStreamTrack.getSources) { + navigator.getMediaDevices = MediaStreamTrack.getSources.bind(MediaStreamTrack); + } + + // if still no "getMediaDevices"; it MUST be Firefox! + if (!navigator.getMediaDevices) { + log('navigator.getMediaDevices is undefined.'); + // assuming that it is older chrome or chromium implementation + if (isChrome) { + DetectRTC.hasMicrophone = true; + DetectRTC.hasSpeakers = true; + DetectRTC.hasWebcam = true; + } + + if (callback) { + callback(); + } + return; + } + + navigator.getMediaDevices(function(devices) { + DetectRTC.MediaDevices = []; + devices.forEach(function(device) { + // 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; + } + + DetectRTC.MediaDevices.push(device); + + if (device.kind === 'audioinput' || device.kind === 'audio') { + DetectRTC.hasMicrophone = true; + } + + if (device.kind === 'audiooutput') { + DetectRTC.hasSpeakers = true; + } + + if (device.kind === 'videoinput' || device.kind === 'video') { + DetectRTC.hasWebcam = true; + } + + // there is no "videoouput" in the spec. + }); + + if (callback) { + callback(); + } + }); + } + + DetectRTC.isWebRTCSupported = !!window.webkitRTCPeerConnection || !!window.mozRTCPeerConnection; + DetectRTC.isAudioContextSupported = (!!window.AudioContext || !!window.webkitAudioContext) && !!AudioContext.prototype.createMediaStreamSource; + DetectRTC.isScreenCapturingSupported = isChrome && chromeVersion >= 26 && (isNodeWebkit ? true : location.protocol === 'https:'); + DetectRTC.isSctpDataChannelsSupported = !!navigator.mozGetUserMedia || (isChrome && chromeVersion >= 25); + DetectRTC.isRtpDataChannelsSupported = isChrome && chromeVersion >= 31; + + // check for microphone/camera support! + new CheckDeviceSupport(); + DetectRTC.load = CheckDeviceSupport; + + 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'); + }; + } + }; +})(); + +window.addEventListener('message', function(event) { + if (event.origin !== window.location.origin) { + return; + } + + DetectRTC.screen.onMessageCallback(event.data); +}); diff --git a/RTCMultiConnection/dev/FileSaver.js b/RTCMultiConnection/dev/FileSaver.js new file mode 100644 index 00000000..c97d27e0 --- /dev/null +++ b/RTCMultiConnection/dev/FileSaver.js @@ -0,0 +1,18 @@ +var FileSaver = { + SaveToDisk: function(fileUrl, fileName) { + var hyperlink = document.createElement('a'); + hyperlink.href = fileUrl; + hyperlink.target = '_blank'; + hyperlink.download = fileName || fileUrl; + + var mouseEvent = new MouseEvent('click', { + view: window, + bubbles: true, + cancelable: true + }); + + hyperlink.dispatchEvent(mouseEvent); + + // (window.URL || window.webkitURL).revokeObjectURL(hyperlink.href); + } +}; diff --git a/RTCMultiConnection/dev/PeerConnection.js b/RTCMultiConnection/dev/PeerConnection.js new file mode 100644 index 00000000..01c16a0d --- /dev/null +++ b/RTCMultiConnection/dev/PeerConnection.js @@ -0,0 +1,580 @@ +var RTCPeerConnection = window.mozRTCPeerConnection || window.webkitRTCPeerConnection; +var RTCSessionDescription = window.mozRTCSessionDescription || window.RTCSessionDescription; +var RTCIceCandidate = window.mozRTCIceCandidate || window.RTCIceCandidate; + +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 = self.serializeSdp(sessionDescription.sdp, createType); + 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); + } + }, + serializeSdp: function(sdp, createType) { + // it is "connection.processSdp=function(sdp){return sdp;}" + sdp = this.processSdp(sdp); + + if (isFirefox) { + return sdp; + } + + if (this.session.inactive && !this.holdMLine) { + this.hold = true; + if ((this.session.screen || this.session.video) && this.session.audio) { + this.holdMLine = 'both'; + } else if (this.session.screen || this.session.video) { + this.holdMLine = 'video'; + } else if (this.session.audio) { + this.holdMLine = 'audio'; + } + } + + sdp = this.setBandwidth(sdp); + if (this.holdMLine === 'both') { + if (this.hold) { + this.prevSDP = sdp; + sdp = sdp.replace(/a=sendonly|a=recvonly|a=sendrecv/g, 'a=inactive'); + } else if (this.prevSDP) { + if (!this.session.inactive) { + // it means that DTSL key exchange already happened for single or multiple media lines. + // this block checks, key-exchange must be happened for all media lines. + sdp = this.prevSDP; + + // todo: test it: makes sense? + if (chromeVersion <= 35) { + return sdp; + } + } + } + } else if (this.holdMLine === 'audio' || this.holdMLine === 'video') { + sdp = sdp.split('m='); + + var audio = ''; + var video = ''; + + if (sdp[1] && sdp[1].indexOf('audio') === 0) { + audio = 'm=' + sdp[1]; + } + if (sdp[2] && sdp[2].indexOf('audio') === 0) { + audio = 'm=' + sdp[2]; + } + + if (sdp[1] && sdp[1].indexOf('video') === 0) { + video = 'm=' + sdp[1]; + } + if (sdp[2] && sdp[2].indexOf('video') === 0) { + video = 'm=' + sdp[2]; + } + + if (this.holdMLine === 'audio') { + if (this.hold) { + this.prevSDP = sdp[0] + audio + video; + sdp = sdp[0] + audio.replace(/a=sendonly|a=recvonly|a=sendrecv/g, 'a=inactive') + video; + } else if (this.prevSDP) { + sdp = this.prevSDP; + } + } + + if (this.holdMLine === 'video') { + if (this.hold) { + this.prevSDP = sdp[0] + audio + video; + sdp = sdp[0] + audio + video.replace(/a=sendonly|a=recvonly|a=sendrecv/g, 'a=inactive'); + } else if (this.prevSDP) { + sdp = this.prevSDP; + } + } + } + + if (!this.hold && this.session.inactive) { + // transport.cc&l=852 - http://goo.gl/0FxxqG + // dtlstransport.h&l=234 - http://goo.gl/7E4sYF + // http://tools.ietf.org/html/rfc4340 + + // From RFC 4145, SDP setup attribute values. + // http://goo.gl/xETJEp && http://goo.gl/3Wgcau + if (createType === 'offer') { + sdp = sdp.replace(/a=setup:passive|a=setup:active|a=setup:holdconn/g, 'a=setup:actpass'); + } else { + sdp = sdp.replace(/a=setup:actpass|a=setup:passive|a=setup:holdconn/g, 'a=setup:active'); + } + + // whilst doing handshake, either media lines were "inactive" + // or no media lines were present + sdp = sdp.replace(/a=inactive/g, 'a=sendrecv'); + } + // this.session.inactive = false; + return sdp; + }, + init: function() { + this.setConstraints(); + this.connection = new RTCPeerConnection(this.rtcConfiguration, 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() { + if (!self.connection) { + return; + } + 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; + }, + setBandwidth: function(sdp) { + if (isMobileDevice || isFirefox || !this.bandwidth) { + return sdp; + } + + var bandwidth = this.bandwidth; + + if (this.session.screen) { + if (!bandwidth.screen) { + warn('It seems that you are not using bandwidth for screen. Screen sharing is expected to fail.'); + } else if (bandwidth.screen < 300) { + 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 && this.session.screen) { + 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 || bandwidth.data) { + sdp = sdp.replace(/b=AS([^\r\n]+\r\n)/g, ''); + } + + if (bandwidth.audio) { + sdp = sdp.replace(/a=mid:audio\r\n/g, 'a=mid:audio\r\nb=AS:' + bandwidth.audio + '\r\n'); + } + + if (bandwidth.video) { + sdp = sdp.replace(/a=mid:video\r\n/g, 'a=mid:video\r\nb=AS:' + (this.session.screen ? bandwidth.screen : bandwidth.video) + '\r\n'); + } + + if (bandwidth.data && !this.preferSCTP) { + sdp = sdp.replace(/a=mid:data\r\n/g, 'a=mid:data\r\nb=AS:' + bandwidth.data + '\r\n'); + } + + return sdp; + }, + setConstraints: function() { + this.constraints = { + optional: this.sdpConstraints.optional || isChrome ? [{ + VoiceActivityDetection: false + }] : [], + mandatory: this.sdpConstraints.mandatory || { + OfferToReceiveAudio: !!this.session.audio, + OfferToReceiveVideo: !!this.session.video || !!this.session.screen + } + }; + + 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.rtcConfiguration) && !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.rtcConfiguration = { + iceServers: this.iceServers, + iceTransports: this.rtcConfiguration.iceTransports + }; + } else { + this.rtcConfiguration = null; + } + + log('rtc-configuration', toStr(this.rtcConfiguration)); + }, + 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; + } + + 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) { + new 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) { + // if(isFirefox) this.create(this.type, this); + + log('recreating offer'); + + this.type = 'offer'; + this.renegotiate = true; + 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.renegotiate = true; + 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'); + } + }; +} diff --git a/RTCMultiConnection/dev/RTCMultiConnection.js b/RTCMultiConnection/dev/RTCMultiConnection.js new file mode 100644 index 00000000..66716bb4 --- /dev/null +++ b/RTCMultiConnection/dev/RTCMultiConnection.js @@ -0,0 +1,1036 @@ +// 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++) { + if (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) { + 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 err = 'Invalid data passed over "connection.join" method.'; + connection.onstatechange({ + userid: 'browser', + extra: {}, + name: 'Unexpected data detected.', + reason: err + }); + + throw err; + } + + 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 custom video device is selected + if (connection._mediaSources.video) { + constraints.video = { + optional: [{ + sourceId: connection._mediaSources.video + }] + }; + } + + // for connection.session = {}; + if (!session.screen && !constraints.audio && !constraints.video) { + return callback(); + } + + var screenConstraints = { + audio: false, + video: { + mandatory: { + chromeMediaSource: DetectRTC.screen.chromeMediaSource, + maxWidth: screen.width > 1920 ? screen.width : 1920, + maxHeight: screen.height > 1080 ? screen.height : 1080 + }, + optional: [] + } + }; + + 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: screenConstraints, + 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 (isFirefox && session.screen) { + if (location.protocol !== 'https:') { + return error(SCREEN_COMMON_FAILURE); + } + warn(firefoxScreenCapturingWarning); + + screenConstraints.video = merge(screenConstraints.video.mandatory, { + 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 + if (constraints.audio /* && !session.video */ ) { + screenConstraints.audio = true; + constraints = {}; + } + + delete screenConstraints.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); + + 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: screenConstraints, + 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') { + screenConstraints.video.mandatory.chromeMediaSourceId = DetectRTC.screen.sourceId; + } + + var _isFirstSession = isFirstSession; + + _captureUserMedia(screenConstraints, 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, screenConstraints, constraints, session); + }, + onerror: function(e, constraintUsed) { + var mediaStreamError; + + // 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: firefoxScreenCapturingWarning, + 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')) { + 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') { + 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(firefoxScreenCapturingWarning); + } 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, screenConstraints, 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 === screenConstraints; + 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 screenConstraints = { + video: { + chromeMediaSource: 'external-stream' + } + }; + var forcedConstraints = isScreen ? screenConstraints : constraints; + onStreamSuccessCallback(stream, false, '', null, forcedConstraints, false, false, screenConstraints, 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; + } + + 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); + } + } + } + + if (!streamid) { + streamid = 'all'; + } + + var stream; + if (!isString(streamid) || streamid.search(/all|audio|video|screen/gi) !== -1) { + for (stream in connection.streams) { + if (connection._skip.indexOf(stream) === -1) { + var _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; + } + + 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); +}; diff --git a/RTCMultiConnection/dev/RTCMultiSession.js b/RTCMultiConnection/dev/RTCMultiSession.js new file mode 100644 index 00000000..9a524cfa --- /dev/null +++ b/RTCMultiConnection/dev/RTCMultiSession.js @@ -0,0 +1,2326 @@ +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(/typ host/g)) { + return; + } + + if (!turn && !!candidate.candidate.match(/typ relay/g)) { + return; + } + + if (!stun && !!candidate.candidate.match(/typ srflx/g)) { + return; + } + + var protocol = connection.iceProtocols; + + if (!protocol.udp && !!candidate.candidate.match(/ udp /g)) { + return; + } + + if (!protocol.tcp && !!candidate.candidate.match(/ tcp /g)) { + return; + } + + if (!window.selfNPObject) { + window.selfNPObject = candidate; + } + + if (!socket) { + return; + } + + 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) { + 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 Firefox; 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 = {}; + + function eventListener() { + setTimeout(function() { + mediaElement.muted = false; + afterRemoteStreamStartedFlowing({ + mediaElement: mediaElement, + session: session, + stream: stream, + preMuted: preMuted + }); + }, 3000); + + mediaElement.removeEventListener('play', eventListener); + } + + 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) { + 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') { + if (!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) { + if (!socket) { + return; + } + + 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 === false)) { + return afterRemoteStreamStartedFlowing(args); + } + + if (!args.numberOfTimes) { + args.numberOfTimes = 0; + } + + args.numberOfTimes++; + + if (!(args.mediaElement.readyState <= window.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); + } + } + } + + if (!!dontSendMessage) { + return; + } + + 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 + if (!!args.once) { + return; + } + + 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; + if (!peer.connection) { + return; + } + 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() { + if (!defaultSocket) { + return; + } + + 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; + + if (isChrome || (isFirefox && !peer.connection.getLocalStreams().length)) { + peer.addStream(stream); + } + + connection.renegotiatedSessions[JSON.stringify(_config.renegotiate)] = { + session: _config.renegotiate, + stream: stream + }; + + delete _config.renegotiate; + + createAnswer(); + }, _config.renegotiate); + } + + function createAnswer() { + // because Firefox has no support of renegotiation yet; + // so both chrome and firefox should redial instead of renegotiate! + if (isFirefox || _config.userinfo.browser === 'firefox') { + if (connection.peers[_config.userid]) { + connection.peers[_config.userid].redial(); + } + return; + } + + 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 newChannel = connection.token(); + newPrivateSocket({ + channel: newChannel, + extra: response.userData ? response.userData.extra : response.extra, + userid: response.userData ? response.userData.userid : response.userid + }); + + defaultSocket.send({ + participant: true, + targetUser: channel, + channel: newChannel + }); + } + + // if a user leaves + + function clearSession() { + 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 streamid in connection.streams) { + var stream = connection.streams[streamid]; + if (stream.userid === userid) { + onStreamEndedHandler(stream, connection); + delete connection.streams[streamid]; + } + } + + 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 && defaultSocket) { + // as soon as signaling gateway is connected; + // user should check existing rooms! + 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) { + if (!socket) { + return; + } + + 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.getExternalIceServers) { + loadIceFrame(function(iceServers) { + connection.iceServers = connection.iceServers.concat(iceServers); + }); + } + + 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) { + 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) { + peer.attachStreams = [e.stream]; + } + + // detaching old streams + detachMediaStream(connection.detachStreams, peer.connection); + + if (e.stream && (session.audio || session.video || session.screen)) { + // removeStream is not yet implemented in Firefox + // if(isFirefox) peer.connection.removeStream(e.stream); + + if (isChrome || (isFirefox && !peer.connection.getLocalStreams().length)) { + peer.addStream(e.stream); + } + } + + // because Firefox has no support of renegotiation yet; + // so both chrome and firefox should redial instead of renegotiate! + if (isFirefox || _peer.userinfo.browser === 'firefox') { + return _peer.redial(); + } + + 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); + + log('Disconnected your sockets, peers, streams and everything except RTCMultiConnection object.'); + }; +} diff --git a/RTCMultiConnection/dev/TextReceiver.js b/RTCMultiConnection/dev/TextReceiver.js new file mode 100644 index 00000000..d512daa4 --- /dev/null +++ b/RTCMultiConnection/dev/TextReceiver.js @@ -0,0 +1,53 @@ +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 + }; +} diff --git a/RTCMultiConnection/dev/TextSender.js b/RTCMultiConnection/dev/TextSender.js new file mode 100644 index 00000000..39854155 --- /dev/null +++ b/RTCMultiConnection/dev/TextSender.js @@ -0,0 +1,58 @@ +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); + } + } + } +}; diff --git a/RTCMultiConnection/dev/getUserMedia.js b/RTCMultiConnection/dev/getUserMedia.js new file mode 100644 index 00000000..c63136cd --- /dev/null +++ b/RTCMultiConnection/dev/getUserMedia.js @@ -0,0 +1,236 @@ +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); + } + + if (hints.video !== false) { + // 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; + } + + 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.video && !hints.video.mozMediaSource && hints.video.optional && hints.video.mandatory) { + if (!hints.video.optional.length && isEmpty(hints.video.mandatory)) { + hints.video = true; + } + } + } + + // 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.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 (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.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); + }); + } +} diff --git a/RTCMultiConnection/dev/globals.js b/RTCMultiConnection/dev/globals.js new file mode 100644 index 00000000..e67c85b9 --- /dev/null +++ b/RTCMultiConnection/dev/globals.js @@ -0,0 +1,619 @@ +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 && window.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 stringified = JSON.stringify(session); + if (stringified === '{}' || !stringified.split(',').length) { + return true; + } + return false; +} + +// 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() {} +}; + +var log = function() { + console.log(arguments); +}; + +var error = function() { + console.error(arguments); +}; + +var warn = function() { + console.warn(arguments); +}; + +if (isChrome || isFirefox || isSafari) { + log = console.log.bind(console); + error = console.error.bind(console); + warn = console.warn.bind(console); +} + +function toStr(obj) { + return JSON.stringify(obj, function(key, value) { + key = key; + if (value && value.sdp) { + log(value.sdp.type, '\t', value.sdp.sdp); + return ''; + } else { + return value; + } + }, '\t'); +} + +function getLength(obj) { + if (typeof obj !== 'object') { + throw 'Invalid data-type: ' + (typeof obj) + '; expected: object'; + } + + var stringified = JSON.stringify(obj); + if (stringified === '{}' || !stringified.split(',').length) { + return 0; + } + return stringified.split(',').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.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) + if (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); + + connection.numberOfConnectedUsers--; +} + +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'); + + if (!args.callback) { + return; + } + 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 + window.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 window.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 + }, '*'); + } + }; +} + +var loadedIceFrame; + +function loadIceFrame(callback, skip) { + if (loadedIceFrame) { + return; + } + if (!skip) { + return loadIceFrame(callback, true); + } + + loadedIceFrame = true; + + var iframe = document.createElement('iframe'); + iframe.onload = function() { + iframe.isLoaded = true; + + listenEventHandler('message', iFrameLoaderCallback); + + function iFrameLoaderCallback(event) { + if (!event.data || !event.data.iceServers) { + return; + } + callback(event.data.iceServers); + + // this event listener is no more needed + window.removeEventListener('message', iFrameLoaderCallback); + } + + iframe.contentWindow.postMessage('get-ice-servers', '*'); + }; + iframe.src = 'https://cdn.webrtc-experiment.com/getIceServers/'; + iframe.style.display = 'none'; + (document.body || document.documentElement).appendChild(iframe); +} + +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 firefoxScreenCapturingWarning = '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; + +var MediaStreamTrack = window.MediaStreamTrack; + +// 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); +} + +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 = window.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)); + } + }); +} + +window.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'); +} diff --git a/RTCMultiConnection/dev/head.js b/RTCMultiConnection/dev/head.js new file mode 100644 index 00000000..c4c6c754 --- /dev/null +++ b/RTCMultiConnection/dev/head.js @@ -0,0 +1,35 @@ +// Last time updated at Nov 18, 2014, 08:32:23 + +// 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.4 + +/* issues/features need to be fixed & implemented: + +-. v2.0.* changes-log here: http://www.rtcmulticonnection.org/changes-log/#v2.2 +-. trello: https://trello.com/b/8bhi1G6n/rtcmulticonnection + +--. screen capturing improved & some bugs fixed. +--. connection.stopMediaStream improved. +--. fixed: audio-only stream & crash. +--. added: connection.attachExternalStream(MediaStream, isScreen); +--. connection.candidates={relay:true} fixed. (a=candidate is removed). +--. connection.numberOfConnectedUsers is fixed. + +--. connection.rtcConfiguration +*/ + +'use strict'; + +(function() { diff --git a/RTCMultiConnection/dev/setDefaults.js b/RTCMultiConnection/dev/setDefaults.js new file mode 100644 index 00000000..211a4d12 --- /dev/null +++ b/RTCMultiConnection/dev/setDefaults.js @@ -0,0 +1,1409 @@ +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://chat.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) { + function _muteOrUnMute(stream, session, isMute) { + if (session.local && stream.type !== 'local') { + return; + } + if (session.remote && stream.type !== 'remote') { + return; + } + + if (session.screen && !stream.isScreen) { + return; + } + if (session.audio && !stream.isAudio) { + return; + } + if (session.video && !stream.isVideo) { + return; + } + + if (isMute) { + stream.mute(session); + } else { + stream.unmute(session); + } + } + + var stream; + + if (session && !isString(session)) { + for (stream in this) { + if (connection._skip.indexOf(stream) === -1) { + _muteOrUnMute(this[stream], session, enabled); + } + } + return; + } + + // implementation from #68 + for (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]; + } + }; + + var iceServers = []; + + iceServers.push({ + url: 'stun:stun.l.google.com:19302' + }); + + iceServers.push({ + url: 'stun:stun.anyfirewall.com:3478' + }); + + iceServers.push({ + url: 'turn:turn.bistri.com:80', + credential: 'homeo', + username: 'homeo' + }); + + iceServers.push({ + url: 'turn:turn.anyfirewall.com:443?transport=tcp', + credential: 'webrtc', + username: 'webrtc' + }); + + connection.iceServers = iceServers; + + 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(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(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 (session.video) { + self.videoRecorder = new window.RecordRTC(self.stream, { + type: 'video' + }); + } else if (session.audio) { + self.audioRecorder = new window.RecordRTC(self.stream, { + type: 'audio' + }); + } + } else if (isChrome) { + // chrome supports recording in two separate files: WAV and WebM + if (session.video) { + self.videoRecorder = new window.RecordRTC(self.stream, { + type: 'video' + }); + } + + if (session.audio) { + self.audioRecorder = new window.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) { + return; + } + + volumeChangeEventFired = true; + + if (!connection.streams[streamid]) { + return; + } + + 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 (!window.PreRecordedMediaStreamer) { + loadScript(connection.resources.PreRecordedMediaStreamer, function() { + connection.shareMediaFile(file, video, streamerid); + }); + return streamerid; + } + + return window.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 + if (!!args.once) { + return; + } + 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 + window.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++; + + if (event.peer.numOfRetries > 2) { + event.peer.numOfRetries = 0; + return; + } + + if (isFirefox || event.targetuser.browser === 'firefox') { + error('ICE connectivity check is failed. Re-establishing peer connection.'); + event.peer.redial(); + } else { + error('ICE connectivity check is failed. Renegotiating peer connection.'); + event.peer.renegotiate(); + } + }; + + 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 window.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; +} diff --git a/RTCMultiConnection/dev/tail.js b/RTCMultiConnection/dev/tail.js new file mode 100644 index 00000000..0319a0fe --- /dev/null +++ b/RTCMultiConnection/dev/tail.js @@ -0,0 +1 @@ +})(); diff --git a/RTCMultiConnection/latest.js b/RTCMultiConnection/latest.js deleted file mode 100644 index d1aeb785..00000000 --- a/RTCMultiConnection/latest.js +++ /dev/null @@ -1,6098 +0,0 @@ -// Last time updated at Nov 05, 2014, 08:32:23 - -// 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.3 - -/* issues/features need to be fixed & implemented: - --. v2.0.* changes-log here: http://www.rtcmulticonnection.org/changes-log/#v2.2 --. trello: https://trello.com/b/8bhi1G6n/rtcmulticonnection - --. v2.2.1 breaking updates: ---. connection.stats.sessions is removed; use connection.sessionDescriptions instead. ---. connection.stats.numberOfSessions is removed; use connection.numberOfSessions instead. ---. connection.stats.numberOfConnectedUsers is removed; use connection.numberOfConnectedUsers instead. ---. connection.getStats and connection.stats are removed. - ---. screen capturing improved & some bugs fixed. ---. connection.stopMediaStream improved. ---. fixed: audio-only stream & crash. ---. added: connection.attachExternalStream(MediaStream, isScreen); - -connection.rtcConfiguration -*/ - -(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, ''); - - // 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 custom video device is selected - if (connection._mediaSources.video) { - constraints.video = { - optional: [{ - sourceId: connection._mediaSources.video - }] - }; - } - - // 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 = merge(screen_constraints.video.mandatory, { - 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 - 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) { - 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(); - } - - 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); - } - } - } - 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 Firefox; 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; - - if (isChrome || (isFirefox && !peer.connection.getLocalStreams().length)) { - peer.addStream(stream); - } - - connection.renegotiatedSessions[JSON.stringify(_config.renegotiate)] = { - session: _config.renegotiate, - stream: stream - }; - - delete _config.renegotiate; - - createAnswer(); - }, _config.renegotiate); - } - - function createAnswer() { - // because Firefox has no support of renegotiation yet; - // so both chrome and firefox should redial instead of renegotiate! - if (isFirefox || _config.userinfo.browser == 'firefox') { - if (connection.peers[_config.userid]) { - connection.peers[_config.userid].redial(); - } - return; - } - - 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(); - } - - connection.getExternalIceServers && loadIceFrame(function(iceServers) { - connection.iceServers = connection.iceServers.concat(iceServers); - }); - - 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) { - peer.attachStreams = [e.stream]; - } - - // detaching old streams - detachMediaStream(connection.detachStreams, peer.connection); - - if (e.stream && (session.audio || session.video || session.screen)) { - // removeStream is not yet implemented in Firefox - // if(isFirefox) peer.connection.removeStream(e.stream); - - if (isChrome || (isFirefox && !peer.connection.getLocalStreams().length)) { - peer.addStream(e.stream); - } - } - - // because Firefox has no support of renegotiation yet; - // so both chrome and firefox should redial instead of renegotiate! - if (isFirefox || _peer.userinfo.browser == 'firefox') { - return _peer.redial(); - } - - 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 RTCPeerConnection = window.mozRTCPeerConnection || window.webkitRTCPeerConnection; - var RTCSessionDescription = window.mozRTCSessionDescription || window.RTCSessionDescription; - var RTCIceCandidate = window.mozRTCIceCandidate || window.RTCIceCandidate; - - 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 = self.serializeSdp(sessionDescription.sdp, createType); - 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); - } - }, - serializeSdp: function(sdp, createType) { - // it is "connection.processSdp=function(sdp){return sdp;}" - sdp = this.processSdp(sdp); - - if (isFirefox) return sdp; - - if (this.renegotiate) { - // sdp = sdp.replace(/a=rtpmap:.* rtx.*\r\n/gi, ''); - // sdp = sdp.replace(/a=fmtp:.* apt=.*\r\n/gi, ''); - // sdp = sdp.replace(/a=rtcp-fb.*\r\n/gi, ''); - // sdp = sdp.replace(/a=candidate:.*\r\n/gi, ''); - } - - if (this.session.inactive && !this.holdMLine) { - this.hold = true; - if ((this.session.screen || this.session.video) && this.session.audio) { - this.holdMLine = 'both'; - } else if (this.session.screen || this.session.video) { - this.holdMLine = 'video'; - } else if (this.session.audio) { - this.holdMLine = 'audio'; - } - } - - sdp = this.setBandwidth(sdp); - if (this.holdMLine == 'both') { - if (this.hold) { - this.prevSDP = sdp; - sdp = sdp.replace(/a=sendonly|a=recvonly|a=sendrecv/g, 'a=inactive'); - } else if (this.prevSDP) { - if (!this.session.inactive) { - // it means that DTSL key exchange already happened for single or multiple media lines. - // this block checks, key-exchange must be happened for all media lines. - sdp = this.prevSDP; - - // todo: test it: makes sense? - if (chromeVersion <= 35) { - return sdp; - } - } - } - } else if (this.holdMLine == 'audio' || this.holdMLine == 'video') { - sdp = sdp.split('m='); - - var audio = ''; - var video = ''; - - if (sdp[1] && sdp[1].indexOf('audio') == 0) { - audio = 'm=' + sdp[1]; - } - if (sdp[2] && sdp[2].indexOf('audio') == 0) { - audio = 'm=' + sdp[2]; - } - - if (sdp[1] && sdp[1].indexOf('video') == 0) { - video = 'm=' + sdp[1]; - } - if (sdp[2] && sdp[2].indexOf('video') == 0) { - video = 'm=' + sdp[2]; - } - - if (this.holdMLine == 'audio') { - if (this.hold) { - this.prevSDP = sdp[0] + audio + video; - sdp = sdp[0] + audio.replace(/a=sendonly|a=recvonly|a=sendrecv/g, 'a=inactive') + video; - } else if (this.prevSDP) { - sdp = this.prevSDP; - } - } - - if (this.holdMLine == 'video') { - if (this.hold) { - this.prevSDP = sdp[0] + audio + video; - sdp = sdp[0] + audio + video.replace(/a=sendonly|a=recvonly|a=sendrecv/g, 'a=inactive'); - } else if (this.prevSDP) { - sdp = this.prevSDP; - } - } - } - - if (!this.hold && this.session.inactive) { - // transport.cc&l=852 - http://goo.gl/0FxxqG - // dtlstransport.h&l=234 - http://goo.gl/7E4sYF - // http://tools.ietf.org/html/rfc4340 - - // From RFC 4145, SDP setup attribute values. - // http://goo.gl/xETJEp && http://goo.gl/3Wgcau - if (createType == 'offer') { - sdp = sdp.replace(/a=setup:passive|a=setup:active|a=setup:holdconn/g, 'a=setup:actpass'); - } else { - sdp = sdp.replace(/a=setup:actpass|a=setup:passive|a=setup:holdconn/g, 'a=setup:active'); - } - - // whilst doing handshake, either media lines were "inactive" - // or no media lines were present - sdp = sdp.replace(/a=inactive/g, 'a=sendrecv'); - } - // this.session.inactive = false; - return sdp; - }, - 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; - }, - setBandwidth: function(sdp) { - if (isMobileDevice || isFirefox || !this.bandwidth) return sdp; - - var bandwidth = this.bandwidth; - - if (this.session.screen) { - if (!bandwidth.screen) { - warn('It seems that you are not using bandwidth for screen. Screen sharing is expected to fail.'); - } else if (bandwidth.screen < 300) { - 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 && this.session.screen) { - 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 || bandwidth.data) { - sdp = sdp.replace(/b=AS([^\r\n]+\r\n)/g, ''); - } - - if (bandwidth.audio) { - sdp = sdp.replace(/a=mid:audio\r\n/g, 'a=mid:audio\r\nb=AS:' + bandwidth.audio + '\r\n'); - } - - if (bandwidth.video) { - sdp = sdp.replace(/a=mid:video\r\n/g, 'a=mid:video\r\nb=AS:' + (this.session.screen ? bandwidth.screen : bandwidth.video) + '\r\n'); - } - - if (bandwidth.data && !this.preferSCTP) { - sdp = sdp.replace(/a=mid:data\r\n/g, 'a=mid:data\r\nb=AS:' + bandwidth.data + '\r\n'); - } - - return sdp; - }, - setConstraints: function() { - this.constraints = { - optional: this.sdpConstraints.optional || isChrome ? [{ - VoiceActivityDetection: false - }] : [], - mandatory: this.sdpConstraints.mandatory || { - OfferToReceiveAudio: !!this.session.audio, - OfferToReceiveVideo: !!this.session.video || !!this.session.screen - } - }; - - if (this.constraints.mandatory) { - log('sdp-mandatory-constraints', toStr(this.constraints.mandatory)); - } - - if (this.constraints.optional) { - log('sdp-optional-constraints', toStr(this.constraints.optional)); - } - - 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, - iceTransports: this.rtcConfiguration.iceTransports - }; - } 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; - - 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) { - // if(isFirefox) this.create(this.type, this); - - log('recreating offer'); - - this.type = 'offer'; - this.renegotiate = true; - 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.renegotiate = true; - 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 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.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); - }); - } - } - - var FileSaver = { - SaveToDisk: function(fileUrl, fileName) { - var hyperlink = document.createElement('a'); - hyperlink.href = fileUrl; - hyperlink.target = '_blank'; - hyperlink.download = fileName || fileUrl; - - var mouseEvent = new MouseEvent('click', { - view: window, - bubbles: true, - cancelable: true - }); - - hyperlink.dispatchEvent(mouseEvent); - - // (window.URL || window.webkitURL).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 - }; - } - - 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.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 - }, '*'); - } - }; - } - - var iceFrame, loadedIceFrame; - - function loadIceFrame(callback, skip) { - if (loadedIceFrame) return; - if (!skip) return loadIceFrame(callback, true); - - loadedIceFrame = true; - - var iframe = document.createElement('iframe'); - iframe.onload = function() { - iframe.isLoaded = true; - - listenEventHandler('message', iFrameLoaderCallback); - - function iFrameLoaderCallback(event) { - if (!event.data || !event.data.iceServers) return; - callback(event.data.iceServers); - - // this event listener is no more needed - window.removeEventListener('message', iFrameLoaderCallback); - } - - iframe.contentWindow.postMessage('get-ice-servers', '*'); - }; - iframe.src = 'https://cdn.webrtc-experiment.com/getIceServers/'; - iframe.style.display = 'none'; - (document.body || document.documentElement).appendChild(iframe); - } - - 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; - - var MediaStreamTrack = window.MediaStreamTrack; - - // http://goo.gl/6ScAV9 - var DetectRTC = {}; - - (function() { - - DetectRTC.hasMicrophone = false; - DetectRTC.hasSpeakers = false; - DetectRTC.hasWebcam = false; - - DetectRTC.MediaDevices = []; - - // http://goo.gl/UVQRKk - // todo: switch to enumerateDevices when landed in canary. - function CheckDeviceSupport(callback) { - // This method is useful only for Chrome! - - // Firefox seems having no support of enumerateDevices feature yet. - if (isPluginRTC || isFirefox) { - callback && callback(); - return; - } - - // if "getSources" is available; prefer it over "getMediaDevices" - if (MediaStreamTrack && MediaStreamTrack.getSources) { - navigator.getMediaDevices = MediaStreamTrack.getSources.bind(MediaStreamTrack); - } - - // if still no "getMediaDevices"; it MUST be Firefox! - if (!navigator.getMediaDevices) { - log('navigator.getMediaDevices is undefined.'); - // assuming that it is older chrome or chromium implementation - if (isChrome) { - DetectRTC.hasMicrophone = true; - DetectRTC.hasSpeakers = true; - DetectRTC.hasWebcam = true; - } - - callback && callback(); - return; - } - - navigator.getMediaDevices(function(devices) { - DetectRTC.MediaDevices = []; - devices.forEach(function(device) { - // 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; - } - - DetectRTC.MediaDevices.push(device); - - if (device.kind == 'audioinput' || device.kind == 'audio') { - DetectRTC.hasMicrophone = true; - } - - if (device.kind == 'audiooutput') { - DetectRTC.hasSpeakers = true; - } - - if (device.kind == 'videoinput' || device.kind == 'video') { - DetectRTC.hasWebcam = true; - } - - // there is no "videoouput" in the spec. - }); - - if (callback) callback(); - }); - } - - DetectRTC.isWebRTCSupported = !!window.webkitRTCPeerConnection || !!window.mozRTCPeerConnection; - DetectRTC.isAudioContextSupported = (!!window.AudioContext || !!window.webkitAudioContext) && !!AudioContext.prototype.createMediaStreamSource; - DetectRTC.isScreenCapturingSupported = isChrome && chromeVersion >= 26 && (isNodeWebkit ? true : location.protocol == 'https:'); - DetectRTC.isSctpDataChannelsSupported = !!navigator.mozGetUserMedia || (isChrome && chromeVersion >= 25); - DetectRTC.isRtpDataChannelsSupported = isChrome && chromeVersion >= 31; - - // check for microphone/camera support! - CheckDeviceSupport(); - DetectRTC.load = CheckDeviceSupport; - - 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 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'); - } - - 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://chat.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]; - } - }; - - var iceServers = []; - - iceServers.push({ - url: 'stun:stun.l.google.com:19302' - }); - - iceServers.push({ - url: 'stun:stun.anyfirewall.com:3478' - }); - - iceServers.push({ - url: 'turn:turn.bistri.com:80', - credential: 'homeo', - username: 'homeo' - }); - - iceServers.push({ - url: 'turn:turn.anyfirewall.com:443?transport=tcp', - credential: 'webrtc', - username: 'webrtc' - }); - - connection.iceServers = iceServers; - - 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 (session.video) { - self.videoRecorder = RecordRTC(self.stream, { - type: 'video' - }); - } else if (session.audio) { - self.audioRecorder = RecordRTC(self.stream, { - type: 'audio' - }); - } - } else if (isChrome) { - // 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++; - - if (isFirefox || event.targetuser.browser == 'firefox') { - error('ICE connectivity check is failed. Re-establishing peer connection.'); - event.peer.numOfRetries < 2 && event.peer.redial(); - } else { - 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; - } -})(); diff --git a/RTCMultiConnection/package.json b/RTCMultiConnection/package.json index 152d7c52..5dc3f08c 100644 --- a/RTCMultiConnection/package.json +++ b/RTCMultiConnection/package.json @@ -1,15 +1,12 @@ { "name": "rtcmulticonnection", "preferGlobal": true, - "version": "2.2.3", - "author": { - "name": "Muaz Khan", - "email": "muazkh@gmail.com", - "url": "http://www.muazkhan.com/" - }, + "version": "2.2.4", + "author": "Muaz Khan (http://www.muazkhan.com/)", "description": "RTCMultiConnection is a WebRTC JavaScript wrapper library runs top over RTCPeerConnection API to provide multi-session establishment scenarios.", "scripts": { - "start": "node RTCMultiConnection.js" + "start": "node RTCMultiConnection.js", + "test": "grunt" }, "main": "./RTCMultiConnection.js", "repository": { @@ -38,11 +35,22 @@ ], "analyze": false, "license": "MIT", - "readmeFilename": "README.md", "bugs": { "url": "https://github.com/muaz-khan/RTCMultiConnection/wiki", "email": "muazkh@gmail.com" }, - "homepage": "http://www.rtcmulticonnection.org/changes-log/#v2.0", - "_from": "rtcmulticonnection@" + "homepage": "http://www.rtcmulticonnection.org/", + "_from": "rtcmulticonnection@", + "devDependencies": { + "grunt": "latest", + "grunt-cli": "latest", + "load-grunt-tasks": "latest", + "grunt-contrib-concat": "latest", + "grunt-contrib-csslint": "latest", + "grunt-contrib-jshint": "latest", + "grunt-contrib-uglify": "latest", + "grunt-htmlhint": "latest", + "grunt-jsbeautifier": "latest", + "grunt-bump": "latest" + } } diff --git a/RecordRTC/.gitignore b/RecordRTC/.gitignore new file mode 100644 index 00000000..3fb9d160 --- /dev/null +++ b/RecordRTC/.gitignore @@ -0,0 +1,5 @@ +# Node +node_modules + +# bower +bower_components diff --git a/RecordRTC/.jshintrc b/RecordRTC/.jshintrc new file mode 100644 index 00000000..4e109a39 --- /dev/null +++ b/RecordRTC/.jshintrc @@ -0,0 +1,33 @@ +{ + "browser": true, + "camelcase": true, + "curly": true, + "devel": true, + "eqeqeq": true, + "forin": false, + "globalstrict": true, + "quotmark": "single", + "undef": true, + "globals": { + "RecordRTC": true, + "MRecordRTC": true, + "Storage": true, + "StereoRecorder": true, + "StereoAudioRecorder": true, + "CanvasRecorder": true, + "WhammyRecorder": true, + "Whammy": true, + "DiskStorage": true, + "GifRecorder": true, + "requestAnimationFrame": true, + "cancelAnimationFrame": true, + "AudioContext": true, + "URL": true, + "IsChrome": true, + "mergeProps": true, + "reformatProps": true, + "bytesToSize": true, + "FileReaderSync": true, + "postMessage": true + } +} diff --git a/RecordRTC/.npmignore b/RecordRTC/.npmignore new file mode 100644 index 00000000..fe416ccd --- /dev/null +++ b/RecordRTC/.npmignore @@ -0,0 +1,3 @@ +node_modules +lib-cov +npm-debug.log \ No newline at end of file diff --git a/RecordRTC/.travis.yml b/RecordRTC/.travis.yml new file mode 100644 index 00000000..c1cc7069 --- /dev/null +++ b/RecordRTC/.travis.yml @@ -0,0 +1,10 @@ +language: node_js +node_js: + - "0.11" +install: npm install +before_script: + - npm install grunt-cli + - npm install grunt + - grunt +matrix: + fast_finish: true diff --git a/RecordRTC/AudioVideo-on-Firefox.html b/RecordRTC/AudioVideo-on-Firefox.html index 7bbed071..6dc92f1b 100644 --- a/RecordRTC/AudioVideo-on-Firefox.html +++ b/RecordRTC/AudioVideo-on-Firefox.html @@ -7,207 +7,211 @@ - - WebRTC Audio+Video Recording using RecordRTC ® Muaz Khan - - - - - - - - - - + + + WebRTC Audio+Video Recording using RecordRTC ® Muaz Khan + + + + + + + + + + + + + + + + + +
+
+

+ Recording audio+video using RecordRTC! +

+

+ HOME + © + Muaz Khan . + @WebRTCWeb . + Github . + Latest issues . + What's New? +

+
+ +
+ +
+ This WebRTC experiment records both audio/video in single WebM/mp4 container/format using MediaRecorder API! It currently works only on Firefox >= 29. +
+
For chrome, please try https://www.webrtc-experiment.com/RecordRTC/. +
+ +
+ +
+

Record Audio+Video using Firefox >= 29

+
+

+
+ +
+ + +
+
+
+ - - - - - - -
-
-

- Recording audio+video using RecordRTC! -

-

- HOME - © - Muaz Khan - - . - @WebRTCWeb - - . - Github - - . - Latest issues - - . - What's New? -

-
- -
- -
- This WebRTC experiment records both audio/video in single WebM/mp4 container/format using MediaRecorder API! It currently works only on Firefox >= 29.

- For chrome, please try https://www.webrtc-experiment.com/RecordRTC/. -
- -
- -
-

Record Audio+Video using Firefox >= 29

-
-


-
- -
-
-
- - - -
-

Firefox and RecordRTC...

-
    -
  1. - Individual audio recordings in ogg container (Firefox >= 26) -
  2. -
  3. - Audio+Video in single WebM/mp4 container (Firefox >= 29) -
  4. -
  5. - Gif recording (Firefox >= 18) -
  6. -
-
- -
-

Using RecordRTC...

-
    -
  1. - You can record both audio/video in single webm file. It works only on Firefox! -
  2. -
  3. - For chrome; you can record audio as WAV and video as WebM or animated Gif. -
  4. -
-
- -
-

Feedback

-
- -
- Enter your email too; if you want "direct" reply! -
- -
-

- RecordRTC is MIT licensed on Github! Documentation -

-
- -
-

Latest Updates

-
-
-
- - - -
-

- WebRTC Experiments - © - muazkh@gmail.com + function captureUserMedia(callback) { + navigator.getUserMedia = navigator.mozGetUserMedia || navigator.webkitGetUserMedia; + navigator.getUserMedia({ + audio: true, + video: true + }, function(stream) { + video.src = URL.createObjectURL(stream); + video.muted = true; + video.controls = true; + video.play(); + + callback(stream); + }, function(error) { + console.error(error); + }); + } + + +

+

Firefox and RecordRTC...

+
    +
  1. + Individual audio recordings in ogg container (Firefox >= 26) +
  2. +
  3. + Audio+Video in single WebM/mp4 container (Firefox >= 29) +
  4. +
  5. + Gif recording (Firefox >= 18) +
  6. +
+
+ +
+

Using RecordRTC...

+
    +
  1. + You can record both audio/video in single webm file. It works only on Firefox! +
  2. +
  3. + For chrome; you can record audio as WAV and video as WebM or animated Gif. +
  4. +
+
+ +
+

Feedback

+
+ +
+ + Enter your email too; if you want "direct" reply! +
+ +
+

+ RecordRTC is MIT licensed on Github! Documentation

-
- - - - - + + +
+

Latest Updates +

+
+
+
+ + + + + + + + + + \ No newline at end of file diff --git a/RecordRTC/Canvas-Recording/index.html b/RecordRTC/Canvas-Recording/index.html index ce539b77..43f8a668 100644 --- a/RecordRTC/Canvas-Recording/index.html +++ b/RecordRTC/Canvas-Recording/index.html @@ -1,66 +1,79 @@ -RecordRTC Canvas/HTML Recorder - +RecordRTC Canvas/HTML Recorder + +
@@ -72,14 +85,8 @@

Content is edit-able.


-var recorder = RecordRTC(elementToShare, {
-    type: 'canvas'
-});
-recorder.startRecording();
-recorder.stopRecording(function(url) {
-    window.open(url);
-});
-
+ var recorder = RecordRTC(elementToShare, { type: 'canvas' }); recorder.startRecording(); recorder.stopRecording(function(url) { window.open(url); }); +
@@ -87,8 +94,10 @@

Content is edit-able.

- - + + + + + + + + + + +
+
+

+ MRecordRTC: WebRTC audio+video recording ® + Muaz Khan +

+

+ HOME + © + Muaz Khan . + @WebRTCWeb . + Github . + Latest issues . + What's New? +

+
+ +
+ +
+ + + + +
+ +
+ +
+

+ Try RecordRTC +

+
    +
  1. + Audio+Video Recording on Firefox +
  2. +
  3. + RecordRTC / PHP / FFmpeg (synced audio/video in single file!) +
  4. +
  5. RecordRTC-to-Nodejs (used ffmpeg to merge wav/webm in single WebM container)
  6. +
  7. RecordRTC-to-PHP (audio/video recording and uploading to server)
  8. +
  9. RecordRTC-to-ASP.NET MVC (audio/video recording and uploading to server)
  10. +
  11. Canvas Recording! (webpage recording)
  12. +
  13. MRecordRTC and writeToDisk/getFromDisk! +
  14. +
  15. RecordRTC-to-Socket.io (used ffmpeg to merge wav/webm in single WebM container)
  16. +
  17. RecordRTC and ffmpeg-asm.js (ffmpeg inside the browser!)
  18. +
+
+ - - - - - - - - -
-
-

- MRecordRTC: WebRTC audio+video recording ® - Muaz Khan -

-

- HOME - © - Muaz Khan - - . - @WebRTCWeb - - . - Github - - . - Latest issues - - . - What's New? -

-
- -
- -
- - - - -
- -
- -
-

- Try RecordRTC -

-
    -
  1. - Audio+Video Recording on Firefox -
  2. -
  3. - RecordRTC / PHP / FFmpeg (synced audio/video in single file!) -
  4. -
  5. RecordRTC-to-Nodejs (used ffmpeg to merge wav/webm in single WebM container)
  6. -
  7. RecordRTC-to-PHP (audio/video recording and uploading to server)
  8. -
  9. RecordRTC-to-ASP.NET MVC (audio/video recording and uploading to server)
  10. -
  11. Canvas Recording! (webpage recording)
  12. -
  13. MRecordRTC and writeToDisk/getFromDisk!
  14. -
  15. RecordRTC-to-Socket.io (used ffmpeg to merge wav/webm in single WebM container)
  16. -
  17. RecordRTC and ffmpeg-asm.js (ffmpeg inside the browser!)
  18. -
-
- - - -
-

Using MRecordRTC...

-

- You can record audio in wav/ogg file format; and video in either webm format or - as animated GIF image.

-
- -
-

- How to use MRecordRTC?

-
-<script src="//cdn.WebRTC-Experiment.com/RecordRTC.js"></script>
-
-
-
-
-var recorder = new MRecordRTC();
-recorder.addStream(MediaStream);
-recorder.mediaType = {
-   audio: true,
-   video: true,
-   gif: true
-};
-recorder.startRecording();
-recorder.stopRecording(function(url, type) {
-    document.querySelector(type).src = url;
-});
-
-recorder.getBlob(function(blobs) {
-   blobs.audio --- audio blob
-   blobs.video --- video blob
-   blobs.gif   --- gif blob
-});
-
-// invoke save-as dialog
-// for all recorded blobs
-recorder.save();
-
-recorder.writeToDisk();
-
-// get all blobs from disk
-MRecordRTC.getFromDisk('all', function(dataURL, type) {
-   type == 'audio'
-   type == 'video'
-   type == 'gif'
-});
-
-// or get just single blob
-MRecordRTC.getFromDisk('audio', function(dataURL) {
-   // only audio blob is returned from disk!
-});
-
- -
- -
-

- MRecordRTC is MIT licensed on Github! Documentation -

-
- -
-

Feedback

-
- -
- Enter your email too; if you want "direct" reply! -
- -
-

Latest Updates

-
-
-
- - - -
-

- WebRTC Experiments - © - muazkh@gmail.com + videosContainer.appendChild(mediaElement); + document.querySelector('#audio').play(); + mediaElement.media.play(); + } + + if (type == 'gif') { + var gifImage = document.createElement('img'); + gifImage.src = dataURL; + videosContainer.appendChild(gifImage); + } + }); + + !!navigator.mozGetUserMedia && MRecordRTC.getFromDisk('video', function(dataURL) { + if (!dataURL) return; + + var video = document.createElement('video'); + video.src = dataURL; + var mediaElement = getMediaElement(video, { + buttons: ['mute-video'], + showOnMouseEnter: false, + enableTooltip: false, + onMuted: function() { + mediaElement.muted = true; + }, + onUnMuted: function() { + mediaElement.muted = false; + mediaElement.play(); + } + }); + videosContainer.appendChild(mediaElement); + mediaElement.media.play(); + }); + }; + + window.addEventListener('beforeunload', function() { + document.querySelector('#start').disabled = false; + document.querySelector('#stop').disabled = false; + document.querySelector('#get').disabled = false; + }, false); + + +

+

Using MRecordRTC...

+

+ You can record audio in wav/ogg file format; and video in either webm format or as animated GIF image.

+
+ +
+

+ How to use MRecordRTC?

+
+                <script src="//cdn.WebRTC-Experiment.com/RecordRTC.js"></script>
+            
+
+
+
+                var recorder = new MRecordRTC(); recorder.addStream(MediaStream); recorder.mediaType = { audio: true, video: true, gif: true }; recorder.startRecording(); recorder.stopRecording(function(url, type) { document.querySelector(type).src = url; }); recorder.getBlob(function(blobs) { blobs.audio --- audio blob blobs.video --- video blob blobs.gif --- gif blob }); // invoke save-as dialog // for all recorded blobs recorder.save(); recorder.writeToDisk(); // get all blobs from disk MRecordRTC.getFromDisk('all', function(dataURL, type) { type == 'audio' type == 'video' type == 'gif' }); // or get just single blob MRecordRTC.getFromDisk('audio', function(dataURL) { // only audio blob is returned from disk! });
+            
+ +
+ +
+

+ MRecordRTC is MIT licensed on Github! Documentation

-
- - - - - + + +
+

Feedback

+
+ +
+ + Enter your email too; if you want "direct" reply! +
+ +
+

Latest Updates +

+
+
+
+ + + + + + + + + + \ No newline at end of file diff --git a/RecordRTC/PHP-and-FFmpeg/index.html b/RecordRTC/PHP-and-FFmpeg/index.html index d1752352..743e414c 100644 --- a/RecordRTC/PHP-and-FFmpeg/index.html +++ b/RecordRTC/PHP-and-FFmpeg/index.html @@ -1,4 +1,4 @@ - + + + + +

+ RecordRTC + PHP + FFmpeg +

+
+

+ +

+ + + + +
+
+ + - - - -

- RecordRTC - PHP - FFmpeg -

-
-

- -

- - - - -
-
- - - - + }; + request.open('POST', url); + request.send(data); + } + + + + \ No newline at end of file diff --git a/RecordRTC/README.md b/RecordRTC/README.md index 9f2a39d6..a1c7c710 100644 --- a/RecordRTC/README.md +++ b/RecordRTC/README.md @@ -2,10 +2,12 @@ [RecordRTC Documentation](https://RecordRTC.org/) / [RecordRTC Wiki Pages](https://github.com/muaz-khan/RecordRTC/wiki) / [RecordRTC Demo](https://www.webrtc-experiment.com/RecordRTC/) / [WebRTC Experiments](https://www.webrtc-experiment.com/) -[![npm](https://img.shields.io/npm/v/recordrtc.svg)](https://npmjs.org/package/recordrtc) [![downloads](https://img.shields.io/npm/dm/recordrtc.svg)](https://npmjs.org/package/recordrtc) +[![npm](https://img.shields.io/npm/v/recordrtc.svg)](https://npmjs.org/package/recordrtc) [![downloads](https://img.shields.io/npm/dm/recordrtc.svg)](https://npmjs.org/package/recordrtc) [![Build Status: Linux](https://travis-ci.org/muaz-khan/RecordRTC.png?branch=master)](https://travis-ci.org/muaz-khan/RecordRTC) > [RecordRTC](https://www.webrtc-experiment.com/RecordRTC/) is a JavaScript-based media-recording library for modern web-browsers (supporting WebRTC getUserMedia API). It is optimized for different devices and browsers to bring all client-side (pluginfree) recording solutions in single place. +Please check [dev](https://github.com/muaz-khan/RecordRTC/tree/master/dev) directory for development files. + 1. [RecordRTC API Reference](http://RecordRTC.org/RecordRTC.html) 2. [MRecordRTC API Reference](http://RecordRTC.org/MRecordRTC.html) 3. [MediaStreamRecorder API Reference](http://RecordRTC.org/MediaStreamRecorder.html) @@ -53,6 +55,12 @@ npm install recordrtc ``` +or using [Bower](http://bower.io): + +``` +bower install recordrtc +``` + To use it: ```html diff --git a/RecordRTC/RecordRTC-over-Socketio/index.html b/RecordRTC/RecordRTC-over-Socketio/index.html index b86b92d3..c8739f5f 100644 --- a/RecordRTC/RecordRTC-over-Socketio/index.html +++ b/RecordRTC/RecordRTC-over-Socketio/index.html @@ -10,209 +10,214 @@ - - - RecordRTC over Socket.io - - - - - - - - - - - -

RecordRTC over Socket.io

-

- -

- -
- -
-
- -
- - -
- - + + + + + + + + + + + +

RecordRTC over Socket.io +

+

+ +

+ +
+ + +
+
+ +
+ + +
+ + - - - + cameraPreview.src = ''; + cameraPreview.poster = 'ajax-loader.gif'; + }); + }; + + socketio.on('merged', function(fileName) { + var href = (location.href.split('/').pop().length ? location.href.replace(location.href.split('/').pop(), '') : location.href); + + href = href + '/uploads/' + fileName; + + console.log('got file ' + href); + + cameraPreview.src = href + cameraPreview.play(); + cameraPreview.muted = false; + cameraPreview.controls = true; + }); + + socketio.on('ffmpeg-output', function(result) { + if (parseInt(result) >= 100) { + progressBar.parentNode.style.display = 'none'; + return; + } + progressBar.parentNode.style.display = 'block'; + progressBar.value = result; + percentage.innerHTML = 'Ffmpeg Progress ' + result + "%"; + }); + + socketio.on('ffmpeg-error', function(error) { + alert(error); + }); + + + + + \ No newline at end of file diff --git a/RecordRTC/RecordRTC-to-Nodejs/static/index.html b/RecordRTC/RecordRTC-to-Nodejs/static/index.html index 9f481c14..086833cb 100644 --- a/RecordRTC/RecordRTC-to-Nodejs/static/index.html +++ b/RecordRTC/RecordRTC-to-Nodejs/static/index.html @@ -6,289 +6,305 @@ - - - RecordRTC over Node.js - - - - - - + - a { - color: #2844FA; - text-decoration: none; - } + +

RecordRTC over Node.js +

+

+ +

+
- a:hover, a:focus { color: #1B29A4; } +
+ + +
+
- a:active { color: #000; } - - audio, video { - border: 1px solid rgb(15, 158, 238); width: 94%; - } - button[disabled], input[disabled] { background: rgba(216, 205, 205, 0.2); border: 1px solid rgb(233, 224, 224);} - - - -

RecordRTC over Node.js

-

- -


- -
- -
-
- -
- -
- - -
- - - - + + + + - - + + + + - - - - + + + - btnStopRecording.onclick = function() { - btnStartRecording.disabled = false; - btnStopRecording.disabled = true; - - if(isRecordOnlyAudio) { - audioRecorder.stopRecording(onStopRecording); - } - - if(!isRecordOnlyAudio) { - audioRecorder.stopRecording(function() { - videoRecorder.stopRecording(function() { - onStopRecording(); - }); - }); - } - }; - - - - - - + \ No newline at end of file diff --git a/RecordRTC/RecordRTC-to-PHP/index.html b/RecordRTC/RecordRTC-to-PHP/index.html index 228cc89e..19667989 100644 --- a/RecordRTC/RecordRTC-to-PHP/index.html +++ b/RecordRTC/RecordRTC-to-PHP/index.html @@ -1,4 +1,4 @@ - - - RecordRTC to PHP ® Muaz Khan - - - - - - - - - + + + + + + + +
+
+

+ RecordRTC to PHP ® + Muaz Khan +

+

+ HOME + © + Muaz Khan . + @WebRTCWeb . + Github . + Latest issues . + What's New? +

+
- input { - border: 1px solid #d9d9d9; - border-radius: 1px; - font-size: 2em; - margin: .2em; - width: 30%; - } +
- p, .inner { padding: 1em; } +
+

Record and POST to Server! ( www.RecordRTC.org )

- li { - border-bottom: 1px solid rgb(189, 189, 189); - border-left: 1px solid rgb(189, 189, 189); - padding: .5em; - } +

+ +

+
+ + + + + +
+
+ +
+

Try Audio+Video Recording on Firefox +

+
    +
  1. + RecordRTC / PHP / FFmpeg (synced audio/video in single file!) +
  2. +
  3. RecordRTC-to-Nodejs (used ffmpeg to merge wav/webm in single WebM container)
  4. +
  5. RecordRTC-to-PHP (audio/video recording and uploading to server)
  6. +
  7. RecordRTC-to-ASP.NET MVC (audio/video recording and uploading to server)
  8. +
  9. Canvas Recording! (webpage recording)
  10. +
  11. MRecordRTC and writeToDisk/getFromDisk! +
  12. +
  13. RecordRTC-to-Socket.io (used ffmpeg to merge wav/webm in single WebM container)
  14. +
  15. RecordRTC and ffmpeg-asm.js (ffmpeg inside the browser!)
  16. +
+
- label { - display: inline-block; - width: 8em; - } - - - - - - - -
-
-

- RecordRTC to PHP ® - Muaz Khan -

-

- HOME - © - Muaz Khan - - . - @WebRTCWeb - - . - Github - - . - Latest issues - - . - What's New? -

-
- -
- -
-

Record and POST to Server! ( www.RecordRTC.org )

- -

- -

-
- - - - - -
-
- -
-

Try Audio+Video Recording on Firefox

-
    -
  1. - RecordRTC / PHP / FFmpeg (synced audio/video in single file!) -
  2. -
  3. RecordRTC-to-Nodejs (used ffmpeg to merge wav/webm in single WebM container)
  4. -
  5. RecordRTC-to-PHP (audio/video recording and uploading to server)
  6. -
  7. RecordRTC-to-ASP.NET MVC (audio/video recording and uploading to server)
  8. -
  9. Canvas Recording! (webpage recording)
  10. -
  11. MRecordRTC and writeToDisk/getFromDisk!
  12. -
  13. RecordRTC-to-Socket.io (used ffmpeg to merge wav/webm in single WebM container)
  14. -
  15. RecordRTC and ffmpeg-asm.js (ffmpeg inside the browser!)
  16. -
-
- - - -
-
    -
  1. Both files are recorded and uploaded individually (wav/webm)
  2. -
  3. You can merge/mux them in single format like avi or mkv — using tools like ffmpeg/avconv
  4. -
-
- -
-

- How to save recorded wav/webm file to PHP server?

-
    -
  1. Write a PHP file to write recrded blob on disk
  2. -
  3. Write Javascript to POST recorded blobs to server using XHR2/FormdData
  4. -
-
- -
-

PHP Code

-
-<?php
-foreach(array('video', 'audio') as $type) {
-    if (isset($_FILES["${type}-blob"])) {
-
-        $fileName = $_POST["${type}-filename"];
-        $uploadDirectory = "uploads/$fileName";
-
-        if (!move_uploaded_file($_FILES["${type}-blob"]["tmp_name"], $uploadDirectory)) {
-            echo("problem moving uploaded file");
-        }
+            }
 
-        echo($uploadDirectory);
-    }
-}
-?>
-
-
-
-

Javascript Code

-
-var fileType = 'video'; // or "audio"
-var fileName = 'ABCDEF.webm';  // or "wav"
-
-var formData = new FormData();
-formData.append(fileType + '-filename', fileName);
-formData.append(fileType + '-blob', blob);
-
-xhr('save.php', formData, function (fName) {
-    window.open(location.href + fName);
-});
-
-function xhr(url, data, callback) {
-    var request = new XMLHttpRequest();
-    request.onreadystatechange = function () {
-        if (request.readyState == 4 && request.status == 200) {
-            callback(location.href + request.responseText);
+            request.open('POST', url);
+            request.send(data);
         }
-    };
-    request.open('POST', url);
-    request.send(data);
-}
-
-
-

- How to use RecordRTC?

-
-<script src="https://www.webrtc-experiment.com/RecordRTC.js"></script>
-
-
-
-

- How to record audio using RecordRTC?

-
-var recordRTC = RecordRTC(mediaStream);
-recordRTC.startRecording();
-recordRTC.stopRecording(function(audioURL) {
-   window.open(audioURL);
-});
-
- -
- - -
-

- How to record video using RecordRTC?

-
-var options = {
-   type: 'video',
-   video: {
-      width: 320,
-      height: 240
-   },
-   canvas: {
-      width: 320,
-      height: 240
-   }
-};
-var recordRTC = RecordRTC(mediaStream, options);
-recordRTC.startRecording();
-recordRTC.stopRecording(function(videoURL) {
-   window.open(videoURL);
-});
-
- -
- -
- -

- How to record animated GIF using RecordRTC?

-
-var options = {
-   type: 'gif',
-   video: {
-      width: 320,
-      height: 240
-   },
-   canvas: {
-      width: 320,
-      height: 240
-   },
-   frameRate: 200,
-   quality: 10
-};
-var recordRTC = RecordRTC(mediaStream, options);
-recordRTC.startRecording();
-recordRTC.stopRecording(function(gifURL) {
-   window.open(gifURL);
-});
-
-
- -
-

- Possible + + window.onbeforeunload = function() { + if (!!fileName) { + deleteAudioVideoFiles(); + return 'It seems that you\'ve not deleted audio/video files from the server.'; + } + }; + + +
+
    +
  1. Both files are recorded and uploaded individually (wav/webm)
  2. +
  3. You can merge/mux them in single format like avi or mkv — using tools like ffmpeg/avconv
  4. +
+
+ +
+

+ How to save recorded wav/webm file to PHP server?

+
    +
  1. Write a PHP file to write recrded blob on disk
  2. +
  3. Write Javascript to POST recorded blobs to server using XHR2/FormdData
  4. +
+
+ +
+

PHP Code

+
+                <?php foreach(array('video', 'audio') as $type) { if (isset($_FILES["${type}-blob"])) { $fileName = $_POST["${type}-filename"]; $uploadDirectory = "uploads/$fileName"; if (!move_uploaded_file($_FILES["${type}-blob"]["tmp_name"], $uploadDirectory)) { echo("problem moving uploaded file"); } echo($uploadDirectory); } } ?>
+            
+
+
+

Javascript Code

+
+                var fileType = 'video'; // or "audio" var fileName = 'ABCDEF.webm'; // or "wav" var formData = new FormData(); formData.append(fileType + '-filename', fileName); formData.append(fileType + '-blob', blob); xhr('save.php', formData, function (fName) { window.open(location.href + fName); }); function xhr(url, data, callback) { var request = new XMLHttpRequest(); request.onreadystatechange = function () { if (request.readyState == 4 && request.status == 200) { callback(location.href + request.responseText); } }; request.open('POST', url); request.send(data); }
+            
+
+
+

+ How to use RecordRTC?

+
+                <script src="https://www.webrtc-experiment.com/RecordRTC.js"></script>
+            
+
+
+

+ How to record audio using RecordRTC?

+
+                var recordRTC = RecordRTC(mediaStream); recordRTC.
+                startRecording(); recordRTC.
+                stopRecording(function(
+                audioURL) { window.open(audioURL); });
+            
+ +
+ + +
+

+ How to record video using RecordRTC?

+
+                var options = {
+                type: 'video',
+                video: { width: 320, height: 240 },
+                canvas: { width: 320, height: 240 } }; var recordRTC = RecordRTC(mediaStream, options); recordRTC.
+                startRecording(); recordRTC.
+                stopRecording(function(
+                videoURL) { window.open(videoURL); });
+            
+ +
+ +
+ +

+ How to record animated GIF using RecordRTC?

+
+                var options = {
+                type: 'gif',
+                video: { width: 320, height: 240 },
+                canvas: { width: 320, height: 240 },
+                frameRate: 200,
+                quality: 10 }; var recordRTC = RecordRTC(mediaStream, options); recordRTC.
+                startRecording(); recordRTC.
+                stopRecording(function(
+                gifURL) { window.open(gifURL); });
+            
+
+ +
+

+ Possible + issues/failures: -

-

- The biggest issue is that RecordRTC is unable to record both audio and video streams in single file.

- Do you know "RecordRTC" fails recording audio because following conditions fails: -

    -
  1. Sample rate and channel configuration must be the same for input and output sides - on Windows i.e. audio input/output devices must match
  2. -
  3. Only the Default microphone device can be used for capturing.
  4. -
  5. The requesting scheme is must be one of the following: http, https, chrome, extension's, - or file (only works with --allow-file-access-from-files)
  6. -
  7. The browser must be able to create/initialize the metadata database for the API - under the profile directory
  8. -
-

-
-
-

- RecordRTC is MIT licensed on Github! Documentation -

-
- -
-

Feedback

-
- -
- Enter your email too; if you want "direct" reply! -
- -
-

Latest Updates

-
-
-

- - - -

+

+ The biggest issue is that RecordRTC is + unable to record both audio and video streams in single file. +
+
Do you know "RecordRTC" fails recording audio because following conditions fails: +

    +
  1. Sample rate and channel configuration must be the same for input and output sides on Windows i.e. audio input/output devices must match
  2. +
  3. Only the Default microphone device can be used for capturing.
  4. +
  5. The requesting scheme is must be one of the following: http, https, chrome, extension's, or file (only works with --allow-file-access-from-files)
  6. +
  7. The browser must be able to create/initialize the metadata database for the API under the profile directory
  8. +
+

+ +

- WebRTC Experiments - © - muazkh@gmail.com + RecordRTC is MIT licensed on Github! Documentation

- - - - - - +
+ +
+

Feedback

+
+ +
+ + Enter your email too; if you want "direct" reply! +
+ +
+

Latest Updates +

+
+
+ + + + + + + + + + + \ No newline at end of file diff --git a/RecordRTC/RecordRTC.js b/RecordRTC/RecordRTC.js index c89cca27..9f33457e 100644 --- a/RecordRTC/RecordRTC.js +++ b/RecordRTC/RecordRTC.js @@ -1,4 +1,4 @@ -// Last time updated at Nov 11, 2014, 08:32:23 +// Last time updated at Nov 19, 2014, 08:32:23 // links: // Open-Sourced: https://github.com/muaz-khan/RecordRTC @@ -45,32 +45,38 @@ // 12. GifRecorder.js //------------------------------------ +'use strict'; // ____________ // RecordRTC.js /** -* RecordRTC is a JavaScript-based media-recording library for modern web-browsers (supporting WebRTC getUserMedia API). It is optimized for different devices and browsers to bring all client-side (pluginfree) recording solutions in single place. -* @summary JavaScript audio/video recording library runs top over WebRTC getUserMedia API. -* @license {@link https://www.webrtc-experiment.com/licence/|MIT} -* @author {@link https://www.MuazKhan.com|Muaz Khan} -* @typedef RecordRTC -* @class -* @example -* var recordRTC = RecordRTC(mediaStream, { -* type: 'video' // audio or video or gif or canvas -* }); -* -* // or, you can even use keyword "new" -* var recordRTC = new RecordRTC(mediaStream[, config]); -* @see For further information: -* @see {@link https://github.com/muaz-khan/RecordRTC|RecordRTC Source Code} -*/ + * RecordRTC is a JavaScript-based media-recording library for modern web-browsers (supporting WebRTC getUserMedia API). It is optimized for different devices and browsers to bring all client-side (pluginfree) recording solutions in single place. + * @summary JavaScript audio/video recording library runs top over WebRTC getUserMedia API. + * @license {@link https://www.webrtc-experiment.com/licence/|MIT} + * @author {@link https://www.MuazKhan.com|Muaz Khan} + * @typedef RecordRTC + * @class + * @example + * var recordRTC = RecordRTC(mediaStream, { + * type: 'video' // audio or video or gif or canvas + * }); + * + * // or, you can even use keyword "new" + * var recordRTC = new RecordRTC(mediaStream[, config]); + * @see For further information: + * @see {@link https://github.com/muaz-khan/RecordRTC|RecordRTC Source Code} + */ function RecordRTC(mediaStream, config) { config = config || {}; - if (!mediaStream) throw 'MediaStream is mandatory.'; - if (!config.type) config.type = 'audio'; + if (!mediaStream) { + throw 'MediaStream is mandatory.'; + } + + if (!config.type) { + config.type = 'audio'; + } var self = this; @@ -79,16 +85,22 @@ function RecordRTC(mediaStream, config) { // Media Stream Recording API has not been implemented in chrome yet; // That's why using WebAudio API to record stereo audio in WAV format - var Recorder = IsChrome ? window.StereoRecorder : window.MediaStreamRecorder; + var Recorder = isChrome ? window.StereoRecorder : window.MediaStreamRecorder; // video recorder (in WebM format) - if (config.type == 'video' && IsChrome) Recorder = window.WhammyRecorder; + if (config.type === 'video' && isChrome) { + Recorder = window.WhammyRecorder; + } // video recorder (in Gif format) - if (config.type == 'gif') Recorder = window.GifRecorder; + if (config.type === 'gif') { + Recorder = window.GifRecorder; + } // html2canvas recording! - if (config.type == 'canvas') Recorder = window.CanvasRecorder; + if (config.type === 'canvas') { + Recorder = window.CanvasRecorder; + } mediaRecorder = new Recorder(mediaStream); @@ -96,11 +108,15 @@ function RecordRTC(mediaStream, config) { mediaRecorder = mergeProps(mediaRecorder, config); mediaRecorder.onAudioProcessStarted = function() { - if (config.onAudioProcessStarted) config.onAudioProcessStarted(); + if (config.onAudioProcessStarted) { + config.onAudioProcessStarted(); + } }; mediaRecorder.onGifPreview = function(gif) { - if (config.onGifPreview) config.onGifPreview(gif); + if (config.onGifPreview) { + config.onGifPreview(gif); + } }; mediaRecorder.record(); @@ -109,13 +125,16 @@ function RecordRTC(mediaStream, config) { } function stopRecording(callback) { - if (!mediaRecorder) return console.warn(WARNING); + if (!mediaRecorder) { + return console.warn(WARNING); + } + /*jshint validthis:true */ var recordRTC = this; - console.warn('stopped recording ' + config.type + ' stream.'); + console.warn('Stopped recording ' + config.type + ' stream.'); - if (config.type != 'gif') { + if (config.type !== 'gif') { mediaRecorder.stop(_callback); } else { mediaRecorder.stop(); @@ -124,7 +143,13 @@ function RecordRTC(mediaStream, config) { function _callback() { for (var item in mediaRecorder) { - self[item] = recordRTC[item] = mediaRecorder[item]; + if (self) { + self[item] = mediaRecorder[item]; + } + + if (recordRTC) { + recordRTC[item] = mediaRecorder[item]; + } } var blob = mediaRecorder.blob; @@ -135,7 +160,9 @@ function RecordRTC(mediaStream, config) { console.debug(blob.type, '->', bytesToSize(blob.size)); - if (!config.autoWriteToDisk) return; + if (!config.autoWriteToDisk) { + return; + } getDataURL(function(dataURL) { var parameter = {}; @@ -146,14 +173,16 @@ function RecordRTC(mediaStream, config) { } function getDataURL(callback, _mediaRecorder) { - if (!callback) throw 'Pass a callback function over getDataURL.'; + if (!callback) { + throw 'Pass a callback function over getDataURL.'; + } var blob = _mediaRecorder ? _mediaRecorder.blob : mediaRecorder.blob; if (!blob) { console.warn('Blob encoder did not yet finished its job.'); setTimeout(function() { - getDataURL(callback, _mediaRecorder) + getDataURL(callback, _mediaRecorder); }, 1000); return; } @@ -233,7 +262,10 @@ function RecordRTC(mediaStream, config) { * }); */ getBlob: function() { - if (!mediaRecorder) return console.warn(WARNING); + if (!mediaRecorder) { + return console.warn(WARNING); + } + return mediaRecorder.blob; }, @@ -263,7 +295,10 @@ function RecordRTC(mediaStream, config) { * }); */ toURL: function() { - if (!mediaRecorder) return console.warn(WARNING); + if (!mediaRecorder) { + return console.warn(WARNING); + } + return URL.createObjectURL(mediaRecorder.blob); }, @@ -313,7 +348,10 @@ function RecordRTC(mediaStream, config) { * }); */ getFromDisk: function(callback) { - if (!mediaRecorder) return console.warn(WARNING); + if (!mediaRecorder) { + return console.warn(WARNING); + } + RecordRTC.getFromDisk(config.type, callback); }, @@ -408,6 +446,10 @@ function RecordRTC(mediaStream, config) { view: null }; + if (!this) { + return returnObject; + } + // if someone wanna use RecordRTC with "new" keyword. for (var prop in returnObject) { this[prop] = returnObject[prop]; @@ -417,45 +459,47 @@ function RecordRTC(mediaStream, config) { } /** -* This method can be used to get all recorded blobs from IndexedDB storage. -* @param {string} type - 'all' or 'audio' or 'video' or 'gif' -* @param {function} callback - Callback function to get all stored blobs. -* @method -* @memberof RecordRTC -* @example -* RecordRTC.getFromDisk('all', function(dataURL, type){ -* if(type == 'audio') { } -* if(type == 'video') { } -* if(type == 'gif') { } -* }); -*/ + * This method can be used to get all recorded blobs from IndexedDB storage. + * @param {string} type - 'all' or 'audio' or 'video' or 'gif' + * @param {function} callback - Callback function to get all stored blobs. + * @method + * @memberof RecordRTC + * @example + * RecordRTC.getFromDisk('all', function(dataURL, type){ + * if(type === 'audio') { } + * if(type === 'video') { } + * if(type === 'gif') { } + * }); + */ RecordRTC.getFromDisk = function(type, callback) { - if (!callback) throw 'callback is mandatory.'; + if (!callback) { + throw 'callback is mandatory.'; + } - console.log('Getting recorded ' + (type == 'all' ? 'blobs' : type + ' blob ') + ' from disk!'); + console.log('Getting recorded ' + (type === 'all' ? 'blobs' : type + ' blob ') + ' from disk!'); DiskStorage.Fetch(function(dataURL, _type) { - if (type != 'all' && _type == type + 'Blob') { - if (callback) callback(dataURL); + if (type !== 'all' && _type === type + 'Blob' && callback) { + callback(dataURL); } - if (type == 'all') { - if (callback) callback(dataURL, _type.replace('Blob', '')); + if (type === 'all' && callback) { + callback(dataURL, _type.replace('Blob', '')); } }); }; /** -* This method can be used to store recorded blobs into IndexedDB storage. -* @param {object} options - {audio: Blob, video: Blob, gif: Blob} -* @method -* @memberof RecordRTC -* @example -* RecordRTC.writeToDisk({ -* audio: audioBlob, -* video: videoBlob, -* gif : gifBlob -* }); -*/ + * This method can be used to store recorded blobs into IndexedDB storage. + * @param {object} options - {audio: Blob, video: Blob, gif: Blob} + * @method + * @memberof RecordRTC + * @example + * RecordRTC.writeToDisk({ + * audio: audioBlob, + * video: videoBlob, + * gif : gifBlob + * }); + */ RecordRTC.writeToDisk = function(options) { console.log('Writing recorded blob(s) to disk!'); options = options || {}; @@ -518,85 +562,86 @@ RecordRTC.writeToDisk = function(options) { }); } }; - // _____________ // MRecordRTC.js /** -* MRecordRTC runs top over {@link RecordRTC} to bring multiple recordings in single place, by providing simple API. -* @summary MRecordRTC stands for "Multiple-RecordRTC". -* @license {@link https://www.webrtc-experiment.com/licence/|MIT} -* @author {@link https://www.MuazKhan.com|Muaz Khan} -* @typedef MRecordRTC -* @class -* @example -* var recorder = new MRecordRTC(); -* recorder.addStream(MediaStream); -* recorder.mediaType = { -* audio: true, -* video: true, -* gif: true -* }; -* recorder.startRecording(); -* @see For further information: -* @see {@link https://github.com/muaz-khan/RecordRTC/tree/master/MRecordRTC|MRecordRTC Source Code} -*/ + * MRecordRTC runs top over {@link RecordRTC} to bring multiple recordings in single place, by providing simple API. + * @summary MRecordRTC stands for "Multiple-RecordRTC". + * @license {@link https://www.webrtc-experiment.com/licence/|MIT} + * @author {@link https://www.MuazKhan.com|Muaz Khan} + * @typedef MRecordRTC + * @class + * @example + * var recorder = new MRecordRTC(); + * recorder.addStream(MediaStream); + * recorder.mediaType = { + * audio: true, + * video: true, + * gif: true + * }; + * recorder.startRecording(); + * @see For further information: + * @see {@link https://github.com/muaz-khan/RecordRTC/tree/master/MRecordRTC|MRecordRTC Source Code} + */ function MRecordRTC(mediaStream) { /** - * This method attaches MediaStream object to {@link MRecordRTC}. - * @param {MediaStream} mediaStream - A MediaStream object, either fetched using getUserMedia API, or generated using captureStreamUntilEnded or WebAudio API. - * @method - * @memberof MRecordRTC - * @example - * recorder.addStream(MediaStream); - */ + * This method attaches MediaStream object to {@link MRecordRTC}. + * @param {MediaStream} mediaStream - A MediaStream object, either fetched using getUserMedia API, or generated using captureStreamUntilEnded or WebAudio API. + * @method + * @memberof MRecordRTC + * @example + * recorder.addStream(MediaStream); + */ this.addStream = function(_mediaStream) { - if (_mediaStream) mediaStream = _mediaStream; + if (_mediaStream) { + mediaStream = _mediaStream; + } }; /** - * This property can be used to set recording type e.g. audio, or video, or gif, or canvas. - * @property {object} mediaType - {audio: true, video: true, gif: true} - * @memberof MRecordRTC - * @example - * var recorder = new MRecordRTC(); - * recorder.mediaType = { - * audio: true, - * video: true, - * gif : true - * }; - */ + * This property can be used to set recording type e.g. audio, or video, or gif, or canvas. + * @property {object} mediaType - {audio: true, video: true, gif: true} + * @memberof MRecordRTC + * @example + * var recorder = new MRecordRTC(); + * recorder.mediaType = { + * audio: true, + * video: true, + * gif : true + * }; + */ this.mediaType = { audio: true, video: true }; /** - * This method starts recording. - * @method - * @memberof MRecordRTC - * @example - * recorder.startRecording(); - */ + * This method starts recording. + * @method + * @memberof MRecordRTC + * @example + * recorder.startRecording(); + */ this.startRecording = function() { - if (!IsChrome && mediaStream && mediaStream.getAudioTracks && mediaStream.getAudioTracks().length && mediaStream.getVideoTracks().length) { + if (!isChrome && mediaStream && mediaStream.getAudioTracks && mediaStream.getAudioTracks().length && mediaStream.getVideoTracks().length) { // Firefox is supporting both audio/video in single blob this.mediaType.audio = false; } if (this.mediaType.audio) { - this.audioRecorder = RecordRTC(mediaStream, { + this.audioRecorder = new RecordRTC(mediaStream, { type: 'audio', bufferSize: this.bufferSize, sampleRate: this.sampleRate - }); + }); this.audioRecorder.startRecording(); } if (this.mediaType.video) { - this.videoRecorder = RecordRTC(mediaStream, { + this.videoRecorder = new RecordRTC(mediaStream, { type: 'video', video: this.video, canvas: this.canvas @@ -605,7 +650,7 @@ function MRecordRTC(mediaStream) { } if (this.mediaType.gif) { - this.gifRecorder = RecordRTC(mediaStream, { + this.gifRecorder = new RecordRTC(mediaStream, { type: 'gif', frameRate: this.frameRate || 200, quality: this.quality || 10 @@ -615,17 +660,17 @@ function MRecordRTC(mediaStream) { }; /** - * This method stop recording. - * @param {function} callback - Callback function is invoked when all encoders finish their jobs. - * @method - * @memberof MRecordRTC - * @example - * recorder.stopRecording(function(recording){ - * var audioBlob = recording.audio; - * var videoBlob = recording.video; - * var gifBlob = recording.gif; - * }); - */ + * This method stop recording. + * @param {function} callback - Callback function is invoked when all encoders finish their jobs. + * @method + * @memberof MRecordRTC + * @example + * recorder.stopRecording(function(recording){ + * var audioBlob = recording.audio; + * var videoBlob = recording.video; + * var gifBlob = recording.gif; + * }); + */ this.stopRecording = function(callback) { callback = callback || function() {}; @@ -649,17 +694,17 @@ function MRecordRTC(mediaStream) { }; /** - * This method can be used to manually get all recorded blobs. - * @param {function} callback - All recorded blobs are passed back to "callback" function. - * @method - * @memberof MRecordRTC - * @example - * recorder.getBlob(function(recording){ - * var audioBlob = recording.audio; - * var videoBlob = recording.video; - * var gifBlob = recording.gif; - * }); - */ + * This method can be used to manually get all recorded blobs. + * @param {function} callback - All recorded blobs are passed back to "callback" function. + * @method + * @memberof MRecordRTC + * @example + * recorder.getBlob(function(recording){ + * var audioBlob = recording.audio; + * var videoBlob = recording.video; + * var gifBlob = recording.gif; + * }); + */ this.getBlob = function(callback) { var output = {}; @@ -674,21 +719,24 @@ function MRecordRTC(mediaStream) { if (this.gifRecorder) { output.gif = this.gifRecorder.getBlob(); } - if (callback) callback(output); + + if (callback) { + callback(output); + } }; /** - * This method can be used to manually get all recorded blobs' DataURLs. - * @param {function} callback - All recorded blobs' DataURLs are passed back to "callback" function. - * @method - * @memberof MRecordRTC - * @example - * recorder.getDataURL(function(recording){ - * var audioDataURL = recording.audio; - * var videoDataURL = recording.video; - * var gifDataURL = recording.gif; - * }); - */ + * This method can be used to manually get all recorded blobs' DataURLs. + * @param {function} callback - All recorded blobs' DataURLs are passed back to "callback" function. + * @method + * @memberof MRecordRTC + * @example + * recorder.getDataURL(function(recording){ + * var audioDataURL = recording.audio; + * var videoDataURL = recording.video; + * var gifDataURL = recording.gif; + * }); + */ this.getDataURL = function(callback) { this.getBlob(function(blob) { getDataURL(blob.audio, function(_audioDataURL) { @@ -735,12 +783,12 @@ function MRecordRTC(mediaStream) { }; /** - * This method can be used to ask {@link MRecordRTC} to write all recorded blobs into IndexedDB storage. - * @method - * @memberof MRecordRTC - * @example - * recorder.writeToDisk(); - */ + * This method can be used to ask {@link MRecordRTC} to write all recorded blobs into IndexedDB storage. + * @method + * @memberof MRecordRTC + * @example + * recorder.writeToDisk(); + */ this.writeToDisk = function() { RecordRTC.writeToDisk({ audio: this.audioRecorder, @@ -750,17 +798,17 @@ function MRecordRTC(mediaStream) { }; /** - * This method can be used to invoke save-as dialog for all recorded blobs. - * @param {object} args - {audio: 'audio-name', video: 'video-name', gif: 'gif-name'} - * @method - * @memberof MRecordRTC - * @example - * recorder.save({ - * audio: 'audio-file-name', - * video: 'video-file-name', - * gif : 'gif-file-name' - * }); - */ + * This method can be used to invoke save-as dialog for all recorded blobs. + * @param {object} args - {audio: 'audio-name', video: 'video-name', gif: 'gif-name'} + * @method + * @memberof MRecordRTC + * @example + * recorder.save({ + * audio: 'audio-file-name', + * video: 'video-file-name', + * gif : 'gif-file-name' + * }); + */ this.save = function(args) { args = args || { audio: true, @@ -769,57 +817,56 @@ function MRecordRTC(mediaStream) { }; if (!!args.audio && this.audioRecorder) { - this.audioRecorder.save(typeof args.audio == 'string' ? args.audio : ''); + this.audioRecorder.save(typeof args.audio === 'string' ? args.audio : ''); } if (!!args.video && this.videoRecorder) { - this.videoRecorder.save(typeof args.video == 'string' ? args.video : ''); + this.videoRecorder.save(typeof args.video === 'string' ? args.video : ''); } if (!!args.gif && this.gifRecorder) { - this.gifRecorder.save(typeof args.gif == 'string' ? args.gif : ''); + this.gifRecorder.save(typeof args.gif === 'string' ? args.gif : ''); } }; } /** -* This method can be used to get all recorded blobs from IndexedDB storage. -* @param {string} type - 'all' or 'audio' or 'video' or 'gif' -* @param {function} callback - Callback function to get all stored blobs. -* @method -* @memberof MRecordRTC -* @example -* MRecordRTC.getFromDisk('all', function(dataURL, type){ -* if(type == 'audio') { } -* if(type == 'video') { } -* if(type == 'gif') { } -* }); -*/ + * This method can be used to get all recorded blobs from IndexedDB storage. + * @param {string} type - 'all' or 'audio' or 'video' or 'gif' + * @param {function} callback - Callback function to get all stored blobs. + * @method + * @memberof MRecordRTC + * @example + * MRecordRTC.getFromDisk('all', function(dataURL, type){ + * if(type === 'audio') { } + * if(type === 'video') { } + * if(type === 'gif') { } + * }); + */ MRecordRTC.getFromDisk = RecordRTC.getFromDisk; /** -* This method can be used to store recorded blobs into IndexedDB storage. -* @param {object} options - {audio: Blob, video: Blob, gif: Blob} -* @method -* @memberof MRecordRTC -* @example -* MRecordRTC.writeToDisk({ -* audio: audioBlob, -* video: videoBlob, -* gif : gifBlob -* }); -*/ + * This method can be used to store recorded blobs into IndexedDB storage. + * @param {object} options - {audio: Blob, video: Blob, gif: Blob} + * @method + * @memberof MRecordRTC + * @example + * MRecordRTC.writeToDisk({ + * audio: audioBlob, + * video: videoBlob, + * gif : gifBlob + * }); + */ MRecordRTC.writeToDisk = RecordRTC.writeToDisk; - // _____________________________ // Cross-Browser-Declarations.js // animation-frame used in WebM recording if (!window.requestAnimationFrame) { - requestAnimationFrame = window.webkitRequestAnimationFrame || window.mozRequestAnimationFrame; + window.requestAnimationFrame = window.webkitRequestAnimationFrame || window.mozRequestAnimationFrame; } if (!window.cancelAnimationFrame) { - cancelAnimationFrame = window.webkitCancelAnimationFrame || window.mozCancelAnimationFrame; + window.cancelAnimationFrame = window.webkitCancelAnimationFrame || window.mozCancelAnimationFrame; } // WebAudio API representer @@ -827,26 +874,28 @@ if (!window.AudioContext) { window.AudioContext = window.webkitAudioContext || window.mozAudioContext; } -URL = window.URL || window.webkitURL; +window.URL = window.URL || window.webkitURL; navigator.getUserMedia = navigator.webkitGetUserMedia || navigator.mozGetUserMedia; -if (window.webkitMediaStream) window.MediaStream = window.webkitMediaStream; +if (window.webkitMediaStream) { + window.MediaStream = window.webkitMediaStream; +} -IsChrome = !!navigator.webkitGetUserMedia; +var isChrome = !!navigator.webkitGetUserMedia; // Merge all other data-types except "function" /** -* @param {object} mergein - Merge another object in this object. -* @param {object} mergeto - Merge this object in another object. -* @returns {object} - merged object -* @example -* var mergedObject = mergeProps({}, { -* x: 10, // this will be merged -* y: 10, // this will be merged -* add: function() {} // this will be skipped -* }); -*/ + * @param {object} mergein - Merge another object in this object. + * @param {object} mergeto - Merge this object in another object. + * @returns {object} - merged object + * @example + * var mergedObject = mergeProps({}, { + * x: 10, // this will be merged + * y: 10, // this will be merged + * add: function() {} // this will be skipped + * }); + */ function mergeProps(mergein, mergeto) { mergeto = reformatProps(mergeto); for (var t in mergeto) { @@ -858,43 +907,64 @@ function mergeProps(mergein, mergeto) { } /** -* @param {object} obj - If a property name is "sample-rate"; it will be converted into "sampleRate". -* @returns {object} - formatted object. -* @example -* var mergedObject = reformatProps({ -* 'sample-rate': 44100, -* 'buffer-size': 4096 -* }); -* -* mergedObject.sampleRate == 44100 -* mergedObject.bufferSize == 4096 -*/ + * @param {object} obj - If a property name is "sample-rate"; it will be converted into "sampleRate". + * @returns {object} - formatted object. + * @example + * var mergedObject = reformatProps({ + * 'sample-rate': 44100, + * 'buffer-size': 4096 + * }); + * + * mergedObject.sampleRate === 44100 + * mergedObject.bufferSize === 4096 + */ function reformatProps(obj) { var output = {}; for (var o in obj) { - if (o.indexOf('-') != -1) { + if (o.indexOf('-') !== -1) { var splitted = o.split('-'); var name = splitted[0] + splitted[1].split('')[0].toUpperCase() + splitted[1].substr(1); output[name] = obj[o]; - } else output[o] = obj[o]; + } else { + output[o] = obj[o]; + } } return output; } -// __________ (used to handle stuff like http://goo.gl/xmE5eg) issue #129 -// Storage.js +if (location.href.indexOf('file:') === 0) { + console.error('Please load this HTML file on HTTP or HTTPS.'); +} + +// below function via: http://goo.gl/B3ae8c +/** + * @param {number} bytes - Pass bytes and get formafted string. + * @returns {string} - formafted string + * @example + * bytesToSize(1024*1024*5) === '5 GB' + */ +function bytesToSize(bytes) { + var k = 1000; + var sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB']; + if (bytes === 0) { + return '0 Bytes'; + } + var i = parseInt(Math.floor(Math.log(bytes) / Math.log(k)), 10); + return (bytes / Math.pow(k, i)).toPrecision(3) + ' ' + sizes[i]; + } + // __________ (used to handle stuff like http://goo.gl/xmE5eg) issue #129 + // Storage.js /** -* Storage is a standalone object used by {@link RecordRTC} to store reusable objects e.g. "new AudioContext". -* @example -* Storage.AudioContext == webkitAudioContext -* @property {webkitAudioContext} AudioContext - Keeps a reference to AudioContext object. -*/ + * Storage is a standalone object used by {@link RecordRTC} to store reusable objects e.g. "new AudioContext". + * @example + * Storage.AudioContext === webkitAudioContext + * @property {webkitAudioContext} AudioContext - Keeps a reference to AudioContext object. + */ var Storage = { AudioContext: window.AudioContext || window.webkitAudioContext }; - // ______________________ // MediaStreamRecorder.js @@ -912,937 +982,956 @@ var Storage = { * Also extract the encoded data and create blobs on every timeslice passed from start function or RequestData function called by UA. */ - /** -* MediaStreamRecorder is an abstraction layer for "MediaRecorder API". -* @summary Runs top over MediaRecorder API. -* @typedef MediaStreamRecorder -* @class -* @example -* var recorder = new MediaStreamRecorder(MediaStream); -* recorder.mimeType = 'video/webm'; // audio/ogg or video/webm -* recorder.record(); -* recorder.stop(function(blob) { -* video.src = URL.createObjectURL(blob); -* -* // or -* var blob = recorder.blob; -* }); -* @param {MediaStream} mediaStream - MediaStream object fetched using getUserMedia API or generated using captureStreamUntilEnded or WebAudio API. -*/ +/** + * MediaStreamRecorder is an abstraction layer for "MediaRecorder API". + * @summary Runs top over MediaRecorder API. + * @typedef MediaStreamRecorder + * @class + * @example + * var recorder = new MediaStreamRecorder(MediaStream); + * recorder.mimeType = 'video/webm'; // audio/ogg or video/webm + * recorder.record(); + * recorder.stop(function(blob) { + * video.src = URL.createObjectURL(blob); + * + * // or + * var blob = recorder.blob; + * }); + * @param {MediaStream} mediaStream - MediaStream object fetched using getUserMedia API or generated using captureStreamUntilEnded or WebAudio API. + */ function MediaStreamRecorder(mediaStream) { - var self = this; + var self = this; - // if user chosen only audio option; and he tried to pass MediaStream with - // both audio and video tracks; - // using a dirty workaround to generate audio-only stream so that we can get audio/ogg output. - if (self.mimeType && self.mimeType != 'video/webm' && mediaStream.getVideoTracks && mediaStream.getVideoTracks().length) { - var context = new AudioContext(); - var mediaStreamSource = context.createMediaStreamSource(mediaStream); + // if user chosen only audio option; and he tried to pass MediaStream with + // both audio and video tracks; + // using a dirty workaround to generate audio-only stream so that we can get audio/ogg output. + if (self.mimeType && self.mimeType !== 'video/webm' && mediaStream.getVideoTracks && mediaStream.getVideoTracks().length) { + var context = new AudioContext(); + var mediaStreamSource = context.createMediaStreamSource(mediaStream); - var destination = context.createMediaStreamDestination(); - mediaStreamSource.connect(destination); + var destination = context.createMediaStreamDestination(); + mediaStreamSource.connect(destination); - mediaStream = destination.stream; - } + mediaStream = destination.stream; + } - var dataAvailable = false; + var dataAvailable = false; - /** - * This method records MediaStream. - * @method - * @memberof MediaStreamRecorder - * @example - * recorder.record(); - */ - this.record = function() { - // http://dxr.mozilla.org/mozilla-central/source/content/media/MediaRecorder.cpp - // https://wiki.mozilla.org/Gecko:MediaRecorder - // https://dvcs.w3.org/hg/dap/raw-file/default/media-stream-capture/MediaRecorder.html + /** + * This method records MediaStream. + * @method + * @memberof MediaStreamRecorder + * @example + * recorder.record(); + */ + this.record = function() { + // http://dxr.mozilla.org/mozilla-central/source/content/media/MediaRecorder.cpp + // https://wiki.mozilla.org/Gecko:MediaRecorder + // https://dvcs.w3.org/hg/dap/raw-file/default/media-stream-capture/MediaRecorder.html + + // starting a recording session; which will initiate "Reading Thread" + // "Reading Thread" are used to prevent main-thread blocking scenarios + mediaRecorder = new window.MediaRecorder(mediaStream); + + // Dispatching OnDataAvailable Handler + mediaRecorder.ondataavailable = function(e) { + if (dataAvailable) { + return; + } - // starting a recording session; which will initiate "Reading Thread" - // "Reading Thread" are used to prevent main-thread blocking scenarios - mediaRecorder = new MediaRecorder(mediaStream); + if (!e.data.size) { + console.warn('Recording of', e.data.type, 'failed.'); + return; + } - // Dispatching OnDataAvailable Handler - mediaRecorder.ondataavailable = function(e) { - if (dataAvailable) return; + dataAvailable = true; + + /** + * @property {Blob} blob - Recorded frames in video/webm blob. + * @memberof MediaStreamRecorder + * @example + * recorder.stop(function() { + * var blob = recorder.blob; + * }); + */ + self.blob = new Blob([e.data], { + type: e.data.type || self.mimeType || 'audio/ogg' + }); - if (!e.data.size) { - console.warn('Recording of', e.data.type, 'failed.'); - return; - } + self.callback(); + }; - dataAvailable = true; + mediaRecorder.onerror = function(error) { + console.warn(error); + mediaRecorder.stop(); + self.record(0); + }; - /** - * @property {Blob} blob - Recorded frames in video/webm blob. - * @memberof MediaStreamRecorder - * @example - * recorder.stop(function() { - * var blob = recorder.blob; - * }); - */ - self.blob = new Blob([e.data], { - type: e.data.type || self.mimeType || 'audio/ogg' - }); + // void start(optional long mTimeSlice) + // The interval of passing encoded data from EncodedBufferCache to onDataAvailable + // handler. "mTimeSlice < 0" means Session object does not push encoded data to + // onDataAvailable, instead, it passive wait the client side pull encoded data + // by calling requestData API. + mediaRecorder.start(0); - self.callback(); - }; + // Start recording. If timeSlice has been provided, mediaRecorder will + // raise a dataavailable event containing the Blob of collected data on every timeSlice milliseconds. + // If timeSlice isn't provided, UA should call the RequestData to obtain the Blob data, also set the mTimeSlice to zero. - mediaRecorder.onerror = function(error) { - console.warn(error); - mediaRecorder.stop(); - self.record(0); + if (self.onAudioProcessStarted) { + self.onAudioProcessStarted(); + } }; - // void start(optional long mTimeSlice) - // The interval of passing encoded data from EncodedBufferCache to onDataAvailable - // handler. "mTimeSlice < 0" means Session object does not push encoded data to - // onDataAvailable, instead, it passive wait the client side pull encoded data - // by calling requestData API. - mediaRecorder.start(0); - - // Start recording. If timeSlice has been provided, mediaRecorder will - // raise a dataavailable event containing the Blob of collected data on every timeSlice milliseconds. - // If timeSlice isn't provided, UA should call the RequestData to obtain the Blob data, also set the mTimeSlice to zero. - - if (self.onAudioProcessStarted) self.onAudioProcessStarted(); - }; - - /** - * This method stops recording MediaStream. - * @param {function} callback - Callback function, that is used to pass recorded blob back to the callee. - * @method - * @memberof MediaStreamRecorder - * @example - * recorder.stop(function(blob) { - * video.src = URL.createObjectURL(blob); - * }); - */ - this.stop = function(callback) { - this.callback = callback; - // mediaRecorder.state == 'recording' means that media recorder is associated with "session" - // mediaRecorder.state == 'stopped' means that media recorder is detached from the "session" ... in this case; "session" will also be deleted. - - if (mediaRecorder.state == 'recording') { - // "stop" method auto invokes "requestData"! - // mediaRecorder.requestData(); - mediaRecorder.stop(); - } - }; - - // Reference to "MediaRecorder" object - var mediaRecorder; -} + /** + * This method stops recording MediaStream. + * @param {function} callback - Callback function, that is used to pass recorded blob back to the callee. + * @method + * @memberof MediaStreamRecorder + * @example + * recorder.stop(function(blob) { + * video.src = URL.createObjectURL(blob); + * }); + */ + this.stop = function(callback) { + this.callback = callback; + // mediaRecorder.state === 'recording' means that media recorder is associated with "session" + // mediaRecorder.state === 'stopped' means that media recorder is detached from the "session" ... in this case; "session" will also be deleted. + + if (mediaRecorder.state === 'recording') { + // "stop" method auto invokes "requestData"! + // mediaRecorder.requestData(); + mediaRecorder.stop(); + } + }; -// _________________ -// StereoRecorder.js + // Reference to "MediaRecorder" object + var mediaRecorder; + } + // _________________ + // StereoRecorder.js /** -* StereoRecorder is a standalone class used by RecordRTC to bring audio-recording in chrome. It runs top over {@link StereoAudioRecorder}. -* @summary JavaScript standalone object for stereo audio recording. -* @typedef StereoRecorder -* @class -* @example -* var recorder = new StereoRecorder(MediaStream); -* recorder.record(); -* recorder.stop(function(blob) { -* video.src = URL.createObjectURL(blob); -* }); -* @param {MediaStream} mediaStream - MediaStream object fetched using getUserMedia API or generated using captureStreamUntilEnded or WebAudio API. -*/ + * StereoRecorder is a standalone class used by RecordRTC to bring audio-recording in chrome. It runs top over {@link StereoAudioRecorder}. + * @summary JavaScript standalone object for stereo audio recording. + * @typedef StereoRecorder + * @class + * @example + * var recorder = new StereoRecorder(MediaStream); + * recorder.record(); + * recorder.stop(function(blob) { + * video.src = URL.createObjectURL(blob); + * }); + * @param {MediaStream} mediaStream - MediaStream object fetched using getUserMedia API or generated using captureStreamUntilEnded or WebAudio API. + */ function StereoRecorder(mediaStream) { - var self = this; + var self = this; - /** - * This method records MediaStream. - * @method - * @memberof StereoRecorder - * @example - * recorder.record(); - */ - this.record = function() { - mediaRecorder = new StereoAudioRecorder(mediaStream, this); - mediaRecorder.onAudioProcessStarted = function() { - if (self.onAudioProcessStarted) self.onAudioProcessStarted(); + /** + * This method records MediaStream. + * @method + * @memberof StereoRecorder + * @example + * recorder.record(); + */ + this.record = function() { + mediaRecorder = new StereoAudioRecorder(mediaStream, this); + mediaRecorder.onAudioProcessStarted = function() { + if (self.onAudioProcessStarted) { + self.onAudioProcessStarted(); + } + }; + mediaRecorder.record(); }; - mediaRecorder.record(); - }; - /** - * This method stops recording MediaStream. - * @param {function} callback - Callback function, that is used to pass recorded blob back to the callee. - * @method - * @memberof StereoRecorder - * @example - * recorder.stop(function(blob) { - * video.src = URL.createObjectURL(blob); - * }); - */ - this.stop = function(callback) { - if (!mediaRecorder) return; - mediaRecorder.stop(function() { - for (var item in mediaRecorder) { - self[item] = mediaRecorder[item]; + /** + * This method stops recording MediaStream. + * @param {function} callback - Callback function, that is used to pass recorded blob back to the callee. + * @method + * @memberof StereoRecorder + * @example + * recorder.stop(function(blob) { + * video.src = URL.createObjectURL(blob); + * }); + */ + this.stop = function(callback) { + if (!mediaRecorder) { + return; } - callback(); - }); - }; - // Reference to "StereoAudioRecorder" object - var mediaRecorder; -} + mediaRecorder.stop(function() { + for (var item in mediaRecorder) { + self[item] = mediaRecorder[item]; + } + callback(); + }); + }; -// source code from: http://typedarray.org/wp-content/projects/WebAudioRecorder/script.js -// https://github.com/mattdiamond/Recorderjs#license-mit -// ______________________ -// StereoAudioRecorder.js + // Reference to "StereoAudioRecorder" object + var mediaRecorder; + } + // source code from: http://typedarray.org/wp-content/projects/WebAudioRecorder/script.js + // https://github.com/mattdiamond/Recorderjs#license-mit + // ______________________ + // StereoAudioRecorder.js /** -* StereoAudioRecorder is a standalone class used by RecordRTC to bring "stereo" audio-recording in chrome. -* @summary JavaScript standalone object for stereo audio recording. -* @typedef StereoAudioRecorder -* @class -* @example -* var recorder = new StereoAudioRecorder(MediaStream, { -* sampleRate: 44100, -* bufferSize: 4096 -* }); -* recorder.record(); -* recorder.stop(function(blob) { -* video.src = URL.createObjectURL(blob); -* }); -* @param {MediaStream} mediaStream - MediaStream object fetched using getUserMedia API or generated using captureStreamUntilEnded or WebAudio API. -* @param {object} config - {sampleRate: 44100, bufferSize: 4096} -*/ + * StereoAudioRecorder is a standalone class used by RecordRTC to bring "stereo" audio-recording in chrome. + * @summary JavaScript standalone object for stereo audio recording. + * @typedef StereoAudioRecorder + * @class + * @example + * var recorder = new StereoAudioRecorder(MediaStream, { + * sampleRate: 44100, + * bufferSize: 4096 + * }); + * recorder.record(); + * recorder.stop(function(blob) { + * video.src = URL.createObjectURL(blob); + * }); + * @param {MediaStream} mediaStream - MediaStream object fetched using getUserMedia API or generated using captureStreamUntilEnded or WebAudio API. + * @param {object} config - {sampleRate: 44100, bufferSize: 4096} + */ var __stereoAudioRecorderJavacriptNode; function StereoAudioRecorder(mediaStream, config) { - if (!mediaStream.getAudioTracks().length) throw 'Your stream has no audio tracks.'; + if (!mediaStream.getAudioTracks().length) { + throw 'Your stream has no audio tracks.'; + } - // variables - var leftchannel = []; - var rightchannel = []; - var recording = false; - var recordingLength = 0; + // variables + var leftchannel = []; + var rightchannel = []; + var recording = false; + var recordingLength = 0; - /** - * This method records MediaStream. - * @method - * @memberof StereoAudioRecorder - * @example - * recorder.record(); - */ - this.record = function() { - // reset the buffers for the new recording - leftchannel.length = rightchannel.length = 0; - recordingLength = 0; + /** + * This method records MediaStream. + * @method + * @memberof StereoAudioRecorder + * @example + * recorder.record(); + */ + this.record = function() { + // reset the buffers for the new recording + leftchannel.length = rightchannel.length = 0; + recordingLength = 0; - recording = true; - }; + recording = true; + }; - /** - * This method stops recording MediaStream. - * @param {function} callback - Callback function, that is used to pass recorded blob back to the callee. - * @method - * @memberof StereoAudioRecorder - * @example - * recorder.stop(function(blob) { - * video.src = URL.createObjectURL(blob); - * }); - */ - this.stop = function(callback) { - // stop recording - recording = false; + /** + * This method stops recording MediaStream. + * @param {function} callback - Callback function, that is used to pass recorded blob back to the callee. + * @method + * @memberof StereoAudioRecorder + * @example + * recorder.stop(function(blob) { + * video.src = URL.createObjectURL(blob); + * }); + */ + this.stop = function(callback) { + // stop recording + recording = false; - // to make sure onaudioprocess stops firing - audioInput.disconnect(); - volume.disconnect(); + // to make sure onaudioprocess stops firing + audioInput.disconnect(); + volume.disconnect(); - // flat the left and right channels down - var leftBuffer = mergeBuffers(leftchannel, recordingLength); - var rightBuffer = mergeBuffers(rightchannel, recordingLength); + // flat the left and right channels down + var leftBuffer = mergeBuffers(leftchannel, recordingLength); + var rightBuffer = mergeBuffers(rightchannel, recordingLength); - // interleave both channels together - var interleaved = interleave(leftBuffer, rightBuffer); + // interleave both channels together + var interleaved = interleave(leftBuffer, rightBuffer); - // create our wav file - var buffer = new ArrayBuffer(44 + interleaved.length * 2); + // create our wav file + var buffer = new ArrayBuffer(44 + interleaved.length * 2); - var view = new DataView(buffer); + var view = new DataView(buffer); - // RIFF chunk descriptor/identifier - writeUTFBytes(view, 0, 'RIFF'); + // RIFF chunk descriptor/identifier + writeUTFBytes(view, 0, 'RIFF'); - // RIFF chunk length - // view.setUint32(4, 44 + interleaved.length * 2, true); - view.setUint32(4, 36 + interleaved.length * 2, true); + // RIFF chunk length + // view.setUint32(4, 44 + interleaved.length * 2, true); + view.setUint32(4, 36 + interleaved.length * 2, true); - // RIFF type - writeUTFBytes(view, 8, 'WAVE'); + // RIFF type + writeUTFBytes(view, 8, 'WAVE'); - // format chunk identifier - // FMT sub-chunk - writeUTFBytes(view, 12, 'fmt '); + // format chunk identifier + // FMT sub-chunk + writeUTFBytes(view, 12, 'fmt '); - // format chunk length - view.setUint32(16, 16, true); + // format chunk length + view.setUint32(16, 16, true); - // sample format (raw) - view.setUint16(20, 1, true); + // sample format (raw) + view.setUint16(20, 1, true); - // stereo (2 channels) - view.setUint16(22, 2, true); + // stereo (2 channels) + view.setUint16(22, 2, true); - // sample rate - view.setUint32(24, sampleRate, true); + // sample rate + view.setUint32(24, sampleRate, true); - // byte rate (sample rate * block align) - view.setUint32(28, sampleRate * 4, true); + // byte rate (sample rate * block align) + view.setUint32(28, sampleRate * 4, true); - // block align (channel count * bytes per sample) - view.setUint16(32, 4, true); + // block align (channel count * bytes per sample) + view.setUint16(32, 4, true); - // bits per sample - view.setUint16(34, 16, true); + // bits per sample + view.setUint16(34, 16, true); - // data sub-chunk - // data chunk identifier - writeUTFBytes(view, 36, 'data'); + // data sub-chunk + // data chunk identifier + writeUTFBytes(view, 36, 'data'); - // data chunk length - view.setUint32(40, interleaved.length * 2, true); + // data chunk length + view.setUint32(40, interleaved.length * 2, true); - // write the PCM samples - var offset = 44; - for (var i = 0; i < interleaved.length; i++, offset += 2) { - var s = Math.max(-1, Math.min(1, interleaved[i])); - view.setInt16(offset, s < 0 ? s * 0x8000 : s * 0x7FFF, true); - } + // write the PCM samples + var offset = 44; + for (var i = 0; i < interleaved.length; i++, offset += 2) { + var s = Math.max(-1, Math.min(1, interleaved[i])); + view.setInt16(offset, s < 0 ? s * 0x8000 : s * 0x7FFF, true); + } - /** - * @property {Blob} blob - The recorded blob object. - * @memberof StereoAudioRecorder - * @example - * recorder.stop(function(){ - * var blob = recorder.blob; - * }); - */ - this.blob = new Blob([view], { - type: 'audio/wav' - }); + /** + * @property {Blob} blob - The recorded blob object. + * @memberof StereoAudioRecorder + * @example + * recorder.stop(function(){ + * var blob = recorder.blob; + * }); + */ + this.blob = new Blob([view], { + type: 'audio/wav' + }); - /** - * @property {ArrayBuffer} buffer - The recorded buffer object. - * @memberof StereoAudioRecorder - * @example - * recorder.stop(function(){ - * var buffer = recorder.buffer; - * }); - */ - this.buffer = new ArrayBuffer(view); + /** + * @property {ArrayBuffer} buffer - The recorded buffer object. + * @memberof StereoAudioRecorder + * @example + * recorder.stop(function(){ + * var buffer = recorder.buffer; + * }); + */ + this.buffer = new ArrayBuffer(view); - /** - * @property {DataView} view - The recorded data-view object. - * @memberof StereoAudioRecorder - * @example - * recorder.stop(function(){ - * var view = recorder.view; - * }); - */ - this.view = view; + /** + * @property {DataView} view - The recorded data-view object. + * @memberof StereoAudioRecorder + * @example + * recorder.stop(function(){ + * var view = recorder.view; + * }); + */ + this.view = view; - this.sampleRate = sampleRate; - this.bufferSize = bufferSize; + this.sampleRate = sampleRate; + this.bufferSize = bufferSize; - // recorded audio length - this.length = recordingLength; + // recorded audio length + this.length = recordingLength; - callback(); + callback(); - isAudioProcessStarted = false; - }; + isAudioProcessStarted = false; + }; - function interleave(leftChannel, rightChannel) { - var length = leftChannel.length + rightChannel.length; - var result = new Float32Array(length); + function interleave(leftChannel, rightChannel) { + var length = leftChannel.length + rightChannel.length; + var result = new Float32Array(length); - var inputIndex = 0; + var inputIndex = 0; - for (var index = 0; index < length;) { - result[index++] = leftChannel[inputIndex]; - result[index++] = rightChannel[inputIndex]; - inputIndex++; + for (var index = 0; index < length;) { + result[index++] = leftChannel[inputIndex]; + result[index++] = rightChannel[inputIndex]; + inputIndex++; + } + return result; } - return result; - } - function mergeBuffers(channelBuffer, rLength) { - var result = new Float32Array(rLength); - var offset = 0; - var lng = channelBuffer.length; - for (var i = 0; i < lng; i++) { - var buffer = channelBuffer[i]; - result.set(buffer, offset); - offset += buffer.length; + function mergeBuffers(channelBuffer, rLength) { + var result = new Float32Array(rLength); + var offset = 0; + var lng = channelBuffer.length; + for (var i = 0; i < lng; i++) { + var buffer = channelBuffer[i]; + result.set(buffer, offset); + offset += buffer.length; + } + return result; } - return result; - } - function writeUTFBytes(view, offset, string) { - var lng = string.length; - for (var i = 0; i < lng; i++) { - view.setUint8(offset + i, string.charCodeAt(i)); + function writeUTFBytes(view, offset, string) { + var lng = string.length; + for (var i = 0; i < lng; i++) { + view.setUint8(offset + i, string.charCodeAt(i)); + } } - } - // creates the audio context - var audioContext = Storage.AudioContext; + if (!Storage.AudioContextConstructor) { + Storage.AudioContextConstructor = new Storage.AudioContext(); + } - if (!Storage.AudioContextConstructor) - Storage.AudioContextConstructor = new audioContext(); + var context = Storage.AudioContextConstructor; - var context = Storage.AudioContextConstructor; + // creates a gain node + var volume = context.createGain(); - // creates a gain node - var volume = context.createGain(); + // creates an audio node from the microphone incoming stream + var audioInput = context.createMediaStreamSource(mediaStream); - // creates an audio node from the microphone incoming stream - var audioInput = context.createMediaStreamSource(mediaStream); + // connect the stream to the gain node + audioInput.connect(volume); - // connect the stream to the gain node - audioInput.connect(volume); + var legalBufferValues = [256, 512, 1024, 2048, 4096, 8192, 16384]; - var legalBufferValues = [256, 512, 1024, 2048, 4096, 8192, 16384]; - - /** - * From the spec: This value controls how frequently the audioprocess event is - * dispatched and how many sample-frames need to be processed each call. - * Lower values for buffer size will result in a lower (better) latency. - * Higher values will be necessary to avoid audio breakup and glitches - * The size of the buffer (in sample-frames) which needs to - * be processed each time onprocessaudio is called. - * Legal values are (256, 512, 1024, 2048, 4096, 8192, 16384). - * @property {number} bufferSize - Buffer-size for how frequently the audioprocess event is dispatched. - * @memberof StereoAudioRecorder - * @example - * recorder = new StereoAudioRecorder(mediaStream, { - * bufferSize: 4096 - * }); - */ - - // "0" means, let chrome decide the most accurate buffer-size for current platform. - var bufferSize = config.bufferSize || 4096; - - if (legalBufferValues.indexOf(bufferSize) == -1) { - // throw 'Legal values for buffer-size are ' + JSON.stringify(legalBufferValues, null, '\t'); - } + /** + * From the spec: This value controls how frequently the audioprocess event is + * dispatched and how many sample-frames need to be processed each call. + * Lower values for buffer size will result in a lower (better) latency. + * Higher values will be necessary to avoid audio breakup and glitches + * The size of the buffer (in sample-frames) which needs to + * be processed each time onprocessaudio is called. + * Legal values are (256, 512, 1024, 2048, 4096, 8192, 16384). + * @property {number} bufferSize - Buffer-size for how frequently the audioprocess event is dispatched. + * @memberof StereoAudioRecorder + * @example + * recorder = new StereoAudioRecorder(mediaStream, { + * bufferSize: 4096 + * }); + */ - - /** - * The sample rate (in sample-frames per second) at which the - * AudioContext handles audio. It is assumed that all AudioNodes - * in the context run at this rate. In making this assumption, - * sample-rate converters or "varispeed" processors are not supported - * in real-time processing. - * The sampleRate parameter describes the sample-rate of the - * linear PCM audio data in the buffer in sample-frames per second. - * An implementation must support sample-rates in at least - * the range 22050 to 96000. - * @property {number} sampleRate - Buffer-size for how frequently the audioprocess event is dispatched. - * @memberof StereoAudioRecorder - * @example - * recorder = new StereoAudioRecorder(mediaStream, { - * sampleRate: 44100 - * }); - */ - var sampleRate = config.sampleRate || context.sampleRate || 44100; - - if (sampleRate < 22050 || sampleRate > 96000) { - // Ref: http://stackoverflow.com/a/26303918/552182 - // throw 'sample-rate must be under range 22050 and 96000.'; - } + // "0" means, let chrome decide the most accurate buffer-size for current platform. + var bufferSize = config.bufferSize || 4096; + + if (legalBufferValues.indexOf(bufferSize) === -1) { + // throw 'Legal values for buffer-size are ' + JSON.stringify(legalBufferValues, null, '\t'); + } - if (context.createJavaScriptNode) { - __stereoAudioRecorderJavacriptNode = context.createJavaScriptNode(bufferSize, 2, 2); - } else if (context.createScriptProcessor) { - __stereoAudioRecorderJavacriptNode = context.createScriptProcessor(bufferSize, 2, 2); - } else { - throw 'WebAudio API has no support on this browser.'; - } - bufferSize = __stereoAudioRecorderJavacriptNode.bufferSize; + /** + * The sample rate (in sample-frames per second) at which the + * AudioContext handles audio. It is assumed that all AudioNodes + * in the context run at this rate. In making this assumption, + * sample-rate converters or "varispeed" processors are not supported + * in real-time processing. + * The sampleRate parameter describes the sample-rate of the + * linear PCM audio data in the buffer in sample-frames per second. + * An implementation must support sample-rates in at least + * the range 22050 to 96000. + * @property {number} sampleRate - Buffer-size for how frequently the audioprocess event is dispatched. + * @memberof StereoAudioRecorder + * @example + * recorder = new StereoAudioRecorder(mediaStream, { + * sampleRate: 44100 + * }); + */ + var sampleRate = config.sampleRate || context.sampleRate || 44100; - console.log('sample-rate', sampleRate); - console.log('buffer-size', bufferSize); + if (sampleRate < 22050 || sampleRate > 96000) { + // Ref: http://stackoverflow.com/a/26303918/552182 + // throw 'sample-rate must be under range 22050 and 96000.'; + } - var isAudioProcessStarted = false, - self = this; - __stereoAudioRecorderJavacriptNode.onaudioprocess = function(e) { - // if MediaStream().stop() or MediaStreamTrack.stop() is invoked. - if (mediaStream.ended) { - __stereoAudioRecorderJavacriptNode.onaudioprocess = function() {}; - return; + if (context.createJavaScriptNode) { + __stereoAudioRecorderJavacriptNode = context.createJavaScriptNode(bufferSize, 2, 2); + } else if (context.createScriptProcessor) { + __stereoAudioRecorderJavacriptNode = context.createScriptProcessor(bufferSize, 2, 2); + } else { + throw 'WebAudio API has no support on this browser.'; } - if (!recording) return; + bufferSize = __stereoAudioRecorderJavacriptNode.bufferSize; - /** - * This method is called on "onaudioprocess" event's first invocation. - * @method {function} onAudioProcessStarted - * @memberof StereoAudioRecorder - * @example - * recorder.onAudioProcessStarted: function() { }; - */ - if (!isAudioProcessStarted) { - isAudioProcessStarted = true; - if (self.onAudioProcessStarted) { - self.onAudioProcessStarted(); + console.log('sample-rate', sampleRate); + console.log('buffer-size', bufferSize); + + var isAudioProcessStarted = false, + self = this; + __stereoAudioRecorderJavacriptNode.onaudioprocess = function(e) { + // if MediaStream().stop() or MediaStreamTrack.stop() is invoked. + if (mediaStream.ended) { + __stereoAudioRecorderJavacriptNode.onaudioprocess = function() {}; + return; } - } - var left = e.inputBuffer.getChannelData(0); - var right = e.inputBuffer.getChannelData(1); + if (!recording) { + return; + } - // we clone the samples - leftchannel.push(new Float32Array(left)); - rightchannel.push(new Float32Array(right)); + /** + * This method is called on "onaudioprocess" event's first invocation. + * @method {function} onAudioProcessStarted + * @memberof StereoAudioRecorder + * @example + * recorder.onAudioProcessStarted: function() { }; + */ + if (!isAudioProcessStarted) { + isAudioProcessStarted = true; + if (self.onAudioProcessStarted) { + self.onAudioProcessStarted(); + } + } - recordingLength += bufferSize; - }; + var left = e.inputBuffer.getChannelData(0); + var right = e.inputBuffer.getChannelData(1); - // we connect the recorder - volume.connect(__stereoAudioRecorderJavacriptNode); + // we clone the samples + leftchannel.push(new Float32Array(left)); + rightchannel.push(new Float32Array(right)); - // to prevent self audio to be connected with speakers - __stereoAudioRecorderJavacriptNode.connect(context.destination); -} + recordingLength += bufferSize; + }; + + // we connect the recorder + volume.connect(__stereoAudioRecorderJavacriptNode); -// _________________ -// CanvasRecorder.js + // to prevent self audio to be connected with speakers + __stereoAudioRecorderJavacriptNode.connect(context.destination); + } + // _________________ + // CanvasRecorder.js /** -* CanvasRecorder is a standalone class used by RecordRTC to bring HTML5-Canvas recording into video WebM. It uses HTML2Canvas library and runs top over {@link Whammy}. -* @summary HTML2Canvas recording into video WebM. -* @typedef CanvasRecorder -* @class -* @example -* var recorder = new CanvasRecorder(htmlElement); -* recorder.record(); -* recorder.stop(function(blob) { -* video.src = URL.createObjectURL(blob); -* }); -* @param {HTMLElement} htmlElement - querySelector/getElementById/getElementsByTagName[0]/etc. -*/ + * CanvasRecorder is a standalone class used by RecordRTC to bring HTML5-Canvas recording into video WebM. It uses HTML2Canvas library and runs top over {@link Whammy}. + * @summary HTML2Canvas recording into video WebM. + * @typedef CanvasRecorder + * @class + * @example + * var recorder = new CanvasRecorder(htmlElement); + * recorder.record(); + * recorder.stop(function(blob) { + * video.src = URL.createObjectURL(blob); + * }); + * @param {HTMLElement} htmlElement - querySelector/getElementById/getElementsByTagName[0]/etc. + */ function CanvasRecorder(htmlElement) { - if (!window.html2canvas) throw 'Please link: //cdn.webrtc-experiment.com/screenshot.js'; - - var isRecording; + if (!window.html2canvas) { + throw 'Please link: //cdn.webrtc-experiment.com/screenshot.js'; + } - /** - * This method records Canvas. - * @method - * @memberof CanvasRecorder - * @example - * recorder.record(); - */ - this.record = function() { - isRecording = true; - drawCanvasFrame(); - }; + var isRecording; - /** - * This method stops recording Canvas. - * @param {function} callback - Callback function, that is used to pass recorded blob back to the callee. - * @method - * @memberof CanvasRecorder - * @example - * recorder.stop(function(blob) { - * video.src = URL.createObjectURL(blob); - * }); - */ - this.stop = function(callback) { - isRecording = false; - whammy.frames = frames; + /** + * This method records Canvas. + * @method + * @memberof CanvasRecorder + * @example + * recorder.record(); + */ + this.record = function() { + isRecording = true; + whammy.frames = []; + drawCanvasFrame(); + }; /** - * @property {Blob} blob - Recorded frames in video/webm blob. - * @memberof CanvasRecorder - * @example - * recorder.stop(function() { - * var blob = recorder.blob; - * }); - */ - this.blob = whammy.compile(); - - frames = []; - if (callback) callback(this.blob); - }; + * This method stops recording Canvas. + * @param {function} callback - Callback function, that is used to pass recorded blob back to the callee. + * @method + * @memberof CanvasRecorder + * @example + * recorder.stop(function(blob) { + * video.src = URL.createObjectURL(blob); + * }); + */ + this.stop = function(callback) { + isRecording = false; - var frames = []; + /** + * @property {Blob} blob - Recorded frames in video/webm blob. + * @memberof CanvasRecorder + * @example + * recorder.stop(function() { + * var blob = recorder.blob; + * }); + */ + this.blob = whammy.compile(); - function drawCanvasFrame() { - html2canvas(htmlElement, { - onrendered: function(canvas) { - var duration = new Date().getTime() - lastTime; - if (!duration) return drawCanvasFrame(); + if (callback) { + callback(this.blob); + } + }; - // via #206, by Jack i.e. @Seymourr - lastTime = new Date().getTime(); + function drawCanvasFrame() { + window.html2canvas(htmlElement, { + onrendered: function(canvas) { + var duration = new Date().getTime() - lastTime; + if (!duration) { + return drawCanvasFrame(); + } - frames.push({ - duration: duration, - image: canvas.toDataURL('image/webp') - }); + // via #206, by Jack i.e. @Seymourr + lastTime = new Date().getTime(); - if (isRecording) requestAnimationFrame(drawCanvasFrame); - } - }); - } + whammy.frames.push({ + duration: duration, + image: canvas.toDataURL('image/webp') + }); - var lastTime = new Date().getTime(); + if (isRecording) { + requestAnimationFrame(drawCanvasFrame); + } + } + }); + } - var whammy = new Whammy.Video(100); -} + var lastTime = new Date().getTime(); -// _________________ -// WhammyRecorder.js + var whammy = new Whammy.Video(100); + } + // _________________ + // WhammyRecorder.js /** -* WhammyRecorder is a standalone class used by RecordRTC to bring video recording in Chrome. It runs top over {@link Whammy}. -* @summary Video recording feature in Chrome. -* @typedef WhammyRecorder -* @class -* @example -* var recorder = new WhammyRecorder(mediaStream); -* recorder.record(); -* recorder.stop(function(blob) { -* video.src = URL.createObjectURL(blob); -* }); -* @param {MediaStream} mediaStream - MediaStream object fetched using getUserMedia API or generated using captureStreamUntilEnded or WebAudio API. -*/ + * WhammyRecorder is a standalone class used by RecordRTC to bring video recording in Chrome. It runs top over {@link Whammy}. + * @summary Video recording feature in Chrome. + * @typedef WhammyRecorder + * @class + * @example + * var recorder = new WhammyRecorder(mediaStream); + * recorder.record(); + * recorder.stop(function(blob) { + * video.src = URL.createObjectURL(blob); + * }); + * @param {MediaStream} mediaStream - MediaStream object fetched using getUserMedia API or generated using captureStreamUntilEnded or WebAudio API. + */ function WhammyRecorder(mediaStream) { - /** - * This method records video. - * @method - * @memberof WhammyRecorder - * @example - * recorder.record(); - */ - this.record = function() { - if (!this.width) this.width = 320; - if (!this.height) this.height = 240; + /** + * This method records video. + * @method + * @memberof WhammyRecorder + * @example + * recorder.record(); + */ + this.record = function() { + if (!this.width) { + this.width = 320; + } - if (!this.video) { - this.video = { - width: this.width, - height: this.height - }; - } + if (!this.height) { + this.height = 240; + } - if (!this.canvas) { - this.canvas = { - width: this.width, - height: this.height - }; - } + if (!this.video) { + this.video = { + width: this.width, + height: this.height + }; + } - canvas.width = this.canvas.width; - canvas.height = this.canvas.height; + if (!this.canvas) { + this.canvas = { + width: this.width, + height: this.height + }; + } - context = canvas.getContext('2d'); + canvas.width = this.canvas.width; + canvas.height = this.canvas.height; - // setting defaults - if (this.video && this.video instanceof HTMLVideoElement) { - video = this.video.cloneNode(); - } else { - video = document.createElement('video'); - video.src = URL.createObjectURL(mediaStream); + context = canvas.getContext('2d'); - video.width = this.video.width; - video.height = this.video.height; - } + // setting defaults + if (this.video && this.video instanceof HTMLVideoElement) { + video = this.video.cloneNode(); + } else { + video = document.createElement('video'); + video.src = URL.createObjectURL(mediaStream); - video.muted = true; - video.play(); + video.width = this.video.width; + video.height = this.video.height; + } - lastTime = new Date().getTime(); - whammy = new Whammy.Video() - frames = []; + video.muted = true; + video.play(); - console.log('canvas resolutions', canvas.width, '*', canvas.height); - console.log('video width/height', video.width || canvas.width, '*', video.height || canvas.height); + lastTime = new Date().getTime(); + whammy = new Whammy.Video(); - drawFrames(); - }; + console.log('canvas resolutions', canvas.width, '*', canvas.height); + console.log('video width/height', video.width || canvas.width, '*', video.height || canvas.height); - var frames = []; + drawFrames(); + }; - function drawFrames() { - var duration = new Date().getTime() - lastTime; - if (!duration) return drawFrames(); + function drawFrames() { + var duration = new Date().getTime() - lastTime; + if (!duration) { + return drawFrames(); + } - // via #206, by Jack i.e. @Seymourr - lastTime = new Date().getTime(); + // via #206, by Jack i.e. @Seymourr + lastTime = new Date().getTime(); - context.drawImage(video, 0, 0, canvas.width, canvas.height); - frames.push({ - duration: duration, - image: canvas.toDataURL('image/webp') - }); + context.drawImage(video, 0, 0, canvas.width, canvas.height); + whammy.frames.push({ + duration: duration, + image: canvas.toDataURL('image/webp') + }); - if (!isStopDrawing) { - setTimeout(drawFrames, 10); + if (!isStopDrawing) { + setTimeout(drawFrames, 10); + } } - } - /** - * remove black frames from the beginning to the specified frame - * @param {Array} _frames - array of frames to be checked - * @param {number} _framesToCheck - number of frame until check will be executed (-1 - will drop all frames until frame not matched will be found) - * @param {number} _pixTolerance - 0 - very strict (only black pixel color) ; 1 - all - * @param {number} _frameTolerance - 0 - very strict (only black frame color) ; 1 - all - * @returns {Array} - array of frames - */ - // pull#293 by @volodalexey - function dropBlackFrames(_frames, _framesToCheck, _pixTolerance, _frameTolerance) { - var localCanvas = document.createElement('canvas'); - localCanvas.width = canvas.width; - localCanvas.height = canvas.height; - var context2d = localCanvas.getContext('2d'); - var resultFrames = []; - - var checkUntilNotBlack = _framesToCheck === -1; - var endCheckFrame = (_framesToCheck && _framesToCheck > 0 && _framesToCheck <= _frames.length) ? - _framesToCheck : _frames.length; - var sampleColor = { - r: 0, - g: 0, - b: 0 - }; - var maxColorDifference = Math.sqrt( - Math.pow(255, 2) + - Math.pow(255, 2) + - Math.pow(255, 2) - ); - var pixTolerance = _pixTolerance && _pixTolerance >= 0 && _pixTolerance <= 1 ? _pixTolerance : 0; - var frameTolerance = _frameTolerance && _frameTolerance >= 0 && _frameTolerance <= 1 ? _frameTolerance : 0; - var doNotCheckNext = false; - - for (var f = 0; f < endCheckFrame; f++) { - if (!doNotCheckNext) { - var image = new Image(); - image.src = _frames[f].image; - context2d.drawImage(image, 0, 0, canvas.width, canvas.height); - var imageData = context2d.getImageData(0, 0, canvas.width, canvas.height); - var matchPixCount = 0; - var endPixCheck = imageData.data.length; - var maxPixCount = imageData.data.length / 4; - - for (var pix = 0; pix < endPixCheck; pix += 4) { - var currentColor = { - r: imageData.data[pix], - g: imageData.data[pix + 1], - b: imageData.data[pix + 2] - }; - var colorDifference = Math.sqrt( - Math.pow(currentColor.r - sampleColor.r, 2) + - Math.pow(currentColor.g - sampleColor.g, 2) + - Math.pow(currentColor.b - sampleColor.b, 2) - ); - // difference in color it is difference in color vectors (r1,g1,b1) <=> (r2,g2,b2) - if (colorDifference <= maxColorDifference * pixTolerance) { - matchPixCount++; + /** + * remove black frames from the beginning to the specified frame + * @param {Array} _frames - array of frames to be checked + * @param {number} _framesToCheck - number of frame until check will be executed (-1 - will drop all frames until frame not matched will be found) + * @param {number} _pixTolerance - 0 - very strict (only black pixel color) ; 1 - all + * @param {number} _frameTolerance - 0 - very strict (only black frame color) ; 1 - all + * @returns {Array} - array of frames + */ + // pull#293 by @volodalexey + function dropBlackFrames(_frames, _framesToCheck, _pixTolerance, _frameTolerance) { + var localCanvas = document.createElement('canvas'); + localCanvas.width = canvas.width; + localCanvas.height = canvas.height; + var context2d = localCanvas.getContext('2d'); + var resultFrames = []; + + var checkUntilNotBlack = _framesToCheck === -1; + var endCheckFrame = (_framesToCheck && _framesToCheck > 0 && _framesToCheck <= _frames.length) ? + _framesToCheck : _frames.length; + var sampleColor = { + r: 0, + g: 0, + b: 0 + }; + var maxColorDifference = Math.sqrt( + Math.pow(255, 2) + + Math.pow(255, 2) + + Math.pow(255, 2) + ); + var pixTolerance = _pixTolerance && _pixTolerance >= 0 && _pixTolerance <= 1 ? _pixTolerance : 0; + var frameTolerance = _frameTolerance && _frameTolerance >= 0 && _frameTolerance <= 1 ? _frameTolerance : 0; + var doNotCheckNext = false; + + for (var f = 0; f < endCheckFrame; f++) { + var matchPixCount, endPixCheck, maxPixCount; + + if (!doNotCheckNext) { + var image = new Image(); + image.src = _frames[f].image; + context2d.drawImage(image, 0, 0, canvas.width, canvas.height); + var imageData = context2d.getImageData(0, 0, canvas.width, canvas.height); + matchPixCount = 0; + endPixCheck = imageData.data.length; + maxPixCount = imageData.data.length / 4; + + for (var pix = 0; pix < endPixCheck; pix += 4) { + var currentColor = { + r: imageData.data[pix], + g: imageData.data[pix + 1], + b: imageData.data[pix + 2] + }; + var colorDifference = Math.sqrt( + Math.pow(currentColor.r - sampleColor.r, 2) + + Math.pow(currentColor.g - sampleColor.g, 2) + + Math.pow(currentColor.b - sampleColor.b, 2) + ); + // difference in color it is difference in color vectors (r1,g1,b1) <=> (r2,g2,b2) + if (colorDifference <= maxColorDifference * pixTolerance) { + matchPixCount++; + } } } - } - if (!doNotCheckNext && maxPixCount - matchPixCount <= maxPixCount * frameTolerance) { - // console.log('removed black frame : ' + f + ' ; frame duration ' + _frames[f].duration); - } else { - // console.log('frame is passed : ' + f); - if (checkUntilNotBlack) { - doNotCheckNext = true; + if (!doNotCheckNext && maxPixCount - matchPixCount <= maxPixCount * frameTolerance) { + // console.log('removed black frame : ' + f + ' ; frame duration ' + _frames[f].duration); + } else { + // console.log('frame is passed : ' + f); + if (checkUntilNotBlack) { + doNotCheckNext = true; + } + resultFrames.push(_frames[f]); } - resultFrames.push(_frames[f]); } - } - resultFrames = resultFrames.concat(_frames.slice(endCheckFrame)); + resultFrames = resultFrames.concat(_frames.slice(endCheckFrame)); - if (resultFrames.length <= 0) { - // at least one last frame should be available for next manipulation - // if total duration of all frames will be < 1000 than ffmpeg doesn't work well... - resultFrames.push(_frames[_frames.length - 1]); - } + if (resultFrames.length <= 0) { + // at least one last frame should be available for next manipulation + // if total duration of all frames will be < 1000 than ffmpeg doesn't work well... + resultFrames.push(_frames[_frames.length - 1]); + } - return resultFrames; - } + return resultFrames; + } - var isStopDrawing = false; + var isStopDrawing = false; - /** - * This method stops recording video. - * @param {function} callback - Callback function, that is used to pass recorded blob back to the callee. - * @method - * @memberof WhammyRecorder - * @example - * recorder.stop(function(blob) { - * video.src = URL.createObjectURL(blob); - * }); - */ - this.stop = function(callback) { - isStopDrawing = true; - - var _this = this; - // analyse of all frames takes some time! - setTimeout(function() { - // e.g. dropBlackFrames(frames, 10, 1, 1) - will cut all 10 frames - // e.g. dropBlackFrames(frames, 10, 0.5, 0.5) - will analyse 10 frames - // e.g. dropBlackFrames(frames, 10) == dropBlackFrames(frames, 10, 0, 0) - will analyse 10 frames with strict black color - whammy.frames = dropBlackFrames(frames, -1); - - // to display advertisement images! - if (this.advertisement && this.advertisement.length) { - whammy.frames = advertisement.concat(whammy.frames); - } + /** + * This method stops recording video. + * @param {function} callback - Callback function, that is used to pass recorded blob back to the callee. + * @method + * @memberof WhammyRecorder + * @example + * recorder.stop(function(blob) { + * video.src = URL.createObjectURL(blob); + * }); + */ + this.stop = function(callback) { + isStopDrawing = true; - /** - * @property {Blob} blob - Recorded frames in video/webm blob. - * @memberof WhammyRecorder - * @example - * recorder.stop(function() { - * var blob = recorder.blob; - * }); - */ - _this.blob = whammy.compile(); - - if (_this.blob.forEach) { - _this.blob = new Blob([], { - type: 'video/webm' - }); - } + var _this = this; + // analyse of all frames takes some time! + setTimeout(function() { + // e.g. dropBlackFrames(frames, 10, 1, 1) - will cut all 10 frames + // e.g. dropBlackFrames(frames, 10, 0.5, 0.5) - will analyse 10 frames + // e.g. dropBlackFrames(frames, 10) === dropBlackFrames(frames, 10, 0, 0) - will analyse 10 frames with strict black color + whammy.frames = dropBlackFrames(whammy.frames, -1); + + // to display advertisement images! + if (this.advertisement && this.advertisement.length) { + whammy.frames = this.advertisement.concat(whammy.frames); + } - if (callback) callback(_this.blob); - }, 10); - }; + /** + * @property {Blob} blob - Recorded frames in video/webm blob. + * @memberof WhammyRecorder + * @example + * recorder.stop(function() { + * var blob = recorder.blob; + * }); + */ + _this.blob = whammy.compile(); + + if (_this.blob.forEach) { + _this.blob = new Blob([], { + type: 'video/webm' + }); + } - var canvas = document.createElement('canvas'); - var context = canvas.getContext('2d'); + if (callback) { + callback(_this.blob); + } + }, 10); + }; - var video; - var lastTime; - var whammy; -} + var canvas = document.createElement('canvas'); + var context = canvas.getContext('2d'); -// https://github.com/antimatter15/whammy/blob/master/LICENSE -// _________ -// Whammy.js + var video; + var lastTime; + var whammy; + } + // https://github.com/antimatter15/whammy/blob/master/LICENSE + // _________ + // Whammy.js // todo: Firefox now supports webp for webm containers! // their MediaRecorder implementation works well! // should we provide an option to record via Whammy.js or MediaRecorder API is a better solution? /** -* Whammy is a standalone class used by RecordRTC to bring video recording in Chrome. It is written by {@link https://github.com/antimatter15|antimatter15} -* @summary A real time javascript webm encoder based on a canvas hack. -* @typedef Whammy -* @class -* @example -* var recorder = new Whammy().Video(15); -* recorder.add(context || canvas || dataURL); -* var output = recorder.compile(); -*/ + * Whammy is a standalone class used by RecordRTC to bring video recording in Chrome. It is written by {@link https://github.com/antimatter15|antimatter15} + * @summary A real time javascript webm encoder based on a canvas hack. + * @typedef Whammy + * @class + * @example + * var recorder = new Whammy().Video(15); + * recorder.add(context || canvas || dataURL); + * var output = recorder.compile(); + */ var Whammy = (function() { - function toWebM(frames) { + function ArrayToWebM(frames) { var info = checkFrames(frames); if (!info) { return []; } - var CLUSTER_MAX_DURATION = 30000; + var clusterMaxDuration = 30000; var EBML = [{ - "id": 0x1a45dfa3, // EBML - "data": [{ - "data": 1, - "id": 0x4286 // EBMLVersion + 'id': 0x1a45dfa3, // EBML + 'data': [{ + 'data': 1, + 'id': 0x4286 // EBMLVersion }, { - "data": 1, - "id": 0x42f7 // EBMLReadVersion + 'data': 1, + 'id': 0x42f7 // EBMLReadVersion }, { - "data": 4, - "id": 0x42f2 // EBMLMaxIDLength + 'data': 4, + 'id': 0x42f2 // EBMLMaxIDLength }, { - "data": 8, - "id": 0x42f3 // EBMLMaxSizeLength + 'data': 8, + 'id': 0x42f3 // EBMLMaxSizeLength }, { - "data": "webm", - "id": 0x4282 // DocType + 'data': 'webm', + 'id': 0x4282 // DocType }, { - "data": 2, - "id": 0x4287 // DocTypeVersion + 'data': 2, + 'id': 0x4287 // DocTypeVersion }, { - "data": 2, - "id": 0x4285 // DocTypeReadVersion + 'data': 2, + 'id': 0x4285 // DocTypeReadVersion }] }, { - "id": 0x18538067, // Segment - "data": [{ - "id": 0x1549a966, // Info - "data": [{ - "data": 1e6, //do things in millisecs (num of nanosecs for duration scale) - "id": 0x2ad7b1 // TimecodeScale + 'id': 0x18538067, // Segment + 'data': [{ + 'id': 0x1549a966, // Info + 'data': [{ + 'data': 1e6, //do things in millisecs (num of nanosecs for duration scale) + 'id': 0x2ad7b1 // TimecodeScale }, { - "data": "whammy", - "id": 0x4d80 // MuxingApp + 'data': 'whammy', + 'id': 0x4d80 // MuxingApp }, { - "data": "whammy", - "id": 0x5741 // WritingApp + 'data': 'whammy', + 'id': 0x5741 // WritingApp }, { - "data": doubleToString(info.duration), - "id": 0x4489 // Duration + 'data': doubleToString(info.duration), + 'id': 0x4489 // Duration }] }, { - "id": 0x1654ae6b, // Tracks - "data": [{ - "id": 0xae, // TrackEntry - "data": [{ - "data": 1, - "id": 0xd7 // TrackNumber + 'id': 0x1654ae6b, // Tracks + 'data': [{ + 'id': 0xae, // TrackEntry + 'data': [{ + 'data': 1, + 'id': 0xd7 // TrackNumber }, { - "data": 1, - "id": 0x63c5 // TrackUID + 'data': 1, + 'id': 0x63c5 // TrackUID }, { - "data": 0, - "id": 0x9c // FlagLacing + 'data': 0, + 'id': 0x9c // FlagLacing }, { - "data": "und", - "id": 0x22b59c // Language + 'data': 'und', + 'id': 0x22b59c // Language }, { - "data": "V_VP8", - "id": 0x86 // CodecID + 'data': 'V_VP8', + 'id': 0x86 // CodecID }, { - "data": "VP8", - "id": 0x258688 // CodecName + 'data': 'VP8', + 'id': 0x258688 // CodecName }, { - "data": 1, - "id": 0x83 // TrackType + 'data': 1, + 'id': 0x83 // TrackType }, { - "id": 0xe0, // Video - "data": [{ - "data": info.width, - "id": 0xb0 // PixelWidth + 'id': 0xe0, // Video + 'data': [{ + 'data': info.width, + 'id': 0xb0 // PixelWidth }, { - "data": info.height, - "id": 0xba // PixelHeight + 'data': info.height, + 'id': 0xba // PixelHeight }] }] }] @@ -1860,30 +1949,12 @@ var Whammy = (function() { clusterFrames.push(frames[frameNumber]); clusterDuration += frames[frameNumber].duration; frameNumber++; - } while (frameNumber < frames.length && clusterDuration < CLUSTER_MAX_DURATION); + } while (frameNumber < frames.length && clusterDuration < clusterMaxDuration); var clusterCounter = 0; var cluster = { - "id": 0x1f43b675, // Cluster - "data": [{ - "data": clusterTimecode, - "id": 0xe7 // Timecode - }].concat(clusterFrames.map(function(webp) { - var block = makeSimpleBlock({ - discardable: 0, - frame: webp.data.slice(4), - invisible: 0, - keyframe: 1, - lacing: 0, - trackNum: 1, - timecode: Math.round(clusterCounter) - }); - clusterCounter += webp.duration; - return { - data: block, - id: 0xa3 - }; - })) + 'id': 0x1f43b675, // Cluster + 'data': getClusterData(clusterTimecode, clusterCounter, clusterFrames) }; //Add cluster to segment EBML[1].data.push(cluster); clusterTimecode += clusterDuration; @@ -1892,6 +1963,28 @@ var Whammy = (function() { return generateEBML(EBML); } + function getClusterData(clusterTimecode, clusterCounter, clusterFrames) { + return [{ + 'data': clusterTimecode, + 'id': 0xe7 // Timecode + }].concat(clusterFrames.map(function(webp) { + var block = makeSimpleBlock({ + discardable: 0, + frame: webp.data.slice(4), + invisible: 0, + keyframe: 1, + lacing: 0, + trackNum: 1, + timecode: Math.round(clusterCounter) + }); + clusterCounter += webp.duration; + return { + data: block, + id: 0xa3 + }; + })); + } + // sums the lengths of all the frames and gets the duration function checkFrames(frames) { @@ -1943,14 +2036,23 @@ var Whammy = (function() { var ebml = []; for (var i = 0; i < json.length; i++) { var data = json[i].data; - if (typeof data == 'object') data = generateEBML(data); - if (typeof data == 'number') data = bitsToBuffer(data.toString(2)); - if (typeof data == 'string') data = strToBuffer(data); + + if (typeof data === 'object') { + data = generateEBML(data); + } + + if (typeof data === 'number') { + data = bitsToBuffer(data.toString(2)); + } + + if (typeof data === 'string') { + data = strToBuffer(data); + } var len = data.size || data.byteLength || data.length; var zeroes = Math.ceil(Math.ceil(Math.log(len) / Math.log(2)) / 8); - var size_str = len.toString(2); - var padded = (new Array((zeroes * 7 + 7 + 1) - size_str.length)).join('0') + size_str; + var sizeToString = len.toString(2); + var padded = (new Array((zeroes * 7 + 7 + 1) - sizeToString.length)).join('0') + sizeToString; var size = (new Array(zeroes)).join('0') + '1' + padded; ebml.push(numToBuffer(json[i].id)); @@ -1959,11 +2061,11 @@ var Whammy = (function() { } return new Blob(ebml, { - type: "video/webm" + type: 'video/webm' }); } - function toBinStr_old(bits) { + function toBinStrOld(bits) { var data = ''; var pad = (bits.length % 8) ? (new Array(1 + 8 - (bits.length % 8))).join('0') : ''; bits = pad + bits; @@ -1973,34 +2075,29 @@ var Whammy = (function() { return data; } - function generateEBML_old(json) { - var ebml = ''; - for (var i = 0; i < json.length; i++) { - var data = json[i].data; - if (typeof data == 'object') data = generateEBML_old(data); - if (typeof data == 'number') data = toBinStr_old(data.toString(2)); + function makeSimpleBlock(data) { + var flags = 0; - var len = data.length; - var zeroes = Math.ceil(Math.ceil(Math.log(len) / Math.log(2)) / 8); - var size_str = len.toString(2); - var padded = (new Array((zeroes * 7 + 7 + 1) - size_str.length)).join('0') + size_str; - var size = (new Array(zeroes)).join('0') + '1' + padded; + if (data.keyframe) { + flags |= 128; + } - ebml += toBinStr_old(json[i].id.toString(2)) + toBinStr_old(size) + data; + if (data.invisible) { + flags |= 8; + } + if (data.lacing) { + flags |= (data.lacing << 1); + } + + if (data.discardable) { + flags |= 1; } - return ebml; - } - function makeSimpleBlock(data) { - var flags = 0; - if (data.keyframe) flags |= 128; - if (data.invisible) flags |= 8; - if (data.lacing) flags |= (data.lacing << 1); - if (data.discardable) flags |= 1; if (data.trackNum > 127) { - throw "TrackNumber > 127 not supported"; + throw 'TrackNumber > 127 not supported'; } + var out = [data.trackNum | 0x80, data.timecode >> 8, data.timecode & 0xff, flags].map(function(e) { return String.fromCharCode(e); }).join('') + data.frame; @@ -2011,8 +2108,10 @@ var Whammy = (function() { function parseWebP(riff) { var VP8 = riff.RIFF[0].WEBP[0]; - var frame_start = VP8.indexOf('\x9d\x01\x2a'); // A VP8 keyframe starts with the 0x9d012a header - for (var i = 0, c = []; i < 4; i++) c[i] = VP8.charCodeAt(frame_start + 3 + i); + var frameStart = VP8.indexOf('\x9d\x01\x2a'); // A VP8 keyframe starts with the 0x9d012a header + for (var i = 0, c = []; i < 4; i++) { + c[i] = VP8.charCodeAt(frameStart + 3 + i); + } var width, height, tmp; @@ -2029,21 +2128,25 @@ var Whammy = (function() { }; } + function getStrLength(string, offset) { + return parseInt(string.substr(offset + 4, 4).split('').map(function(i) { + var unpadded = i.charCodeAt(0).toString(2); + return (new Array(8 - unpadded.length + 1)).join('0') + unpadded; + }).join(''), 2); + } + function parseRIFF(string) { var offset = 0; var chunks = {}; while (offset < string.length) { var id = string.substr(offset, 4); - var len = parseInt(string.substr(offset + 4, 4).split('').map(function(i) { - var unpadded = i.charCodeAt(0).toString(2); - return (new Array(8 - unpadded.length + 1)).join('0') + unpadded; - }).join(''), 2); + var len = getStrLength(string, offset); var data = string.substr(offset + 4 + 4, len); offset += 4 + 4 + len; chunks[id] = chunks[id] || []; - if (id == 'RIFF' || id == 'LIST') { + if (id === 'RIFF' || id === 'LIST') { chunks[id].push(parseRIFF(data)); } else { chunks[id].push(data); @@ -2068,15 +2171,15 @@ var Whammy = (function() { } /** - * Pass Canvas or Context or image/webp(string) to {@link Whammy} encoder. - * @method - * @memberof Whammy - * @example - * recorder = new Whammy().Video(0.8, 100); - * recorder.add(canvas || context || 'image/webp'); - * @param {string} frame - Canvas || Context || image/webp - * @param {number} duration - Stick a duration (in milliseconds) - */ + * Pass Canvas or Context or image/webp(string) to {@link Whammy} encoder. + * @method + * @memberof Whammy + * @example + * recorder = new Whammy().Video(0.8, 100); + * recorder.add(canvas || context || 'image/webp'); + * @param {string} frame - Canvas || Context || image/webp + * @param {number} duration - Stick a duration (in milliseconds) + */ WhammyVideo.prototype.add = function(frame, duration) { if ('canvas' in frame) { //CanvasRenderingContext2D frame = frame.canvas; @@ -2087,7 +2190,7 @@ var Whammy = (function() { } if (!(/^data:image\/webp;base64,/ig).test(frame)) { - throw "Input must be formatted properly as a base64 encoded DataURI of type image/webp"; + throw 'Input must be formatted properly as a base64 encoded DataURI of type image/webp'; } this.frames.push({ image: frame, @@ -2096,16 +2199,16 @@ var Whammy = (function() { }; /** - * Encodes frames in WebM container. It invokes "toWebM" method. - * @method - * @memberof Whammy - * @example - * recorder = new Whammy().Video(0.8, 100); - * var blob = recorder.compile(); - * @returns {Blob} blob - Encoded WebM blob; - */ + * Encodes frames in WebM container. It invokes 'ArrayToWebM' method. + * @method + * @memberof Whammy + * @example + * recorder = new Whammy().Video(0.8, 100); + * var blob = recorder.compile(); + * @returns {Blob} blob - Encoded WebM blob; + */ WhammyVideo.prototype.compile = function() { - return new toWebM(this.frames.map(function(frame) { + return new ArrayToWebM(this.frames.map(function(frame) { var webp = parseWebP(parseRIFF(atob(frame.image.slice(23)))); webp.duration = frame.duration; return webp; @@ -2114,67 +2217,66 @@ var Whammy = (function() { return { /** - * A more abstract-ish API. - * @method - * @memberof Whammy - * @example - * recorder = new Whammy().Video(0.8, 100); - * @param {?number} speed - 0.8 - * @param {?number} quality - 100 - */ + * A more abstract-ish API. + * @method + * @memberof Whammy + * @example + * recorder = new Whammy().Video(0.8, 100); + * @param {?number} speed - 0.8 + * @param {?number} quality - 100 + */ Video: WhammyVideo, /** - * Encoding frames array into WebM container using WebP images. - * @method - * @memberof Whammy - * @inner - */ - toWebM: toWebM + * Encoding frames array into WebM container using WebP images. + * @method + * @memberof Whammy + * @inner + */ + ArrayToWebM: ArrayToWebM }; })(); - // ______________ (indexed-db) // DiskStorage.js /** -* DiskStorage is a standalone object used by {@link RecordRTC} to store recorded blobs in IndexedDB storage. -* @summary Writing blobs into IndexedDB. -* @example -* DiskStorage.Store({ -* audioBlob: yourAudioBlob, -* videoBlob: yourVideoBlob, -* gifBlob : yourGifBlob -* }); -* DiskStorage.Fetch(function(dataURL, type) { -* if(type == 'audioBlob') { } -* if(type == 'videoBlob') { } -* if(type == 'gifBlob') { } -* }); -* // DiskStorage.dataStoreName = 'recordRTC'; -* // DiskStorage.onError = function(error) { }; -* @property {function} init - This method must be called once to initialize IndexedDB ObjectStore. Though, it is auto-used internally. -* @property {function} Fetch - This method fetches stored blobs from IndexedDB. -* @property {function} Store - This method stores blobs in IndexedDB. -* @property {function} onError - This function is invoked for any known/unknown error. -* @property {string} dataStoreName - Name of the ObjectStore created in IndexedDB storage. -*/ + * DiskStorage is a standalone object used by {@link RecordRTC} to store recorded blobs in IndexedDB storage. + * @summary Writing blobs into IndexedDB. + * @example + * DiskStorage.Store({ + * audioBlob: yourAudioBlob, + * videoBlob: yourVideoBlob, + * gifBlob : yourGifBlob + * }); + * DiskStorage.Fetch(function(dataURL, type) { + * if(type === 'audioBlob') { } + * if(type === 'videoBlob') { } + * if(type === 'gifBlob') { } + * }); + * // DiskStorage.dataStoreName = 'recordRTC'; + * // DiskStorage.onError = function(error) { }; + * @property {function} init - This method must be called once to initialize IndexedDB ObjectStore. Though, it is auto-used internally. + * @property {function} Fetch - This method fetches stored blobs from IndexedDB. + * @property {function} Store - This method stores blobs in IndexedDB. + * @property {function} onError - This function is invoked for any known/unknown error. + * @property {string} dataStoreName - Name of the ObjectStore created in IndexedDB storage. + */ var DiskStorage = { /** - * This method must be called once to initialize IndexedDB ObjectStore. Though, it is auto-used internally. - * @method - * @memberof DiskStorage - * @internal - * @example - * DiskStorage.init(); - */ + * This method must be called once to initialize IndexedDB ObjectStore. Though, it is auto-used internally. + * @method + * @memberof DiskStorage + * @internal + * @example + * DiskStorage.init(); + */ init: function() { var self = this; var indexedDB = window.indexedDB || window.webkitIndexedDB || window.mozIndexedDB || window.OIndexedDB || window.msIndexedDB; var dbVersion = 1; - var dbName = location.href.replace(/\/|:|#|%|\.|\[|\]/g, ''), + var dbName = this.dbName || location.href.replace(/\/|:|#|%|\.|\[|\]/g, ''), db; var request = indexedDB.open(dbName, dbVersion); @@ -2199,7 +2301,9 @@ var DiskStorage = { function getFromStore(portionName) { transaction.objectStore(self.dataStoreName).get(portionName).onsuccess = function(event) { - if (self.callback) self.callback(event.target.result, portionName); + if (self.callback) { + self.callback(event.target.result, portionName); + } }; } @@ -2215,7 +2319,7 @@ var DiskStorage = { db.onerror = self.onError; if (db.setVersion) { - if (db.version != dbVersion) { + if (db.version !== dbVersion) { var setVersion = db.setVersion(dbVersion); setVersion.onsuccess = function() { createObjectStore(db); @@ -2233,17 +2337,17 @@ var DiskStorage = { }; }, /** - * This method fetches stored blobs from IndexedDB. - * @method - * @memberof DiskStorage - * @internal - * @example - * DiskStorage.Fetch(function(dataURL, type) { - * if(type == 'audioBlob') { } - * if(type == 'videoBlob') { } - * if(type == 'gifBlob') { } - * }); - */ + * This method fetches stored blobs from IndexedDB. + * @method + * @memberof DiskStorage + * @internal + * @example + * DiskStorage.Fetch(function(dataURL, type) { + * if(type === 'audioBlob') { } + * if(type === 'videoBlob') { } + * if(type === 'gifBlob') { } + * }); + */ Fetch: function(callback) { this.callback = callback; this.init(); @@ -2251,17 +2355,17 @@ var DiskStorage = { return this; }, /** - * This method stores blobs in IndexedDB. - * @method - * @memberof DiskStorage - * @internal - * @example - * DiskStorage.Store({ - * audioBlob: yourAudioBlob, - * videoBlob: yourVideoBlob, - * gifBlob : yourGifBlob - * }); - */ + * This method stores blobs in IndexedDB. + * @method + * @memberof DiskStorage + * @internal + * @example + * DiskStorage.Store({ + * audioBlob: yourAudioBlob, + * videoBlob: yourVideoBlob, + * gifBlob : yourGifBlob + * }); + */ Store: function(config) { this.audioBlob = config.audioBlob; this.videoBlob = config.videoBlob; @@ -2272,44 +2376,44 @@ var DiskStorage = { return this; }, /** - * This function is invoked for any known/unknown error. - * @method - * @memberof DiskStorage - * @internal - * @example - * DiskStorage.onError = function(error){ - * alerot( JSON.stringify(error) ); - * }; - */ + * This function is invoked for any known/unknown error. + * @method + * @memberof DiskStorage + * @internal + * @example + * DiskStorage.onError = function(error){ + * alerot( JSON.stringify(error) ); + * }; + */ onError: function(error) { console.error(JSON.stringify(error, null, '\t')); }, /** - * @property {string} dataStoreName - Name of the ObjectStore created in IndexedDB storage. - * @memberof DiskStorage - * @internal - * @example - * DiskStorage.dataStoreName = 'recordRTC'; - */ - dataStoreName: 'recordRTC' + * @property {string} dataStoreName - Name of the ObjectStore created in IndexedDB storage. + * @memberof DiskStorage + * @internal + * @example + * DiskStorage.dataStoreName = 'recordRTC'; + */ + dataStoreName: 'recordRTC', + dbName: null }; - // ______________ // GifRecorder.js /** -* GifRecorder is standalone calss used by {@link RecordRTC} to record video as animated gif image. -* @typedef GifRecorder -* @class -* @example -* var recorder = new GifRecorder(mediaStream); -* recorder.record(); -* recorder.stop(function(blob) { -* img.src = URL.createObjectURL(blob); -* }); -* @param {MediaStream} mediaStream - MediaStream object fetched using getUserMedia API or generated using captureStreamUntilEnded or WebAudio API. -*/ + * GifRecorder is standalone calss used by {@link RecordRTC} to record video as animated gif image. + * @typedef GifRecorder + * @class + * @example + * var recorder = new GifRecorder(mediaStream); + * recorder.record(); + * recorder.stop(function(blob) { + * img.src = URL.createObjectURL(blob); + * }); + * @param {MediaStream} mediaStream - MediaStream object fetched using getUserMedia API or generated using captureStreamUntilEnded or WebAudio API. + */ function GifRecorder(mediaStream) { if (!window.GIFEncoder) { @@ -2317,15 +2421,20 @@ function GifRecorder(mediaStream) { } /** - * This method records MediaStream. - * @method - * @memberof GifRecorder - * @example - * recorder.record(); - */ + * This method records MediaStream. + * @method + * @memberof GifRecorder + * @example + * recorder.record(); + */ this.record = function() { - if (!this.width) this.width = video.offsetWidth || 320; - if (!this.height) this.height = video.offsetHeight || 240; + if (!this.width) { + this.width = video.offsetWidth || 320; + } + + if (!this.height) { + this.height = video.offsetHeight || 240; + } if (!this.video) { this.video = { @@ -2348,7 +2457,7 @@ function GifRecorder(mediaStream) { video.height = this.video.height; // external library to record as GIF images - gifEncoder = new GIFEncoder(); + gifEncoder = new window.GIFEncoder(); // void setRepeat(int iter) // Sets the number of times the set of GIF frames should be played. @@ -2386,7 +2495,9 @@ function GifRecorder(mediaStream) { } // ~10 fps - if (time - lastFrameTime < 90) return; + if (time - lastFrameTime < 90) { + return; + } context.drawImage(video, 0, 0, canvas.width, canvas.height); @@ -2402,28 +2513,30 @@ function GifRecorder(mediaStream) { }; /** - * This method stops recording MediaStream. - * @param {function} callback - Callback function, that is used to pass recorded blob back to the callee. - * @method - * @memberof GifRecorder - * @example - * recorder.stop(function(blob) { - * img.src = URL.createObjectURL(blob); - * }); - */ + * This method stops recording MediaStream. + * @param {function} callback - Callback function, that is used to pass recorded blob back to the callee. + * @method + * @memberof GifRecorder + * @example + * recorder.stop(function(blob) { + * img.src = URL.createObjectURL(blob); + * }); + */ this.stop = function() { - if (lastAnimationFrame) cancelAnimationFrame(lastAnimationFrame); + if (lastAnimationFrame) { + cancelAnimationFrame(lastAnimationFrame); + } endTime = Date.now(); /** - * @property {Blob} blob - The recorded blob object. - * @memberof GifRecorder - * @example - * recorder.stop(function(){ - * var blob = recorder.blob; - * }); - */ + * @property {Blob} blob - The recorded blob object. + * @memberof GifRecorder + * @example + * recorder.stop(function(){ + * var blob = recorder.blob; + * }); + */ this.blob = new Blob([new Uint8Array(gifEncoder.stream().bin)], { type: 'image/gif' }); @@ -2446,22 +2559,3 @@ function GifRecorder(mediaStream) { var gifEncoder; } - -if (location.href.indexOf('file:') == 0) { - console.error('Please load this HTML file on HTTP or HTTPS.'); -} - -// below function via: http://goo.gl/B3ae8c -/** -* @param {number} bytes - Pass bytes and get formafted string. -* @returns {string} - formafted string -* @example -* bytesToSize(1024*1024*5) == '5 GB' -*/ -function bytesToSize(bytes) { - var k = 1000; - var sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB']; - if (bytes === 0) return '0 Bytes'; - var i = parseInt(Math.floor(Math.log(bytes) / Math.log(k)), 10); - return (bytes / Math.pow(k, i)).toPrecision(3) + ' ' + sizes[i]; -} diff --git a/RecordRTC/RecordRTC.min.js b/RecordRTC/RecordRTC.min.js new file mode 100644 index 00000000..9248b758 --- /dev/null +++ b/RecordRTC/RecordRTC.min.js @@ -0,0 +1 @@ +"use strict";function RecordRTC(mediaStream,config){function startRecording(){console.debug("started recording "+config.type+" stream.");var Recorder=isChrome?window.StereoRecorder:window.MediaStreamRecorder;return"video"===config.type&&isChrome&&(Recorder=window.WhammyRecorder),"gif"===config.type&&(Recorder=window.GifRecorder),"canvas"===config.type&&(Recorder=window.CanvasRecorder),mediaRecorder=new Recorder(mediaStream),mediaRecorder=mergeProps(mediaRecorder,config),mediaRecorder.onAudioProcessStarted=function(){config.onAudioProcessStarted&&config.onAudioProcessStarted()},mediaRecorder.onGifPreview=function(gif){config.onGifPreview&&config.onGifPreview(gif)},mediaRecorder.record(),self}function stopRecording(callback){function _callback(){for(var item in mediaRecorder)self&&(self[item]=mediaRecorder[item]),recordRTC&&(recordRTC[item]=mediaRecorder[item]);var blob=mediaRecorder.blob;if(callback){var url=URL.createObjectURL(blob);callback(url)}console.debug(blob.type,"->",bytesToSize(blob.size)),config.autoWriteToDisk&&getDataURL(function(dataURL){var parameter={};parameter[config.type+"Blob"]=dataURL,DiskStorage.Store(parameter)})}if(!mediaRecorder)return console.warn(WARNING);var recordRTC=this;console.warn("Stopped recording "+config.type+" stream."),"gif"!==config.type?mediaRecorder.stop(_callback):(mediaRecorder.stop(),_callback())}function getDataURL(callback,_mediaRecorder){function processInWebWorker(_function){var blob=URL.createObjectURL(new Blob([_function.toString(),"this.onmessage = function (e) {readFile(e.data);}"],{type:"application/javascript"})),worker=new Worker(blob);return URL.revokeObjectURL(blob),worker}if(!callback)throw"Pass a callback function over getDataURL.";var blob=_mediaRecorder?_mediaRecorder.blob:mediaRecorder.blob;if(!blob)return console.warn("Blob encoder did not yet finished its job."),void setTimeout(function(){getDataURL(callback,_mediaRecorder)},1e3);if(window.Worker){var webWorker=processInWebWorker(function(_blob){postMessage((new FileReaderSync).readAsDataURL(_blob))});webWorker.onmessage=function(event){callback(event.data)},webWorker.postMessage(blob)}else{var reader=new FileReader;reader.readAsDataURL(blob),reader.onload=function(event){callback(event.target.result)}}}if(config=config||{},!mediaStream)throw"MediaStream is mandatory.";config.type||(config.type="audio");var mediaRecorder,self=this,WARNING='It seems that "startRecording" is not invoked for '+config.type+" recorder.",returnObject={startRecording:startRecording,stopRecording:stopRecording,getBlob:function(){return mediaRecorder?mediaRecorder.blob:console.warn(WARNING)},getDataURL:getDataURL,toURL:function(){return mediaRecorder?URL.createObjectURL(mediaRecorder.blob):console.warn(WARNING)},save:function(fileName){if(!mediaRecorder){var that=this;return setTimeout(function(){that.save(fileName)},2e3),console.warn(WARNING)}var hyperlink=document.createElement("a");hyperlink.href=URL.createObjectURL(mediaRecorder.blob),hyperlink.target="_blank",hyperlink.download=(fileName||Math.round(9999999999*Math.random())+888888888)+"."+mediaRecorder.blob.type.split("/")[1];var evt=new MouseEvent("click",{view:window,bubbles:!0,cancelable:!0});hyperlink.dispatchEvent(evt),(window.URL||window.webkitURL).revokeObjectURL(hyperlink.href)},getFromDisk:function(callback){return mediaRecorder?void RecordRTC.getFromDisk(config.type,callback):console.warn(WARNING)},setAdvertisementArray:function(arrayOfWebPImages){this.advertisement=[];for(var length=arrayOfWebPImages.length,i=0;length>i;i++)this.advertisement.push({duration:i,image:arrayOfWebPImages[i]})},blob:null,bufferSize:0,sampleRate:0,buffer:null,view:null};if(!this)return returnObject;for(var prop in returnObject)this[prop]=returnObject[prop];return returnObject}function MRecordRTC(mediaStream){this.addStream=function(_mediaStream){_mediaStream&&(mediaStream=_mediaStream)},this.mediaType={audio:!0,video:!0},this.startRecording=function(){!isChrome&&mediaStream&&mediaStream.getAudioTracks&&mediaStream.getAudioTracks().length&&mediaStream.getVideoTracks().length&&(this.mediaType.audio=!1),this.mediaType.audio&&(this.audioRecorder=new RecordRTC(mediaStream,{type:"audio",bufferSize:this.bufferSize,sampleRate:this.sampleRate}),this.audioRecorder.startRecording()),this.mediaType.video&&(this.videoRecorder=new RecordRTC(mediaStream,{type:"video",video:this.video,canvas:this.canvas}),this.videoRecorder.startRecording()),this.mediaType.gif&&(this.gifRecorder=new RecordRTC(mediaStream,{type:"gif",frameRate:this.frameRate||200,quality:this.quality||10}),this.gifRecorder.startRecording())},this.stopRecording=function(callback){callback=callback||function(){},this.audioRecorder&&this.audioRecorder.stopRecording(function(blobURL){callback(blobURL,"audio")}),this.videoRecorder&&this.videoRecorder.stopRecording(function(blobURL){callback(blobURL,"video")}),this.gifRecorder&&this.gifRecorder.stopRecording(function(blobURL){callback(blobURL,"gif")})},this.getBlob=function(callback){var output={};this.audioRecorder&&(output.audio=this.audioRecorder.getBlob()),this.videoRecorder&&(output.video=this.videoRecorder.getBlob()),this.gifRecorder&&(output.gif=this.gifRecorder.getBlob()),callback&&callback(output)},this.getDataURL=function(callback){function getDataURL(blob,callback00){if(window.Worker){var webWorker=processInWebWorker(function(_blob){postMessage((new FileReaderSync).readAsDataURL(_blob))});webWorker.onmessage=function(event){callback00(event.data)},webWorker.postMessage(blob)}else{var reader=new FileReader;reader.readAsDataURL(blob),reader.onload=function(event){callback00(event.target.result)}}}function processInWebWorker(_function){var blob=URL.createObjectURL(new Blob([_function.toString(),"this.onmessage = function (e) {readFile(e.data);}"],{type:"application/javascript"})),worker=new Worker(blob);return URL.revokeObjectURL(blob),worker}this.getBlob(function(blob){getDataURL(blob.audio,function(_audioDataURL){getDataURL(blob.video,function(_videoDataURL){callback({audio:_audioDataURL,video:_videoDataURL})})})})},this.writeToDisk=function(){RecordRTC.writeToDisk({audio:this.audioRecorder,video:this.videoRecorder,gif:this.gifRecorder})},this.save=function(args){args=args||{audio:!0,video:!0,gif:!0},args.audio&&this.audioRecorder&&this.audioRecorder.save("string"==typeof args.audio?args.audio:""),args.video&&this.videoRecorder&&this.videoRecorder.save("string"==typeof args.video?args.video:""),args.gif&&this.gifRecorder&&this.gifRecorder.save("string"==typeof args.gif?args.gif:"")}}function mergeProps(mergein,mergeto){mergeto=reformatProps(mergeto);for(var t in mergeto)"function"!=typeof mergeto[t]&&(mergein[t]=mergeto[t]);return mergein}function reformatProps(obj){var output={};for(var o in obj)if(-1!==o.indexOf("-")){var splitted=o.split("-"),name=splitted[0]+splitted[1].split("")[0].toUpperCase()+splitted[1].substr(1);output[name]=obj[o]}else output[o]=obj[o];return output}function bytesToSize(bytes){var k=1e3,sizes=["Bytes","KB","MB","GB","TB"];if(0===bytes)return"0 Bytes";var i=parseInt(Math.floor(Math.log(bytes)/Math.log(k)),10);return(bytes/Math.pow(k,i)).toPrecision(3)+" "+sizes[i]}function MediaStreamRecorder(mediaStream){var self=this;if(self.mimeType&&"video/webm"!==self.mimeType&&mediaStream.getVideoTracks&&mediaStream.getVideoTracks().length){var context=new AudioContext,mediaStreamSource=context.createMediaStreamSource(mediaStream),destination=context.createMediaStreamDestination();mediaStreamSource.connect(destination),mediaStream=destination.stream}var dataAvailable=!1;this.record=function(){mediaRecorder=new window.MediaRecorder(mediaStream),mediaRecorder.ondataavailable=function(e){if(!dataAvailable){if(!e.data.size)return void console.warn("Recording of",e.data.type,"failed.");dataAvailable=!0,self.blob=new Blob([e.data],{type:e.data.type||self.mimeType||"audio/ogg"}),self.callback()}},mediaRecorder.onerror=function(error){console.warn(error),mediaRecorder.stop(),self.record(0)},mediaRecorder.start(0),self.onAudioProcessStarted&&self.onAudioProcessStarted()},this.stop=function(callback){this.callback=callback,"recording"===mediaRecorder.state&&mediaRecorder.stop()};var mediaRecorder}function StereoRecorder(mediaStream){var self=this;this.record=function(){mediaRecorder=new StereoAudioRecorder(mediaStream,this),mediaRecorder.onAudioProcessStarted=function(){self.onAudioProcessStarted&&self.onAudioProcessStarted()},mediaRecorder.record()},this.stop=function(callback){mediaRecorder&&mediaRecorder.stop(function(){for(var item in mediaRecorder)self[item]=mediaRecorder[item];callback()})};var mediaRecorder}function StereoAudioRecorder(mediaStream,config){function interleave(leftChannel,rightChannel){for(var length=leftChannel.length+rightChannel.length,result=new Float32Array(length),inputIndex=0,index=0;length>index;)result[index++]=leftChannel[inputIndex],result[index++]=rightChannel[inputIndex],inputIndex++;return result}function mergeBuffers(channelBuffer,rLength){for(var result=new Float32Array(rLength),offset=0,lng=channelBuffer.length,i=0;lng>i;i++){var buffer=channelBuffer[i];result.set(buffer,offset),offset+=buffer.length}return result}function writeUTFBytes(view,offset,string){for(var lng=string.length,i=0;lng>i;i++)view.setUint8(offset+i,string.charCodeAt(i))}if(!mediaStream.getAudioTracks().length)throw"Your stream has no audio tracks.";var leftchannel=[],rightchannel=[],recording=!1,recordingLength=0;this.record=function(){leftchannel.length=rightchannel.length=0,recordingLength=0,recording=!0},this.stop=function(callback){recording=!1,audioInput.disconnect(),volume.disconnect();var leftBuffer=mergeBuffers(leftchannel,recordingLength),rightBuffer=mergeBuffers(rightchannel,recordingLength),interleaved=interleave(leftBuffer,rightBuffer),buffer=new ArrayBuffer(44+2*interleaved.length),view=new DataView(buffer);writeUTFBytes(view,0,"RIFF"),view.setUint32(4,36+2*interleaved.length,!0),writeUTFBytes(view,8,"WAVE"),writeUTFBytes(view,12,"fmt "),view.setUint32(16,16,!0),view.setUint16(20,1,!0),view.setUint16(22,2,!0),view.setUint32(24,sampleRate,!0),view.setUint32(28,4*sampleRate,!0),view.setUint16(32,4,!0),view.setUint16(34,16,!0),writeUTFBytes(view,36,"data"),view.setUint32(40,2*interleaved.length,!0);for(var offset=44,i=0;is?32768*s:32767*s,!0)}this.blob=new Blob([view],{type:"audio/wav"}),this.buffer=new ArrayBuffer(view),this.view=view,this.sampleRate=sampleRate,this.bufferSize=bufferSize,this.length=recordingLength,callback(),isAudioProcessStarted=!1},Storage.AudioContextConstructor||(Storage.AudioContextConstructor=new Storage.AudioContext);var context=Storage.AudioContextConstructor,volume=context.createGain(),audioInput=context.createMediaStreamSource(mediaStream);audioInput.connect(volume);var legalBufferValues=[256,512,1024,2048,4096,8192,16384],bufferSize=config.bufferSize||4096;-1===legalBufferValues.indexOf(bufferSize);var sampleRate=config.sampleRate||context.sampleRate||44100;if(context.createJavaScriptNode)__stereoAudioRecorderJavacriptNode=context.createJavaScriptNode(bufferSize,2,2);else{if(!context.createScriptProcessor)throw"WebAudio API has no support on this browser.";__stereoAudioRecorderJavacriptNode=context.createScriptProcessor(bufferSize,2,2)}bufferSize=__stereoAudioRecorderJavacriptNode.bufferSize,console.log("sample-rate",sampleRate),console.log("buffer-size",bufferSize);var isAudioProcessStarted=!1,self=this;__stereoAudioRecorderJavacriptNode.onaudioprocess=function(e){if(mediaStream.ended)return void(__stereoAudioRecorderJavacriptNode.onaudioprocess=function(){});if(recording){isAudioProcessStarted||(isAudioProcessStarted=!0,self.onAudioProcessStarted&&self.onAudioProcessStarted());var left=e.inputBuffer.getChannelData(0),right=e.inputBuffer.getChannelData(1);leftchannel.push(new Float32Array(left)),rightchannel.push(new Float32Array(right)),recordingLength+=bufferSize}},volume.connect(__stereoAudioRecorderJavacriptNode),__stereoAudioRecorderJavacriptNode.connect(context.destination)}function CanvasRecorder(htmlElement){function drawCanvasFrame(){window.html2canvas(htmlElement,{onrendered:function(canvas){var duration=(new Date).getTime()-lastTime;return duration?(lastTime=(new Date).getTime(),whammy.frames.push({duration:duration,image:canvas.toDataURL("image/webp")}),void(isRecording&&requestAnimationFrame(drawCanvasFrame))):drawCanvasFrame()}})}if(!window.html2canvas)throw"Please link: //cdn.webrtc-experiment.com/screenshot.js";var isRecording;this.record=function(){isRecording=!0,whammy.frames=[],drawCanvasFrame()},this.stop=function(callback){isRecording=!1,this.blob=whammy.compile(),callback&&callback(this.blob)};var lastTime=(new Date).getTime(),whammy=new Whammy.Video(100)}function WhammyRecorder(mediaStream){function drawFrames(){var duration=(new Date).getTime()-lastTime;return duration?(lastTime=(new Date).getTime(),context.drawImage(video,0,0,canvas.width,canvas.height),whammy.frames.push({duration:duration,image:canvas.toDataURL("image/webp")}),void(isStopDrawing||setTimeout(drawFrames,10))):drawFrames()}function dropBlackFrames(_frames,_framesToCheck,_pixTolerance,_frameTolerance){var localCanvas=document.createElement("canvas");localCanvas.width=canvas.width,localCanvas.height=canvas.height;for(var context2d=localCanvas.getContext("2d"),resultFrames=[],checkUntilNotBlack=-1===_framesToCheck,endCheckFrame=_framesToCheck&&_framesToCheck>0&&_framesToCheck<=_frames.length?_framesToCheck:_frames.length,sampleColor={r:0,g:0,b:0},maxColorDifference=Math.sqrt(Math.pow(255,2)+Math.pow(255,2)+Math.pow(255,2)),pixTolerance=_pixTolerance&&_pixTolerance>=0&&1>=_pixTolerance?_pixTolerance:0,frameTolerance=_frameTolerance&&_frameTolerance>=0&&1>=_frameTolerance?_frameTolerance:0,doNotCheckNext=!1,f=0;endCheckFrame>f;f++){var matchPixCount,endPixCheck,maxPixCount;if(!doNotCheckNext){var image=new Image;image.src=_frames[f].image,context2d.drawImage(image,0,0,canvas.width,canvas.height);var imageData=context2d.getImageData(0,0,canvas.width,canvas.height);matchPixCount=0,endPixCheck=imageData.data.length,maxPixCount=imageData.data.length/4;for(var pix=0;endPixCheck>pix;pix+=4){var currentColor={r:imageData.data[pix],g:imageData.data[pix+1],b:imageData.data[pix+2]},colorDifference=Math.sqrt(Math.pow(currentColor.r-sampleColor.r,2)+Math.pow(currentColor.g-sampleColor.g,2)+Math.pow(currentColor.b-sampleColor.b,2));maxColorDifference*pixTolerance>=colorDifference&&matchPixCount++}}!doNotCheckNext&&maxPixCount*frameTolerance>=maxPixCount-matchPixCount||(checkUntilNotBlack&&(doNotCheckNext=!0),resultFrames.push(_frames[f]))}return resultFrames=resultFrames.concat(_frames.slice(endCheckFrame)),resultFrames.length<=0&&resultFrames.push(_frames[_frames.length-1]),resultFrames}this.record=function(){this.width||(this.width=320),this.height||(this.height=240),this.video||(this.video={width:this.width,height:this.height}),this.canvas||(this.canvas={width:this.width,height:this.height}),canvas.width=this.canvas.width,canvas.height=this.canvas.height,context=canvas.getContext("2d"),this.video&&this.video instanceof HTMLVideoElement?video=this.video.cloneNode():(video=document.createElement("video"),video.src=URL.createObjectURL(mediaStream),video.width=this.video.width,video.height=this.video.height),video.muted=!0,video.play(),lastTime=(new Date).getTime(),whammy=new Whammy.Video,console.log("canvas resolutions",canvas.width,"*",canvas.height),console.log("video width/height",video.width||canvas.width,"*",video.height||canvas.height),drawFrames()};var isStopDrawing=!1;this.stop=function(callback){isStopDrawing=!0;var _this=this;setTimeout(function(){whammy.frames=dropBlackFrames(whammy.frames,-1),this.advertisement&&this.advertisement.length&&(whammy.frames=this.advertisement.concat(whammy.frames)),_this.blob=whammy.compile(),_this.blob.forEach&&(_this.blob=new Blob([],{type:"video/webm"})),callback&&callback(_this.blob)},10)};var video,lastTime,whammy,canvas=document.createElement("canvas"),context=canvas.getContext("2d")}function GifRecorder(mediaStream){if(!window.GIFEncoder)throw"Please link: https://cdn.webrtc-experiment.com/gif-recorder.js";this.record=function(){function drawVideoFrame(time){lastAnimationFrame=requestAnimationFrame(drawVideoFrame),void 0===typeof lastFrameTime&&(lastFrameTime=time),90>time-lastFrameTime||(context.drawImage(video,0,0,canvas.width,canvas.height),self.onGifPreview&&self.onGifPreview(canvas.toDataURL("image/png")),gifEncoder.addFrame(context),lastFrameTime=time)}this.width||(this.width=video.offsetWidth||320),this.height||(this.height=video.offsetHeight||240),this.video||(this.video={width:this.width,height:this.height}),this.canvas||(this.canvas={width:this.width,height:this.height}),canvas.width=this.canvas.width,canvas.height=this.canvas.height,video.width=this.video.width,video.height=this.video.height,gifEncoder=new window.GIFEncoder,gifEncoder.setRepeat(0),gifEncoder.setDelay(this.frameRate||200),gifEncoder.setQuality(this.quality||10),gifEncoder.start(),startTime=Date.now();var self=this;lastAnimationFrame=requestAnimationFrame(drawVideoFrame)},this.stop=function(){lastAnimationFrame&&cancelAnimationFrame(lastAnimationFrame),endTime=Date.now(),this.blob=new Blob([new Uint8Array(gifEncoder.stream().bin)],{type:"image/gif"}),gifEncoder.stream().bin=[]};var canvas=document.createElement("canvas"),context=canvas.getContext("2d"),video=document.createElement("video");video.muted=!0,video.autoplay=!0,video.src=URL.createObjectURL(mediaStream),video.play();var startTime,endTime,lastFrameTime,gifEncoder,lastAnimationFrame=null}RecordRTC.getFromDisk=function(type,callback){if(!callback)throw"callback is mandatory.";console.log("Getting recorded "+("all"===type?"blobs":type+" blob ")+" from disk!"),DiskStorage.Fetch(function(dataURL,_type){"all"!==type&&_type===type+"Blob"&&callback&&callback(dataURL),"all"===type&&callback&&callback(dataURL,_type.replace("Blob",""))})},RecordRTC.writeToDisk=function(options){console.log("Writing recorded blob(s) to disk!"),options=options||{},options.audio&&options.video&&options.gif?options.audio.getDataURL(function(audioDataURL){options.video.getDataURL(function(videoDataURL){options.gif.getDataURL(function(gifDataURL){DiskStorage.Store({audioBlob:audioDataURL,videoBlob:videoDataURL,gifBlob:gifDataURL})})})}):options.audio&&options.video?options.audio.getDataURL(function(audioDataURL){options.video.getDataURL(function(videoDataURL){DiskStorage.Store({audioBlob:audioDataURL,videoBlob:videoDataURL})})}):options.audio&&options.gif?options.audio.getDataURL(function(audioDataURL){options.gif.getDataURL(function(gifDataURL){DiskStorage.Store({audioBlob:audioDataURL,gifBlob:gifDataURL})})}):options.video&&options.gif?options.video.getDataURL(function(videoDataURL){options.gif.getDataURL(function(gifDataURL){DiskStorage.Store({videoBlob:videoDataURL,gifBlob:gifDataURL})})}):options.audio?options.audio.getDataURL(function(audioDataURL){DiskStorage.Store({audioBlob:audioDataURL})}):options.video?options.video.getDataURL(function(videoDataURL){DiskStorage.Store({videoBlob:videoDataURL})}):options.gif&&options.gif.getDataURL(function(gifDataURL){DiskStorage.Store({gifBlob:gifDataURL})})},MRecordRTC.getFromDisk=RecordRTC.getFromDisk,MRecordRTC.writeToDisk=RecordRTC.writeToDisk,window.requestAnimationFrame||(window.requestAnimationFrame=window.webkitRequestAnimationFrame||window.mozRequestAnimationFrame),window.cancelAnimationFrame||(window.cancelAnimationFrame=window.webkitCancelAnimationFrame||window.mozCancelAnimationFrame),window.AudioContext||(window.AudioContext=window.webkitAudioContext||window.mozAudioContext),window.URL=window.URL||window.webkitURL,navigator.getUserMedia=navigator.webkitGetUserMedia||navigator.mozGetUserMedia,window.webkitMediaStream&&(window.MediaStream=window.webkitMediaStream);var isChrome=!!navigator.webkitGetUserMedia;0===location.href.indexOf("file:")&&console.error("Please load this HTML file on HTTP or HTTPS.");var Storage={AudioContext:window.AudioContext||window.webkitAudioContext},__stereoAudioRecorderJavacriptNode,Whammy=function(){function ArrayToWebM(frames){var info=checkFrames(frames);if(!info)return[];for(var clusterMaxDuration=3e4,EBML=[{id:440786851,data:[{data:1,id:17030},{data:1,id:17143},{data:4,id:17138},{data:8,id:17139},{data:"webm",id:17026},{data:2,id:17031},{data:2,id:17029}]},{id:408125543,data:[{id:357149030,data:[{data:1e6,id:2807729},{data:"whammy",id:19840},{data:"whammy",id:22337},{data:doubleToString(info.duration),id:17545}]},{id:374648427,data:[{id:174,data:[{data:1,id:215},{data:1,id:25541},{data:0,id:156},{data:"und",id:2274716},{data:"V_VP8",id:134},{data:"VP8",id:2459272},{data:1,id:131},{id:224,data:[{data:info.width,id:176},{data:info.height,id:186}]}]}]}]}],frameNumber=0,clusterTimecode=0;frameNumberclusterDuration);var clusterCounter=0,cluster={id:524531317,data:getClusterData(clusterTimecode,clusterCounter,clusterFrames)};EBML[1].data.push(cluster),clusterTimecode+=clusterDuration}return generateEBML(EBML)}function getClusterData(clusterTimecode,clusterCounter,clusterFrames){return[{data:clusterTimecode,id:231}].concat(clusterFrames.map(function(webp){var block=makeSimpleBlock({discardable:0,frame:webp.data.slice(4),invisible:0,keyframe:1,lacing:0,trackNum:1,timecode:Math.round(clusterCounter)});return clusterCounter+=webp.duration,{data:block,id:163}}))}function checkFrames(frames){if(!frames[0])return void console.warn("Something went wrong. Maybe WebP format is not supported in the current browser.");for(var width=frames[0].width,height=frames[0].height,duration=frames[0].duration,i=1;i0;)parts.push(255&num),num>>=8;return new Uint8Array(parts.reverse())}function strToBuffer(str){return new Uint8Array(str.split("").map(function(e){return e.charCodeAt(0)}))}function bitsToBuffer(bits){var data=[],pad=bits.length%8?new Array(9-bits.length%8).join("0"):"";bits=pad+bits;for(var i=0;i127)throw"TrackNumber > 127 not supported";var out=[128|data.trackNum,data.timecode>>8,255&data.timecode,flags].map(function(e){return String.fromCharCode(e)}).join("")+data.frame;return out}function parseWebP(riff){for(var VP8=riff.RIFF[0].WEBP[0],frameStart=VP8.indexOf("Â*"),i=0,c=[];4>i;i++)c[i]=VP8.charCodeAt(frameStart+3+i);var width,height,tmp;return tmp=c[1]<<8|c[0],width=16383&tmp,tmp=c[3]<<8|c[2],height=16383&tmp,{width:width,height:height,data:VP8,riff:riff}}function getStrLength(string,offset){return parseInt(string.substr(offset+4,4).split("").map(function(i){var unpadded=i.charCodeAt(0).toString(2);return new Array(8-unpadded.length+1).join("0")+unpadded}).join(""),2)}function parseRIFF(string){for(var offset=0,chunks={};offset', bytesToSize(blob.size)); + + if (!config.autoWriteToDisk) { + return; + } + + getDataURL(function(dataURL) { + var parameter = {}; + parameter[config.type + 'Blob'] = dataURL; + DiskStorage.Store(parameter); + }); + } + } + + function getDataURL(callback, _mediaRecorder) { + if (!callback) { + throw 'Pass a callback function over getDataURL.'; + } + + var blob = _mediaRecorder ? _mediaRecorder.blob : mediaRecorder.blob; + + if (!blob) { + console.warn('Blob encoder did not yet finished its job.'); + setTimeout(function() { + getDataURL(callback, _mediaRecorder); + }, 1000); + return; + } + + if (!!window.Worker) { + var webWorker = processInWebWorker(function readFile(_blob) { + postMessage(new FileReaderSync().readAsDataURL(_blob)); + }); + + webWorker.onmessage = function(event) { + callback(event.data); + }; + + webWorker.postMessage(blob); + } else { + var reader = new FileReader(); + reader.readAsDataURL(blob); + reader.onload = function(event) { + callback(event.target.result); + }; + } + + function processInWebWorker(_function) { + var blob = URL.createObjectURL(new Blob([_function.toString(), + 'this.onmessage = function (e) {readFile(e.data);}' + ], { + type: 'application/javascript' + })); + + var worker = new Worker(blob); + URL.revokeObjectURL(blob); + return worker; + } + } + + var WARNING = 'It seems that "startRecording" is not invoked for ' + config.type + ' recorder.'; + + var mediaRecorder; + + var returnObject = { + /** + * This method starts recording. It doesn't take any argument. + * @method + * @memberof RecordRTC + * @instance + * @example + * recordRTC.startRecording(); + */ + startRecording: startRecording, + + /** + * This method stops recording. It takes single "callback" argument. It is suggested to get blob or URI in the callback to make sure all encoders finished their jobs. + * @param {function} callback - This callback function is invoked after completion of all encoding jobs. + * @method + * @memberof RecordRTC + * @instance + * @example + * recordRTC.stopRecording(function(videoURL) { + * video.src = videoURL; + * recordRTC.blob; recordRTC.buffer; + * }); + * @todo Implement recordRTC.stopRecording().getDataURL(callback); + */ + stopRecording: stopRecording, + + /** + * It is equivalent to "recordRTC.blob" property. + * @method + * @memberof RecordRTC + * @instance + * @example + * recordRTC.stopRecording(function() { + * var blob = recordRTC.getBlob(); + * + * // equivalent to: recordRTC.blob property + * var blob = recordRTC.blob; + * }); + */ + getBlob: function() { + if (!mediaRecorder) { + return console.warn(WARNING); + } + + return mediaRecorder.blob; + }, + + /** + * This method returns DataURL. It takes single "callback" argument. + * @param {function} callback - DataURL is passed back over this callback. + * @method + * @memberof RecordRTC + * @instance + * @example + * recordRTC.stopRecording(function() { + * recordRTC.getDataURL(function(dataURL) { + * video.src = dataURL; + * }); + * }); + */ + getDataURL: getDataURL, + + /** + * This method returns Virutal/Blob URL. It doesn't take any argument. + * @method + * @memberof RecordRTC + * @instance + * @example + * recordRTC.stopRecording(function() { + * video.src = recordRTC.toURL(); + * }); + */ + toURL: function() { + if (!mediaRecorder) { + return console.warn(WARNING); + } + + return URL.createObjectURL(mediaRecorder.blob); + }, + + /** + * This method saves blob/file into disk (by inovking save-as dialog). It takes single (optional) argument i.e. FileName + * @method + * @memberof RecordRTC + * @instance + * @example + * recordRTC.stopRecording(function() { + * recordRTC.save('file-name'); + * }); + */ + save: function(fileName) { + if (!mediaRecorder) { + var that = this; + setTimeout(function() { + that.save(fileName); + }, 2000); + return console.warn(WARNING); + } + + var hyperlink = document.createElement('a'); + hyperlink.href = URL.createObjectURL(mediaRecorder.blob); + hyperlink.target = '_blank'; + hyperlink.download = (fileName || (Math.round(Math.random() * 9999999999) + 888888888)) + '.' + mediaRecorder.blob.type.split('/')[1]; + + var evt = new MouseEvent('click', { + view: window, + bubbles: true, + cancelable: true + }); + + hyperlink.dispatchEvent(evt); + + (window.URL || window.webkitURL).revokeObjectURL(hyperlink.href); + }, + + /** + * This method gets blob from indexed-DB storage. It takes single "callback" argument. + * @method + * @memberof RecordRTC + * @instance + * @example + * recordRTC.getFromDisk(function(dataURL) { + * video.src = dataURL; + * }); + */ + getFromDisk: function(callback) { + if (!mediaRecorder) { + return console.warn(WARNING); + } + + RecordRTC.getFromDisk(config.type, callback); + }, + + /** + * This method appends prepends array of webp images to the recorded video-blob. It takes an "array" object. + * @type {Array.} + * @param {Array} arrayOfWebPImages - Array of webp images. + * @method + * @memberof RecordRTC + * @instance + * @example + * var arrayOfWebPImages = []; + * arrayOfWebPImages.push({ + * duration: index, + * image: 'data:image/webp;base64,...' + * }); + * recordRTC.setAdvertisementArray(arrayOfWebPImages); + */ + setAdvertisementArray: function(arrayOfWebPImages) { + this.advertisement = []; + + var length = arrayOfWebPImages.length; + for (var i = 0; i < length; i++) { + this.advertisement.push({ + duration: i, + image: arrayOfWebPImages[i] + }); + } + }, + + /** + * It is equivalent to "recordRTC.getBlob()" method. + * @property {Blob} blob - Recorded Blob can be accessed using this property. + * @memberof RecordRTC + * @instance + * @example + * recordRTC.stopRecording(function() { + * var blob = recordRTC.blob; + * + * // equivalent to: recordRTC.getBlob() method + * var blob = recordRTC.getBlob(); + * }); + */ + blob: null, + + /** + * @todo Add descriptions. + * @property {number} bufferSize - Either audio device's default buffer-size, or your custom value. + * @memberof RecordRTC + * @instance + * @example + * recordRTC.stopRecording(function() { + * var bufferSize = recordRTC.bufferSize; + * }); + */ + bufferSize: 0, + + /** + * @todo Add descriptions. + * @property {number} sampleRate - Audio device's default sample rates. + * @memberof RecordRTC + * @instance + * @example + * recordRTC.stopRecording(function() { + * var sampleRate = recordRTC.sampleRate; + * }); + */ + sampleRate: 0, + + /** + * @todo Add descriptions. + * @property {ArrayBuffer} buffer - Audio ArrayBuffer, supported only in Chrome. + * @memberof RecordRTC + * @instance + * @example + * recordRTC.stopRecording(function() { + * var buffer = recordRTC.buffer; + * }); + */ + buffer: null, + + /** + * @todo Add descriptions. + * @property {DataView} view - Audio DataView, supported only in Chrome. + * @memberof RecordRTC + * @instance + * @example + * recordRTC.stopRecording(function() { + * var dataView = recordRTC.view; + * }); + */ + view: null + }; + + if (!this) { + return returnObject; + } + + // if someone wanna use RecordRTC with "new" keyword. + for (var prop in returnObject) { + this[prop] = returnObject[prop]; + } + + return returnObject; +} + +/** + * This method can be used to get all recorded blobs from IndexedDB storage. + * @param {string} type - 'all' or 'audio' or 'video' or 'gif' + * @param {function} callback - Callback function to get all stored blobs. + * @method + * @memberof RecordRTC + * @example + * RecordRTC.getFromDisk('all', function(dataURL, type){ + * if(type === 'audio') { } + * if(type === 'video') { } + * if(type === 'gif') { } + * }); + */ +RecordRTC.getFromDisk = function(type, callback) { + if (!callback) { + throw 'callback is mandatory.'; + } + + console.log('Getting recorded ' + (type === 'all' ? 'blobs' : type + ' blob ') + ' from disk!'); + DiskStorage.Fetch(function(dataURL, _type) { + if (type !== 'all' && _type === type + 'Blob' && callback) { + callback(dataURL); + } + + if (type === 'all' && callback) { + callback(dataURL, _type.replace('Blob', '')); + } + }); +}; + +/** + * This method can be used to store recorded blobs into IndexedDB storage. + * @param {object} options - {audio: Blob, video: Blob, gif: Blob} + * @method + * @memberof RecordRTC + * @example + * RecordRTC.writeToDisk({ + * audio: audioBlob, + * video: videoBlob, + * gif : gifBlob + * }); + */ +RecordRTC.writeToDisk = function(options) { + console.log('Writing recorded blob(s) to disk!'); + options = options || {}; + if (options.audio && options.video && options.gif) { + options.audio.getDataURL(function(audioDataURL) { + options.video.getDataURL(function(videoDataURL) { + options.gif.getDataURL(function(gifDataURL) { + DiskStorage.Store({ + audioBlob: audioDataURL, + videoBlob: videoDataURL, + gifBlob: gifDataURL + }); + }); + }); + }); + } else if (options.audio && options.video) { + options.audio.getDataURL(function(audioDataURL) { + options.video.getDataURL(function(videoDataURL) { + DiskStorage.Store({ + audioBlob: audioDataURL, + videoBlob: videoDataURL + }); + }); + }); + } else if (options.audio && options.gif) { + options.audio.getDataURL(function(audioDataURL) { + options.gif.getDataURL(function(gifDataURL) { + DiskStorage.Store({ + audioBlob: audioDataURL, + gifBlob: gifDataURL + }); + }); + }); + } else if (options.video && options.gif) { + options.video.getDataURL(function(videoDataURL) { + options.gif.getDataURL(function(gifDataURL) { + DiskStorage.Store({ + videoBlob: videoDataURL, + gifBlob: gifDataURL + }); + }); + }); + } else if (options.audio) { + options.audio.getDataURL(function(audioDataURL) { + DiskStorage.Store({ + audioBlob: audioDataURL + }); + }); + } else if (options.video) { + options.video.getDataURL(function(videoDataURL) { + DiskStorage.Store({ + videoBlob: videoDataURL + }); + }); + } else if (options.gif) { + options.gif.getDataURL(function(gifDataURL) { + DiskStorage.Store({ + gifBlob: gifDataURL + }); + }); + } +}; diff --git a/RecordRTC/dev/StereoAudioRecorder.js b/RecordRTC/dev/StereoAudioRecorder.js new file mode 100644 index 00000000..783d9442 --- /dev/null +++ b/RecordRTC/dev/StereoAudioRecorder.js @@ -0,0 +1,327 @@ +// source code from: http://typedarray.org/wp-content/projects/WebAudioRecorder/script.js +// https://github.com/mattdiamond/Recorderjs#license-mit +// ______________________ +// StereoAudioRecorder.js + +/** + * StereoAudioRecorder is a standalone class used by RecordRTC to bring "stereo" audio-recording in chrome. + * @summary JavaScript standalone object for stereo audio recording. + * @typedef StereoAudioRecorder + * @class + * @example + * var recorder = new StereoAudioRecorder(MediaStream, { + * sampleRate: 44100, + * bufferSize: 4096 + * }); + * recorder.record(); + * recorder.stop(function(blob) { + * video.src = URL.createObjectURL(blob); + * }); + * @param {MediaStream} mediaStream - MediaStream object fetched using getUserMedia API or generated using captureStreamUntilEnded or WebAudio API. + * @param {object} config - {sampleRate: 44100, bufferSize: 4096} + */ + +var __stereoAudioRecorderJavacriptNode; + +function StereoAudioRecorder(mediaStream, config) { + if (!mediaStream.getAudioTracks().length) { + throw 'Your stream has no audio tracks.'; + } + + // variables + var leftchannel = []; + var rightchannel = []; + var recording = false; + var recordingLength = 0; + + /** + * This method records MediaStream. + * @method + * @memberof StereoAudioRecorder + * @example + * recorder.record(); + */ + this.record = function() { + // reset the buffers for the new recording + leftchannel.length = rightchannel.length = 0; + recordingLength = 0; + + recording = true; + }; + + /** + * This method stops recording MediaStream. + * @param {function} callback - Callback function, that is used to pass recorded blob back to the callee. + * @method + * @memberof StereoAudioRecorder + * @example + * recorder.stop(function(blob) { + * video.src = URL.createObjectURL(blob); + * }); + */ + this.stop = function(callback) { + // stop recording + recording = false; + + // to make sure onaudioprocess stops firing + audioInput.disconnect(); + volume.disconnect(); + + // flat the left and right channels down + var leftBuffer = mergeBuffers(leftchannel, recordingLength); + var rightBuffer = mergeBuffers(rightchannel, recordingLength); + + // interleave both channels together + var interleaved = interleave(leftBuffer, rightBuffer); + + // create our wav file + var buffer = new ArrayBuffer(44 + interleaved.length * 2); + + var view = new DataView(buffer); + + // RIFF chunk descriptor/identifier + writeUTFBytes(view, 0, 'RIFF'); + + // RIFF chunk length + // view.setUint32(4, 44 + interleaved.length * 2, true); + view.setUint32(4, 36 + interleaved.length * 2, true); + + // RIFF type + writeUTFBytes(view, 8, 'WAVE'); + + // format chunk identifier + // FMT sub-chunk + writeUTFBytes(view, 12, 'fmt '); + + // format chunk length + view.setUint32(16, 16, true); + + // sample format (raw) + view.setUint16(20, 1, true); + + // stereo (2 channels) + view.setUint16(22, 2, true); + + // sample rate + view.setUint32(24, sampleRate, true); + + // byte rate (sample rate * block align) + view.setUint32(28, sampleRate * 4, true); + + // block align (channel count * bytes per sample) + view.setUint16(32, 4, true); + + // bits per sample + view.setUint16(34, 16, true); + + // data sub-chunk + // data chunk identifier + writeUTFBytes(view, 36, 'data'); + + // data chunk length + view.setUint32(40, interleaved.length * 2, true); + + // write the PCM samples + var offset = 44; + for (var i = 0; i < interleaved.length; i++, offset += 2) { + var s = Math.max(-1, Math.min(1, interleaved[i])); + view.setInt16(offset, s < 0 ? s * 0x8000 : s * 0x7FFF, true); + } + + /** + * @property {Blob} blob - The recorded blob object. + * @memberof StereoAudioRecorder + * @example + * recorder.stop(function(){ + * var blob = recorder.blob; + * }); + */ + this.blob = new Blob([view], { + type: 'audio/wav' + }); + + /** + * @property {ArrayBuffer} buffer - The recorded buffer object. + * @memberof StereoAudioRecorder + * @example + * recorder.stop(function(){ + * var buffer = recorder.buffer; + * }); + */ + this.buffer = new ArrayBuffer(view); + + /** + * @property {DataView} view - The recorded data-view object. + * @memberof StereoAudioRecorder + * @example + * recorder.stop(function(){ + * var view = recorder.view; + * }); + */ + this.view = view; + + this.sampleRate = sampleRate; + this.bufferSize = bufferSize; + + // recorded audio length + this.length = recordingLength; + + callback(); + + isAudioProcessStarted = false; + }; + + function interleave(leftChannel, rightChannel) { + var length = leftChannel.length + rightChannel.length; + var result = new Float32Array(length); + + var inputIndex = 0; + + for (var index = 0; index < length;) { + result[index++] = leftChannel[inputIndex]; + result[index++] = rightChannel[inputIndex]; + inputIndex++; + } + return result; + } + + function mergeBuffers(channelBuffer, rLength) { + var result = new Float32Array(rLength); + var offset = 0; + var lng = channelBuffer.length; + for (var i = 0; i < lng; i++) { + var buffer = channelBuffer[i]; + result.set(buffer, offset); + offset += buffer.length; + } + return result; + } + + function writeUTFBytes(view, offset, string) { + var lng = string.length; + for (var i = 0; i < lng; i++) { + view.setUint8(offset + i, string.charCodeAt(i)); + } + } + + if (!Storage.AudioContextConstructor) { + Storage.AudioContextConstructor = new Storage.AudioContext(); + } + + var context = Storage.AudioContextConstructor; + + // creates a gain node + var volume = context.createGain(); + + // creates an audio node from the microphone incoming stream + var audioInput = context.createMediaStreamSource(mediaStream); + + // connect the stream to the gain node + audioInput.connect(volume); + + var legalBufferValues = [256, 512, 1024, 2048, 4096, 8192, 16384]; + + /** + * From the spec: This value controls how frequently the audioprocess event is + * dispatched and how many sample-frames need to be processed each call. + * Lower values for buffer size will result in a lower (better) latency. + * Higher values will be necessary to avoid audio breakup and glitches + * The size of the buffer (in sample-frames) which needs to + * be processed each time onprocessaudio is called. + * Legal values are (256, 512, 1024, 2048, 4096, 8192, 16384). + * @property {number} bufferSize - Buffer-size for how frequently the audioprocess event is dispatched. + * @memberof StereoAudioRecorder + * @example + * recorder = new StereoAudioRecorder(mediaStream, { + * bufferSize: 4096 + * }); + */ + + // "0" means, let chrome decide the most accurate buffer-size for current platform. + var bufferSize = config.bufferSize || 4096; + + if (legalBufferValues.indexOf(bufferSize) === -1) { + // throw 'Legal values for buffer-size are ' + JSON.stringify(legalBufferValues, null, '\t'); + } + + + /** + * The sample rate (in sample-frames per second) at which the + * AudioContext handles audio. It is assumed that all AudioNodes + * in the context run at this rate. In making this assumption, + * sample-rate converters or "varispeed" processors are not supported + * in real-time processing. + * The sampleRate parameter describes the sample-rate of the + * linear PCM audio data in the buffer in sample-frames per second. + * An implementation must support sample-rates in at least + * the range 22050 to 96000. + * @property {number} sampleRate - Buffer-size for how frequently the audioprocess event is dispatched. + * @memberof StereoAudioRecorder + * @example + * recorder = new StereoAudioRecorder(mediaStream, { + * sampleRate: 44100 + * }); + */ + var sampleRate = config.sampleRate || context.sampleRate || 44100; + + if (sampleRate < 22050 || sampleRate > 96000) { + // Ref: http://stackoverflow.com/a/26303918/552182 + // throw 'sample-rate must be under range 22050 and 96000.'; + } + + if (context.createJavaScriptNode) { + __stereoAudioRecorderJavacriptNode = context.createJavaScriptNode(bufferSize, 2, 2); + } else if (context.createScriptProcessor) { + __stereoAudioRecorderJavacriptNode = context.createScriptProcessor(bufferSize, 2, 2); + } else { + throw 'WebAudio API has no support on this browser.'; + } + + bufferSize = __stereoAudioRecorderJavacriptNode.bufferSize; + + console.log('sample-rate', sampleRate); + console.log('buffer-size', bufferSize); + + var isAudioProcessStarted = false, + self = this; + __stereoAudioRecorderJavacriptNode.onaudioprocess = function(e) { + // if MediaStream().stop() or MediaStreamTrack.stop() is invoked. + if (mediaStream.ended) { + __stereoAudioRecorderJavacriptNode.onaudioprocess = function() {}; + return; + } + + if (!recording) { + return; + } + + /** + * This method is called on "onaudioprocess" event's first invocation. + * @method {function} onAudioProcessStarted + * @memberof StereoAudioRecorder + * @example + * recorder.onAudioProcessStarted: function() { }; + */ + if (!isAudioProcessStarted) { + isAudioProcessStarted = true; + if (self.onAudioProcessStarted) { + self.onAudioProcessStarted(); + } + } + + var left = e.inputBuffer.getChannelData(0); + var right = e.inputBuffer.getChannelData(1); + + // we clone the samples + leftchannel.push(new Float32Array(left)); + rightchannel.push(new Float32Array(right)); + + recordingLength += bufferSize; + }; + + // we connect the recorder + volume.connect(__stereoAudioRecorderJavacriptNode); + + // to prevent self audio to be connected with speakers + __stereoAudioRecorderJavacriptNode.connect(context.destination); +} diff --git a/RecordRTC/dev/StereoRecorder.js b/RecordRTC/dev/StereoRecorder.js new file mode 100644 index 00000000..b456ff79 --- /dev/null +++ b/RecordRTC/dev/StereoRecorder.js @@ -0,0 +1,63 @@ +// _________________ +// StereoRecorder.js + +/** + * StereoRecorder is a standalone class used by RecordRTC to bring audio-recording in chrome. It runs top over {@link StereoAudioRecorder}. + * @summary JavaScript standalone object for stereo audio recording. + * @typedef StereoRecorder + * @class + * @example + * var recorder = new StereoRecorder(MediaStream); + * recorder.record(); + * recorder.stop(function(blob) { + * video.src = URL.createObjectURL(blob); + * }); + * @param {MediaStream} mediaStream - MediaStream object fetched using getUserMedia API or generated using captureStreamUntilEnded or WebAudio API. + */ + +function StereoRecorder(mediaStream) { + var self = this; + + /** + * This method records MediaStream. + * @method + * @memberof StereoRecorder + * @example + * recorder.record(); + */ + this.record = function() { + mediaRecorder = new StereoAudioRecorder(mediaStream, this); + mediaRecorder.onAudioProcessStarted = function() { + if (self.onAudioProcessStarted) { + self.onAudioProcessStarted(); + } + }; + mediaRecorder.record(); + }; + + /** + * This method stops recording MediaStream. + * @param {function} callback - Callback function, that is used to pass recorded blob back to the callee. + * @method + * @memberof StereoRecorder + * @example + * recorder.stop(function(blob) { + * video.src = URL.createObjectURL(blob); + * }); + */ + this.stop = function(callback) { + if (!mediaRecorder) { + return; + } + + mediaRecorder.stop(function() { + for (var item in mediaRecorder) { + self[item] = mediaRecorder[item]; + } + callback(); + }); + }; + + // Reference to "StereoAudioRecorder" object + var mediaRecorder; +} diff --git a/RecordRTC/dev/Storage.js b/RecordRTC/dev/Storage.js new file mode 100644 index 00000000..c2949495 --- /dev/null +++ b/RecordRTC/dev/Storage.js @@ -0,0 +1,13 @@ +// __________ (used to handle stuff like http://goo.gl/xmE5eg) issue #129 +// Storage.js + +/** + * Storage is a standalone object used by {@link RecordRTC} to store reusable objects e.g. "new AudioContext". + * @example + * Storage.AudioContext === webkitAudioContext + * @property {webkitAudioContext} AudioContext - Keeps a reference to AudioContext object. + */ + +var Storage = { + AudioContext: window.AudioContext || window.webkitAudioContext +}; diff --git a/RecordRTC/dev/Whammy.js b/RecordRTC/dev/Whammy.js new file mode 100644 index 00000000..acd643fe --- /dev/null +++ b/RecordRTC/dev/Whammy.js @@ -0,0 +1,407 @@ +// https://github.com/antimatter15/whammy/blob/master/LICENSE +// _________ +// Whammy.js + +// todo: Firefox now supports webp for webm containers! +// their MediaRecorder implementation works well! +// should we provide an option to record via Whammy.js or MediaRecorder API is a better solution? + +/** + * Whammy is a standalone class used by RecordRTC to bring video recording in Chrome. It is written by {@link https://github.com/antimatter15|antimatter15} + * @summary A real time javascript webm encoder based on a canvas hack. + * @typedef Whammy + * @class + * @example + * var recorder = new Whammy().Video(15); + * recorder.add(context || canvas || dataURL); + * var output = recorder.compile(); + */ + +var Whammy = (function() { + + function ArrayToWebM(frames) { + var info = checkFrames(frames); + if (!info) { + return []; + } + + var clusterMaxDuration = 30000; + + var EBML = [{ + 'id': 0x1a45dfa3, // EBML + 'data': [{ + 'data': 1, + 'id': 0x4286 // EBMLVersion + }, { + 'data': 1, + 'id': 0x42f7 // EBMLReadVersion + }, { + 'data': 4, + 'id': 0x42f2 // EBMLMaxIDLength + }, { + 'data': 8, + 'id': 0x42f3 // EBMLMaxSizeLength + }, { + 'data': 'webm', + 'id': 0x4282 // DocType + }, { + 'data': 2, + 'id': 0x4287 // DocTypeVersion + }, { + 'data': 2, + 'id': 0x4285 // DocTypeReadVersion + }] + }, { + 'id': 0x18538067, // Segment + 'data': [{ + 'id': 0x1549a966, // Info + 'data': [{ + 'data': 1e6, //do things in millisecs (num of nanosecs for duration scale) + 'id': 0x2ad7b1 // TimecodeScale + }, { + 'data': 'whammy', + 'id': 0x4d80 // MuxingApp + }, { + 'data': 'whammy', + 'id': 0x5741 // WritingApp + }, { + 'data': doubleToString(info.duration), + 'id': 0x4489 // Duration + }] + }, { + 'id': 0x1654ae6b, // Tracks + 'data': [{ + 'id': 0xae, // TrackEntry + 'data': [{ + 'data': 1, + 'id': 0xd7 // TrackNumber + }, { + 'data': 1, + 'id': 0x63c5 // TrackUID + }, { + 'data': 0, + 'id': 0x9c // FlagLacing + }, { + 'data': 'und', + 'id': 0x22b59c // Language + }, { + 'data': 'V_VP8', + 'id': 0x86 // CodecID + }, { + 'data': 'VP8', + 'id': 0x258688 // CodecName + }, { + 'data': 1, + 'id': 0x83 // TrackType + }, { + 'id': 0xe0, // Video + 'data': [{ + 'data': info.width, + 'id': 0xb0 // PixelWidth + }, { + 'data': info.height, + 'id': 0xba // PixelHeight + }] + }] + }] + }] + }]; + + //Generate clusters (max duration) + var frameNumber = 0; + var clusterTimecode = 0; + while (frameNumber < frames.length) { + + var clusterFrames = []; + var clusterDuration = 0; + do { + clusterFrames.push(frames[frameNumber]); + clusterDuration += frames[frameNumber].duration; + frameNumber++; + } while (frameNumber < frames.length && clusterDuration < clusterMaxDuration); + + var clusterCounter = 0; + var cluster = { + 'id': 0x1f43b675, // Cluster + 'data': getClusterData(clusterTimecode, clusterCounter, clusterFrames) + }; //Add cluster to segment + EBML[1].data.push(cluster); + clusterTimecode += clusterDuration; + } + + return generateEBML(EBML); + } + + function getClusterData(clusterTimecode, clusterCounter, clusterFrames) { + return [{ + 'data': clusterTimecode, + 'id': 0xe7 // Timecode + }].concat(clusterFrames.map(function(webp) { + var block = makeSimpleBlock({ + discardable: 0, + frame: webp.data.slice(4), + invisible: 0, + keyframe: 1, + lacing: 0, + trackNum: 1, + timecode: Math.round(clusterCounter) + }); + clusterCounter += webp.duration; + return { + data: block, + id: 0xa3 + }; + })); + } + + // sums the lengths of all the frames and gets the duration + + function checkFrames(frames) { + if (!frames[0]) { + console.warn('Something went wrong. Maybe WebP format is not supported in the current browser.'); + return; + } + + var width = frames[0].width, + height = frames[0].height, + duration = frames[0].duration; + + for (var i = 1; i < frames.length; i++) { + duration += frames[i].duration; + } + return { + duration: duration, + width: width, + height: height + }; + } + + function numToBuffer(num) { + var parts = []; + while (num > 0) { + parts.push(num & 0xff); + num = num >> 8; + } + return new Uint8Array(parts.reverse()); + } + + function strToBuffer(str) { + return new Uint8Array(str.split('').map(function(e) { + return e.charCodeAt(0); + })); + } + + function bitsToBuffer(bits) { + var data = []; + var pad = (bits.length % 8) ? (new Array(1 + 8 - (bits.length % 8))).join('0') : ''; + bits = pad + bits; + for (var i = 0; i < bits.length; i += 8) { + data.push(parseInt(bits.substr(i, 8), 2)); + } + return new Uint8Array(data); + } + + function generateEBML(json) { + var ebml = []; + for (var i = 0; i < json.length; i++) { + var data = json[i].data; + + if (typeof data === 'object') { + data = generateEBML(data); + } + + if (typeof data === 'number') { + data = bitsToBuffer(data.toString(2)); + } + + if (typeof data === 'string') { + data = strToBuffer(data); + } + + var len = data.size || data.byteLength || data.length; + var zeroes = Math.ceil(Math.ceil(Math.log(len) / Math.log(2)) / 8); + var sizeToString = len.toString(2); + var padded = (new Array((zeroes * 7 + 7 + 1) - sizeToString.length)).join('0') + sizeToString; + var size = (new Array(zeroes)).join('0') + '1' + padded; + + ebml.push(numToBuffer(json[i].id)); + ebml.push(bitsToBuffer(size)); + ebml.push(data); + } + + return new Blob(ebml, { + type: 'video/webm' + }); + } + + function toBinStrOld(bits) { + var data = ''; + var pad = (bits.length % 8) ? (new Array(1 + 8 - (bits.length % 8))).join('0') : ''; + bits = pad + bits; + for (var i = 0; i < bits.length; i += 8) { + data += String.fromCharCode(parseInt(bits.substr(i, 8), 2)); + } + return data; + } + + function makeSimpleBlock(data) { + var flags = 0; + + if (data.keyframe) { + flags |= 128; + } + + if (data.invisible) { + flags |= 8; + } + + if (data.lacing) { + flags |= (data.lacing << 1); + } + + if (data.discardable) { + flags |= 1; + } + + if (data.trackNum > 127) { + throw 'TrackNumber > 127 not supported'; + } + + var out = [data.trackNum | 0x80, data.timecode >> 8, data.timecode & 0xff, flags].map(function(e) { + return String.fromCharCode(e); + }).join('') + data.frame; + + return out; + } + + function parseWebP(riff) { + var VP8 = riff.RIFF[0].WEBP[0]; + + var frameStart = VP8.indexOf('\x9d\x01\x2a'); // A VP8 keyframe starts with the 0x9d012a header + for (var i = 0, c = []; i < 4; i++) { + c[i] = VP8.charCodeAt(frameStart + 3 + i); + } + + var width, height, tmp; + + //the code below is literally copied verbatim from the bitstream spec + tmp = (c[1] << 8) | c[0]; + width = tmp & 0x3FFF; + tmp = (c[3] << 8) | c[2]; + height = tmp & 0x3FFF; + return { + width: width, + height: height, + data: VP8, + riff: riff + }; + } + + function getStrLength(string, offset) { + return parseInt(string.substr(offset + 4, 4).split('').map(function(i) { + var unpadded = i.charCodeAt(0).toString(2); + return (new Array(8 - unpadded.length + 1)).join('0') + unpadded; + }).join(''), 2); + } + + function parseRIFF(string) { + var offset = 0; + var chunks = {}; + + while (offset < string.length) { + var id = string.substr(offset, 4); + var len = getStrLength(string, offset); + var data = string.substr(offset + 4 + 4, len); + offset += 4 + 4 + len; + chunks[id] = chunks[id] || []; + + if (id === 'RIFF' || id === 'LIST') { + chunks[id].push(parseRIFF(data)); + } else { + chunks[id].push(data); + } + } + return chunks; + } + + function doubleToString(num) { + return [].slice.call( + new Uint8Array((new Float64Array([num])).buffer), 0).map(function(e) { + return String.fromCharCode(e); + }).reverse().join(''); + } + + // a more abstract-ish API + + function WhammyVideo(duration) { + this.frames = []; + this.duration = duration || 1; + this.quality = 100; + } + + /** + * Pass Canvas or Context or image/webp(string) to {@link Whammy} encoder. + * @method + * @memberof Whammy + * @example + * recorder = new Whammy().Video(0.8, 100); + * recorder.add(canvas || context || 'image/webp'); + * @param {string} frame - Canvas || Context || image/webp + * @param {number} duration - Stick a duration (in milliseconds) + */ + WhammyVideo.prototype.add = function(frame, duration) { + if ('canvas' in frame) { //CanvasRenderingContext2D + frame = frame.canvas; + } + + if ('toDataURL' in frame) { + frame = frame.toDataURL('image/webp', this.quality); + } + + if (!(/^data:image\/webp;base64,/ig).test(frame)) { + throw 'Input must be formatted properly as a base64 encoded DataURI of type image/webp'; + } + this.frames.push({ + image: frame, + duration: duration || this.duration + }); + }; + + /** + * Encodes frames in WebM container. It invokes 'ArrayToWebM' method. + * @method + * @memberof Whammy + * @example + * recorder = new Whammy().Video(0.8, 100); + * var blob = recorder.compile(); + * @returns {Blob} blob - Encoded WebM blob; + */ + WhammyVideo.prototype.compile = function() { + return new ArrayToWebM(this.frames.map(function(frame) { + var webp = parseWebP(parseRIFF(atob(frame.image.slice(23)))); + webp.duration = frame.duration; + return webp; + })); + }; + + return { + /** + * A more abstract-ish API. + * @method + * @memberof Whammy + * @example + * recorder = new Whammy().Video(0.8, 100); + * @param {?number} speed - 0.8 + * @param {?number} quality - 100 + */ + Video: WhammyVideo, + + /** + * Encoding frames array into WebM container using WebP images. + * @method + * @memberof Whammy + * @inner + */ + ArrayToWebM: ArrayToWebM + }; +})(); diff --git a/RecordRTC/dev/WhammyRecorder.js b/RecordRTC/dev/WhammyRecorder.js new file mode 100644 index 00000000..38c76cd9 --- /dev/null +++ b/RecordRTC/dev/WhammyRecorder.js @@ -0,0 +1,238 @@ +// _________________ +// WhammyRecorder.js + +/** + * WhammyRecorder is a standalone class used by RecordRTC to bring video recording in Chrome. It runs top over {@link Whammy}. + * @summary Video recording feature in Chrome. + * @typedef WhammyRecorder + * @class + * @example + * var recorder = new WhammyRecorder(mediaStream); + * recorder.record(); + * recorder.stop(function(blob) { + * video.src = URL.createObjectURL(blob); + * }); + * @param {MediaStream} mediaStream - MediaStream object fetched using getUserMedia API or generated using captureStreamUntilEnded or WebAudio API. + */ + +function WhammyRecorder(mediaStream) { + /** + * This method records video. + * @method + * @memberof WhammyRecorder + * @example + * recorder.record(); + */ + this.record = function() { + if (!this.width) { + this.width = 320; + } + + if (!this.height) { + this.height = 240; + } + + if (!this.video) { + this.video = { + width: this.width, + height: this.height + }; + } + + if (!this.canvas) { + this.canvas = { + width: this.width, + height: this.height + }; + } + + canvas.width = this.canvas.width; + canvas.height = this.canvas.height; + + context = canvas.getContext('2d'); + + // setting defaults + if (this.video && this.video instanceof HTMLVideoElement) { + video = this.video.cloneNode(); + } else { + video = document.createElement('video'); + video.src = URL.createObjectURL(mediaStream); + + video.width = this.video.width; + video.height = this.video.height; + } + + video.muted = true; + video.play(); + + lastTime = new Date().getTime(); + whammy = new Whammy.Video(); + + console.log('canvas resolutions', canvas.width, '*', canvas.height); + console.log('video width/height', video.width || canvas.width, '*', video.height || canvas.height); + + drawFrames(); + }; + + function drawFrames() { + var duration = new Date().getTime() - lastTime; + if (!duration) { + return drawFrames(); + } + + // via #206, by Jack i.e. @Seymourr + lastTime = new Date().getTime(); + + context.drawImage(video, 0, 0, canvas.width, canvas.height); + whammy.frames.push({ + duration: duration, + image: canvas.toDataURL('image/webp') + }); + + if (!isStopDrawing) { + setTimeout(drawFrames, 10); + } + } + + /** + * remove black frames from the beginning to the specified frame + * @param {Array} _frames - array of frames to be checked + * @param {number} _framesToCheck - number of frame until check will be executed (-1 - will drop all frames until frame not matched will be found) + * @param {number} _pixTolerance - 0 - very strict (only black pixel color) ; 1 - all + * @param {number} _frameTolerance - 0 - very strict (only black frame color) ; 1 - all + * @returns {Array} - array of frames + */ + // pull#293 by @volodalexey + function dropBlackFrames(_frames, _framesToCheck, _pixTolerance, _frameTolerance) { + var localCanvas = document.createElement('canvas'); + localCanvas.width = canvas.width; + localCanvas.height = canvas.height; + var context2d = localCanvas.getContext('2d'); + var resultFrames = []; + + var checkUntilNotBlack = _framesToCheck === -1; + var endCheckFrame = (_framesToCheck && _framesToCheck > 0 && _framesToCheck <= _frames.length) ? + _framesToCheck : _frames.length; + var sampleColor = { + r: 0, + g: 0, + b: 0 + }; + var maxColorDifference = Math.sqrt( + Math.pow(255, 2) + + Math.pow(255, 2) + + Math.pow(255, 2) + ); + var pixTolerance = _pixTolerance && _pixTolerance >= 0 && _pixTolerance <= 1 ? _pixTolerance : 0; + var frameTolerance = _frameTolerance && _frameTolerance >= 0 && _frameTolerance <= 1 ? _frameTolerance : 0; + var doNotCheckNext = false; + + for (var f = 0; f < endCheckFrame; f++) { + var matchPixCount, endPixCheck, maxPixCount; + + if (!doNotCheckNext) { + var image = new Image(); + image.src = _frames[f].image; + context2d.drawImage(image, 0, 0, canvas.width, canvas.height); + var imageData = context2d.getImageData(0, 0, canvas.width, canvas.height); + matchPixCount = 0; + endPixCheck = imageData.data.length; + maxPixCount = imageData.data.length / 4; + + for (var pix = 0; pix < endPixCheck; pix += 4) { + var currentColor = { + r: imageData.data[pix], + g: imageData.data[pix + 1], + b: imageData.data[pix + 2] + }; + var colorDifference = Math.sqrt( + Math.pow(currentColor.r - sampleColor.r, 2) + + Math.pow(currentColor.g - sampleColor.g, 2) + + Math.pow(currentColor.b - sampleColor.b, 2) + ); + // difference in color it is difference in color vectors (r1,g1,b1) <=> (r2,g2,b2) + if (colorDifference <= maxColorDifference * pixTolerance) { + matchPixCount++; + } + } + } + + if (!doNotCheckNext && maxPixCount - matchPixCount <= maxPixCount * frameTolerance) { + // console.log('removed black frame : ' + f + ' ; frame duration ' + _frames[f].duration); + } else { + // console.log('frame is passed : ' + f); + if (checkUntilNotBlack) { + doNotCheckNext = true; + } + resultFrames.push(_frames[f]); + } + } + + resultFrames = resultFrames.concat(_frames.slice(endCheckFrame)); + + if (resultFrames.length <= 0) { + // at least one last frame should be available for next manipulation + // if total duration of all frames will be < 1000 than ffmpeg doesn't work well... + resultFrames.push(_frames[_frames.length - 1]); + } + + return resultFrames; + } + + var isStopDrawing = false; + + /** + * This method stops recording video. + * @param {function} callback - Callback function, that is used to pass recorded blob back to the callee. + * @method + * @memberof WhammyRecorder + * @example + * recorder.stop(function(blob) { + * video.src = URL.createObjectURL(blob); + * }); + */ + this.stop = function(callback) { + isStopDrawing = true; + + var _this = this; + // analyse of all frames takes some time! + setTimeout(function() { + // e.g. dropBlackFrames(frames, 10, 1, 1) - will cut all 10 frames + // e.g. dropBlackFrames(frames, 10, 0.5, 0.5) - will analyse 10 frames + // e.g. dropBlackFrames(frames, 10) === dropBlackFrames(frames, 10, 0, 0) - will analyse 10 frames with strict black color + whammy.frames = dropBlackFrames(whammy.frames, -1); + + // to display advertisement images! + if (this.advertisement && this.advertisement.length) { + whammy.frames = this.advertisement.concat(whammy.frames); + } + + /** + * @property {Blob} blob - Recorded frames in video/webm blob. + * @memberof WhammyRecorder + * @example + * recorder.stop(function() { + * var blob = recorder.blob; + * }); + */ + _this.blob = whammy.compile(); + + if (_this.blob.forEach) { + _this.blob = new Blob([], { + type: 'video/webm' + }); + } + + if (callback) { + callback(_this.blob); + } + }, 10); + }; + + var canvas = document.createElement('canvas'); + var context = canvas.getContext('2d'); + + var video; + var lastTime; + var whammy; +} diff --git a/RecordRTC/dev/head.js b/RecordRTC/dev/head.js new file mode 100644 index 00000000..3674e3f5 --- /dev/null +++ b/RecordRTC/dev/head.js @@ -0,0 +1,48 @@ +// Last time updated at Nov 19, 2014, 08:32:23 + +// links: +// Open-Sourced: https://github.com/muaz-khan/RecordRTC +// http://cdn.WebRTC-Experiment.com/RecordRTC.js +// http://www.WebRTC-Experiment.com/RecordRTC.js (for China users) +// http://RecordRTC.org/latest.js (for China users) +// npm install recordrtc +// http://recordrtc.org/ + +// updates? +/* +-. Fixed MRecordRTC. +-. Added functionality for analyse black frames and cut them - pull#293 +-. if you're recording GIF, you must link: https://cdn.webrtc-experiment.com/gif-recorder.js +*/ + +//------------------------------------ + +// Browsers Support:: +// Chrome (all versions) [ audio/video separately ] +// Firefox ( >= 29 ) [ audio/video in single webm/mp4 container or only audio in ogg ] +// Opera (all versions) [ same as chrome ] +// Android (Chrome) [ only video ] +// Android (Opera) [ only video ] +// Android (Firefox) [ only video ] + +//------------------------------------ +// Muaz Khan - www.MuazKhan.com +// MIT License - www.WebRTC-Experiment.com/licence +//------------------------------------ +// Note: RecordRTC.js is using 3 other libraries; you need to accept their licences as well. +//------------------------------------ +// 1. RecordRTC.js +// 2. MRecordRTC.js +// 3. Cross-Browser-Declarations.js +// 4. Storage.js +// 5. MediaStreamRecorder.js +// 6. StereoRecorder.js +// 7. StereoAudioRecorder.js +// 8. CanvasRecorder.js +// 9. WhammyRecorder.js +// 10. Whammy.js +// 11. DiskStorage.js +// 12. GifRecorder.js +//------------------------------------ + +'use strict'; diff --git a/RecordRTC/index.html b/RecordRTC/index.html index af7f6356..347475fc 100644 --- a/RecordRTC/index.html +++ b/RecordRTC/index.html @@ -1,4 +1,4 @@ - - - RecordRTC: WebRTC audio/video recording ® Muaz Khan - - - - - - - - - + + + + + + + + +
+
+

+ RecordRTC: WebRTC audio/video recording ® + Muaz Khan +

+

+ HOME + © + Muaz Khan . + @WebRTCWeb . + Github . + Latest issues . + What's New? +

+
+ +
+ +
+

+ Record Audio ( www.RecordRTC.org ) +

+ +
+ + + +

+
+
+ +
+ +

Record Video (in chrome)

+ + / Record video on Firefox + + +
+ + + +
+ + +
+
+ + +
+

+
+ + +
+ +
+
+
+ +
+ +

Record video as animated gif image (both Chrome/Firefox):

+ +
+ + +
+
+ +
+

Try Audio+Video Recording on Firefox +

+
    +
  1. ConcatenateBlobs.js - Concatenate multiple recordings in single Blob!
  2. +
  3. RecordRTC / PHP / FFmpeg (synced audio/video in single file!)
  4. +
  5. RecordRTC-to-Nodejs (used ffmpeg to merge wav/webm in single WebM container)
  6. +
  7. RecordRTC-to-PHP (audio/video recording and uploading to server)
  8. +
  9. RecordRTC-to-ASP.NET MVC (audio/video recording and uploading to server)
  10. +
  11. Canvas Recording! (webpage recording)
  12. +
  13. MRecordRTC and writeToDisk/getFromDisk! +
  14. +
  15. RecordRTC-to-Socket.io (used ffmpeg to merge wav/webm in single WebM container)
  16. +
  17. RecordRTC and ffmpeg-asm.js (ffmpeg inside the browser!)
  18. +
+
+ + - - - - - - - -
-
-

- RecordRTC: WebRTC audio/video recording ® - Muaz Khan -

-

- HOME - © - Muaz Khan - - . - @WebRTCWeb - - . - Github - - . - Latest issues - - . - What's New? -

-
- -
- -
-

- Record Audio ( www.RecordRTC.org ) -

- -
- - - -

-
-
- -
- -

Record Video (in chrome)

- - / Record video on Firefox - -
- - - -
- - -
-
- - -
-


- -
- -
-
-
- -
- -

Record video as animated gif image (both Chrome/Firefox):

- -
- - -
-
- -
-

Try Audio+Video Recording on Firefox

-
    -
  1. ConcatenateBlobs.js - Concatenate multiple recordings in single Blob!
  2. -
  3. RecordRTC / PHP / FFmpeg (synced audio/video in single file!)
  4. -
  5. RecordRTC-to-Nodejs (used ffmpeg to merge wav/webm in single WebM container)
  6. -
  7. RecordRTC-to-PHP (audio/video recording and uploading to server)
  8. -
  9. RecordRTC-to-ASP.NET MVC (audio/video recording and uploading to server)
  10. -
  11. Canvas Recording! (webpage recording)
  12. -
  13. MRecordRTC and writeToDisk/getFromDisk!
  14. -
  15. RecordRTC-to-Socket.io (used ffmpeg to merge wav/webm in single WebM container)
  16. -
  17. RecordRTC and ffmpeg-asm.js (ffmpeg inside the browser!)
  18. -
-
- - - stopRecordingGIF.onclick = function() { - this.disabled = true; - recordGIF.disabled = false; +
+

Using RecordRTC...

+

+ You can record audio in wav/ogg file format; and video in either webm format or as animated GIF image.

+
+ +
+

Technical Guide

+
    +
  1. + Chrome allows getUserMedia invocation on majority of non-file protocols e.g. HTTP/HTTPS/ or inside chrome extension pages. Though, there is always options to use CL (command-line) flags to support file protocols. +
  2. + +
  3. + ( + In Chrome) RecordRTC captures video frames via Canvas2D; which is encoded in webp-DataURL; now it is using a library named “weppy†which encodes webp into webm. +
  4. + +
  5. + ( + In Firefox) RecordRTC is using MediaRecorder API; which supports both audio/video recordings, both in single and multiple files. +
  6. + +
  7. + ( + In Chrome) RecordRTC is using WebAudio API for audio-recording. Such API has many issues e.g. unable to capture mono audio in wav format; unable to capture remote audio; failure on XP SP2; etc. +
  8. + +
  9. + ( + In Chrome) If you’re using notebook’s built-in audio input device for audio recording; then you may get "blank" blob. +
  10. + +
  11. + ( + In Chrome) RecordRTC is incapable to record audio/video in a single file; however there is ffmpeg merging solution available on Github repository. +
  12. +
+
+ +
+

+ How to use RecordRTC?

+
+                <script src="//cdn.WebRTC-Experiment.com/RecordRTC.js"></script>
+            
+
+
+

+ How to record audio using RecordRTC?

+
+                var recordRTC = RecordRTC(mediaStream); recordRTC.
+                startRecording(); recordRTC.
+                stopRecording(function(
+                audioURL) { mediaElement.src = audioURL; });
+            
+ +
+ + +
+

+ How to record video using RecordRTC?

+
+                var options = {
+                type: 'video',
+                video: { width: 320, height: 240 },
+                canvas: { width: 320, height: 240 } }; var recordRTC = RecordRTC(mediaStream, options); recordRTC.
+                startRecording(); recordRTC.
+                stopRecording(function(
+                videoURL) { mediaElement.src = videoURL; });
+            
+ +
+ +
+ +

+ How to record animated GIF using RecordRTC?

+
+                // you must link: // https://cdn.webrtc-experiment.com/gif-recorder.js var options = {
+                type: 'gif',
+                video: { width: 320, height: 240 },
+                canvas: { width: 320, height: 240 },
+                frameRate: 200,
+                quality: 10 }; var recordRTC = RecordRTC(mediaStream, options); recordRTC.
+                startRecording(); recordRTC.
+                stopRecording(function(
+                gifURL) { mediaElement.src = gifURL; });
+            
+
+ +
+

+ Possible - if (recorder) - recorder.stopRecording(function(url) { - document.getElementById('video-url-preview').innerHTML = 'Recorded Gif URL'; - }); - }; - - - - -
-

Using RecordRTC...

-

- You can record audio in wav/ogg file format; and video in either webm format or - as animated GIF image.

-
- -
-

Technical Guide

-
    -
  1. - Chrome allows getUserMedia invocation on majority of non-file protocols e.g. HTTP/HTTPS/ or inside chrome extension pages. Though, there is always options to use CL (command-line) flags to support file protocols. -
  2. - -
  3. - (In Chrome) RecordRTC captures video frames via Canvas2D; which is encoded in webp-DataURL; now it is using a library named “weppy†which encodes webp into webm. -
  4. - -
  5. - (In Firefox) RecordRTC is using MediaRecorder API; which supports both audio/video recordings, both in single and multiple files. -
  6. - -
  7. - (In Chrome) RecordRTC is using WebAudio API for audio-recording. Such API has many issues e.g. unable to capture mono audio in wav format; unable to capture remote audio; failure on XP SP2; etc. -
  8. - -
  9. - (In Chrome) If you’re using notebook’s built-in audio input device for audio recording; then you may get "blank" blob. -
  10. - -
  11. - (In Chrome) RecordRTC is incapable to record audio/video in a single file; however there is ffmpeg merging solution available on Github repository. -
  12. -
-
- -
-

- How to use RecordRTC?

-
-<script src="//cdn.WebRTC-Experiment.com/RecordRTC.js"></script>
-
-
-
-

- How to record audio using RecordRTC?

-
-var recordRTC = RecordRTC(mediaStream);
-recordRTC.startRecording();
-recordRTC.stopRecording(function(audioURL) {
-   mediaElement.src = audioURL;
-});
-
- -
- - -
-

- How to record video using RecordRTC?

-
-var options = {
-   type: 'video',
-   video: {
-      width: 320,
-      height: 240
-   },
-   canvas: {
-      width: 320,
-      height: 240
-   }
-};
-var recordRTC = RecordRTC(mediaStream, options);
-recordRTC.startRecording();
-recordRTC.stopRecording(function(videoURL) {
-   mediaElement.src = videoURL;
-});
-
- -
- -
- -

- How to record animated GIF using RecordRTC?

-
-// you must link:
-// https://cdn.webrtc-experiment.com/gif-recorder.js
-
-var options = {
-   type: 'gif',
-   video: {
-      width: 320,
-      height: 240
-   },
-   canvas: {
-      width: 320,
-      height: 240
-   },
-   frameRate: 200,
-   quality: 10
-};
-var recordRTC = RecordRTC(mediaStream, options);
-recordRTC.startRecording();
-recordRTC.stopRecording(function(gifURL) {
-   mediaElement.src = gifURL;
-});
-
-
- -
-

- Possible issues/failures: -

-

- (In Chrome) The biggest issue is that RecordRTC is unable to record both audio and video streams in single file.

- Do you know "RecordRTC" fails recording audio because following conditions fails (applies only to chrome): -

    -
  1. Sample rate and channel configuration must be the same for input and output sides - on Windows i.e. audio input/output devices must match
  2. -
  3. Only the Default microphone device can be used for capturing.
  4. -
  5. The requesting scheme is must be one of the following: http, https, chrome, extension's, - or file (only works with --allow-file-access-from-files)
  6. -
  7. The browser must be able to create/initialize the metadata database for the API - under the profile directory
  8. -
-

-
-
-

- RecordRTC is MIT licensed on Github! Documentation -

-
- -
-

Feedback

-
- -
- Enter your email too; if you want "direct" reply! -
- -
-

Latest Updates

-
-
-

- - - -
+

- WebRTC Experiments - © - muazkh@gmail.com + ( + In Chrome) The biggest issue is that RecordRTC is + unable to record both audio and video streams in single file. +
+
Do you know "RecordRTC" fails recording audio because following conditions fails ( + applies only to chrome): +

    +
  1. Sample rate and channel configuration must be the same for input and output sides on Windows i.e. audio input/output devices must match
  2. +
  3. Only the Default microphone device can be used for capturing.
  4. +
  5. The requesting scheme is must be one of the following: http, https, chrome, extension's, or file (only works with --allow-file-access-from-files)
  6. +
  7. The browser must be able to create/initialize the metadata database for the API under the profile directory
  8. +
+

+ +
+

+ RecordRTC is MIT licensed on Github! Documentation

-
- - - - - + + +
+

Feedback

+
+ +
+ + Enter your email too; if you want "direct" reply! +
+ +
+

Latest Updates +

+
+
+
+ + + + + + + + + + \ No newline at end of file diff --git a/RecordRTC/package.json b/RecordRTC/package.json index 1b73f822..dbe6cd19 100644 --- a/RecordRTC/package.json +++ b/RecordRTC/package.json @@ -1,7 +1,7 @@ { "name": "recordrtc", "preferGlobal": false, - "version": "5.0.1", + "version": "5.0.4", "author": { "name": "Muaz Khan", "email": "muazkh@gmail.com", @@ -33,5 +33,17 @@ }, "homepage": "http://RecordRTC.org/", "_id": "recordrtc@", - "_from": "recordrtc@" + "_from": "recordrtc@", + "devDependencies": { + "grunt": "latest", + "grunt-cli": "latest", + "load-grunt-tasks": "latest", + "grunt-contrib-concat": "latest", + "grunt-contrib-csslint": "latest", + "grunt-contrib-jshint": "latest", + "grunt-contrib-uglify": "latest", + "grunt-htmlhint": "latest", + "grunt-jsbeautifier": "latest", + "grunt-bump": "latest" + } } diff --git a/Reliable-Signaler/README.md b/Reliable-Signaler/README.md new file mode 100644 index 00000000..e3fb8fc1 --- /dev/null +++ b/Reliable-Signaler/README.md @@ -0,0 +1,159 @@ +# Reliable Signaler [![npm](https://img.shields.io/npm/v/reliable-signaler.svg)](https://npmjs.org/package/reliable-signaler) [![downloads](https://img.shields.io/npm/dm/reliable-signaler.svg)](https://npmjs.org/package/reliable-signaler) + +It is a node.js and socket.io based reliable signaling implementation. Remember, reliable doesn't mean "scalable"; reliable simply means that it auto reconnects in any kind of failure or internet disconnect. It is having following features: + +1. Auto reconnects if node.js gets down out of any reason. +2. Auto reconnects if internet connection disconnects. +3. It provides [custom-signaling](https://github.com/muaz-khan/RTCMultiConnection/wiki/Custom-Private-Servers#signaling-servers) for your [RTCMultiConnection](https://github.com/muaz-khan/RTCMultiConnection) applications! + +``` +npm install reliable-signaler +``` + +# Demos + +* https://www.npmjs.org/package/rtcmulticonnection-client +* https://www.npmjs.org/package/datachannel-client + +``` +# install rtcmulticonnection-client +npm install rtcmulticonnection-client +node ./node_modules/rtcmulticonnection-client/server.js + +# or intall datachannel-client +npm install datachannel-client +node ./node_modules/datachannel-client/server.js +``` + +Now open localhost port:`8080`. + +# 1st Step: Node.js Server + +To use it in your node.js code: (required) + +```javascript +require('reliable-signaler')(httpServer || expressServer || portNumber); +``` + +# 2nd Step: Browser-side code + +Note: Below code targets: [rtcmulticonnection-client](https://www.npmjs.org/package/rtcmulticonnection-client) + +To use it in the browser: (required) + +```htm + +``` + +> Remember, `/reliable-signaler/rmc-signaler.js` route is auto opened by this implementation. If you passed `portNumber` instead of `httpServer` then you need to provide absolute URL instead of static one i.e. `http://localhost:port/reliable-signaler/rmc-signaler.js` + +And your RTCMultiConnection code: + +```javascript +var connection = new RTCMultiConnection(); + +// invoke "initRMCSignaler" and pass "connection" object +var rmcSignaler = initRMCSignaler(connection); + +// or to pass "socket-URL" +var rmcSignaler = initRMCSignaler({ + connection: connection, + socketURL: 'http://domain:port/' +}); +``` + +Call `openNewSession` method as soon as you'll call `open` method: + +```javascript +connection.open(); +rmcSignaler.openNewSession(); + +// or: +connection.open({ + onMediaCaptured: rmcSignaler.openNewSession +}); +``` + +For participants, call `joinSession` method: + +```javascript +rmcSignaler.joinSession('sessioin-id', function(sessionDescription) { + connection.join(sessionDescription); // invoke "join" in callback +}); +``` + +# Complete Client-Side Example + +```html + + +``` + +## API Reference + +Constructor takes either `RTCMultiConnection` instance or a `config` object: + +```javascript +# 1st option: Pass RTCMultiConnection object +var rmcSignaler = initRMCSignaler(rtcMultiConnection); + +# 2nd option: Pass "config" object +var rmcSignaler = initRMCSignaler({ + connection: rtcMultiConnection, + socketURL: '/' +}); +``` + +`initRMCSignaler` global-function exposes/returns 3-objects: + +1. `socket` +2. `openNewSession` +3. `joinSession` + +```javascript +// "socket" object +rmcSignaler.socket.emit('message', 'hello'); + +// "openNewSession" method +rmcSignaler.openNewSession(connection.sessionDescription || null); + +// "joinSession" object +rmcSignaler.joinSession('sessioin-id', callback); +``` + +## `openNewSession` + +This method simply takes `connection.sessionDescription` object and stores in node.js server. + +## `joinSession` + +This method looks for active `sessioin-id` in node.js server. Node.js server will fire callback only when session is found. + +If session is absent, then node.js server will wait until someone opens that session; and node.js will fire `joinSession-callback` as soon a session is opened. + +## License + +[Reliable-Signaler](https://github.com/muaz-khan/Reliable-Signaler) is released under [MIT licence](https://www.webrtc-experiment.com/licence/) . Copyright (c) [Muaz Khan](https://plus.google.com/+MuazKhan). diff --git a/Reliable-Signaler/datachannel-client/DataChannel.js b/Reliable-Signaler/datachannel-client/DataChannel.js new file mode 100644 index 00000000..aafb6a4e --- /dev/null +++ b/Reliable-Signaler/datachannel-client/DataChannel.js @@ -0,0 +1,1197 @@ +// Last time updated at Nov 21, 2014, 08:32:23 + +// Muaz Khan - www.MuazKhan.com +// MIT License - www.WebRTC-Experiment.com/licence +// Documentation - github.com/muaz-khan/WebRTC-Experiment/tree/master/DataChannel +// ______________ +// DataChannel.js + +(function () { + window.DataChannel = function (channel, extras) { + if (channel) this.automatic = true; + this.channel = channel || location.href.replace(/\/|:|#|%|\.|\[|\]/g, ''); + + extras = extras || {}; + + var self = this, + dataConnector, fileReceiver, textReceiver; + + this.onmessage = function (message, userid) { + console.debug(userid, 'sent message:', message); + }; + + this.channels = {}; + this.onopen = function (userid) { + console.debug(userid, 'is connected with you.'); + }; + + this.onclose = function (event) { + console.error('data channel closed:', event); + }; + + this.onerror = function (event) { + console.error('data channel error:', event); + }; + + // by default; received file will be auto-saved to disk + this.autoSaveToDisk = true; + this.onFileReceived = function (fileName) { + console.debug('File <', fileName, '> received successfully.'); + }; + + this.onFileSent = function (file) { + console.debug('File <', file.name, '> sent successfully.'); + }; + + this.onFileProgress = function (packets) { + console.debug('<', packets.remaining, '> items remaining.'); + }; + + function prepareInit(callback) { + for (var extra in extras) { + self[extra] = extras[extra]; + } + self.direction = self.direction || 'many-to-many'; + if (self.userid) window.userid = self.userid; + + if (!self.openSignalingChannel) { + if (typeof self.transmitRoomOnce == 'undefined') self.transmitRoomOnce = true; + + // socket.io over node.js: https://github.com/muaz-khan/WebRTC-Experiment/blob/master/Signaling.md + self.openSignalingChannel = function (config) { + config = config || {}; + + channel = config.channel || self.channel || 'default-channel'; + var socket = new window.Firebase('https://' + (self.firebase || 'chat') + '.firebaseIO.com/' + channel); + socket.channel = channel; + + socket.on('child_added', function (data) { + config.onmessage(data.val()); + }); + + socket.send = function (data) { + this.push(data); + }; + + if (!self.socket) self.socket = socket; + if (channel != self.channel || (self.isInitiator && channel == self.channel)) + socket.onDisconnect().remove(); + + if (config.onopen) setTimeout(config.onopen, 1); + + return socket; + }; + + if (!window.Firebase) { + var script = document.createElement('script'); + script.src = 'https://www.webrtc-experiment.com/firebase.js'; + script.onload = callback; + document.documentElement.appendChild(script); + } else callback(); + } else callback(); + } + + function init() { + if (self.config) return; + + self.config = { + ondatachannel: function (room) { + if (!dataConnector) { + self.room = room; + return; + } + + var tempRoom = { + id: room.roomToken, + owner: room.broadcaster + }; + + if (self.ondatachannel) return self.ondatachannel(tempRoom); + + if (self.joinedARoom) return; + self.joinedARoom = true; + + self.join(tempRoom); + }, + onopen: function (userid, _channel) { + self.onopen(userid, _channel); + self.channels[userid] = { + channel: _channel, + send: function (data) { + self.send(data, this.channel); + } + }; + }, + onmessage: function (data, userid) { + if (IsDataChannelSupported && !data.size) data = JSON.parse(data); + + if (!IsDataChannelSupported) { + if (data.userid === window.userid) return; + data = data.message; + } + + if (data.type === 'text') + textReceiver.receive(data, self.onmessage, userid); + + else if (typeof data.maxChunks != 'undefined') + fileReceiver.receive(data, self); + + else self.onmessage(data, userid); + }, + onclose: function (event) { + var myChannels = self.channels, + closedChannel = event.currentTarget; + + for (var userid in myChannels) { + if (closedChannel === myChannels[userid].channel) { + delete myChannels[userid]; + } + } + + self.onclose(event); + } + }; + + dataConnector = IsDataChannelSupported ? + new DataConnector(self, self.config) : + new SocketConnector(self.channel, self.config); + + fileReceiver = new FileReceiver(self); + textReceiver = new TextReceiver(self); + + if (self.room) self.config.ondatachannel(self.room); + } + + this.open = function (_channel) { + self.joinedARoom = true; + + if (self.socket) self.socket.onDisconnect().remove(); + else self.isInitiator = true; + + if (_channel) self.channel = _channel; + + prepareInit(function () { + init(); + if (IsDataChannelSupported) dataConnector.createRoom(_channel); + }); + }; + + this.connect = function (_channel) { + if (_channel) self.channel = _channel; + prepareInit(init); + }; + + // manually join a room + this.join = function (room) { + if (!room.id || !room.owner) { + throw 'Invalid room info passed.'; + } + + dataConnector.joinRoom({ + roomToken: room.id, + joinUser: room.owner + }); + }; + + this.send = function (data, _channel) { + if (!data) throw 'No file, data or text message to share.'; + if (typeof data.size != 'undefined' && typeof data.type != 'undefined') { + FileSender.send({ + file: data, + channel: dataConnector, + onFileSent: function (file) { + self.onFileSent(file); + }, + onFileProgress: function (packets, uuid) { + self.onFileProgress(packets, uuid); + }, + + _channel: _channel, + root: self + }); + } else { + TextSender.send({ + text: data, + channel: dataConnector, + _channel: _channel, + root: self + }); + } + }; + + this.onleave = function (userid) { + console.debug(userid, 'left!'); + }; + + this.leave = this.eject = function (userid) { + dataConnector.leave(userid, self.autoCloseEntireSession); + }; + + this.openNewSession = function (isOpenNewSession, isNonFirebaseClient) { + if (isOpenNewSession) { + if (self.isNewSessionOpened) return; + self.isNewSessionOpened = true; + + if (!self.joinedARoom) self.open(); + } + + if (!isOpenNewSession || isNonFirebaseClient) self.connect(); + + // for non-firebase clients + if (isNonFirebaseClient) + setTimeout(function () { + self.openNewSession(true); + }, 5000); + }; + + if (typeof this.preferSCTP == 'undefined') { + this.preferSCTP = isFirefox || chromeVersion >= 32 ? true : false; + } + + if (typeof this.chunkSize == 'undefined') { + this.chunkSize = isFirefox || chromeVersion >= 32 ? 13 * 1000 : 1000; // 1000 chars for RTP and 13000 chars for SCTP + } + + if (typeof this.chunkInterval == 'undefined') { + this.chunkInterval = isFirefox || chromeVersion >= 32 ? 100 : 500; // 500ms for RTP and 100ms for SCTP + } + + if (self.automatic) { + if (window.Firebase) { + console.debug('checking presence of the room..'); + new window.Firebase('https://' + (extras.firebase || self.firebase || 'chat') + '.firebaseIO.com/' + self.channel).once('value', function (data) { + console.debug('room is present?', data.val() != null); + self.openNewSession(data.val() == null); + }); + } else self.openNewSession(false, true); + } + }; + + function DataConnector(root, config) { + var self = {}; + var that = this; + + self.userToken = root.userid = root.userid || uniqueToken(); + self.sockets = []; + self.socketObjects = {}; + + var channels = '--', + isbroadcaster, isGetNewRoom = true, + RTCDataChannels = []; + + function newPrivateSocket(_config) { + var socketConfig = { + channel: _config.channel, + onmessage: socketResponse, + onopen: function () { + if (isofferer && !peer) initPeer(); + + _config.socketIndex = socket.index = self.sockets.length; + self.socketObjects[socketConfig.channel] = socket; + self.sockets[_config.socketIndex] = socket; + } + }; + + socketConfig.callback = function (_socket) { + socket = _socket; + socketConfig.onopen(); + }; + + var socket = root.openSignalingChannel(socketConfig), + isofferer = _config.isofferer, + gotstream, inner = {}, peer; + + var peerConfig = { + onICE: function (candidate) { + socket && socket.send({ + userToken: self.userToken, + candidate: { + sdpMLineIndex: candidate.sdpMLineIndex, + candidate: JSON.stringify(candidate.candidate) + } + }); + }, + onopen: onChannelOpened, + onmessage: function (event) { + config.onmessage(event.data, _config.userid); + }, + onclose: config.onclose, + onerror: root.onerror, + preferSCTP: root.preferSCTP + }; + + function initPeer(offerSDP) { + if (root.direction === 'one-to-one' && window.isFirstConnectionOpened) return; + + if (!offerSDP) peerConfig.onOfferSDP = sendsdp; + else { + peerConfig.offerSDP = offerSDP; + peerConfig.onAnswerSDP = sendsdp; + } + + peer = RTCPeerConnection(peerConfig); + } + + function onChannelOpened(channel) { + channel.peer = peer.peer; + RTCDataChannels.push(channel); + + config.onopen(_config.userid, channel); + + if (root.direction === 'many-to-many' && isbroadcaster && channels.split('--').length > 3) { + defaultSocket && defaultSocket.send({ + newParticipant: socket.channel, + userToken: self.userToken + }); + } + + window.isFirstConnectionOpened = gotstream = true; + } + + function sendsdp(sdp) { + sdp = JSON.stringify(sdp); + var part = parseInt(sdp.length / 3); + + var firstPart = sdp.slice(0, part), + secondPart = sdp.slice(part, sdp.length - 1), + thirdPart = ''; + + if (sdp.length > part + part) { + secondPart = sdp.slice(part, part + part); + thirdPart = sdp.slice(part + part, sdp.length); + } + + socket.send({ + userToken: self.userToken, + firstPart: firstPart + }); + + socket.send({ + userToken: self.userToken, + secondPart: secondPart + }); + + socket.send({ + userToken: self.userToken, + thirdPart: thirdPart + }); + } + + function socketResponse(response) { + if (response.userToken == self.userToken) return; + + if (response.firstPart || response.secondPart || response.thirdPart) { + if (response.firstPart) { + // sdp sender's user id passed over "onopen" method + _config.userid = response.userToken; + + inner.firstPart = response.firstPart; + if (inner.secondPart && inner.thirdPart) selfInvoker(); + } + if (response.secondPart) { + inner.secondPart = response.secondPart; + if (inner.firstPart && inner.thirdPart) selfInvoker(); + } + + if (response.thirdPart) { + inner.thirdPart = response.thirdPart; + if (inner.firstPart && inner.secondPart) selfInvoker(); + } + } + + if (response.candidate && !gotstream) { + peer && peer.addICE({ + sdpMLineIndex: response.candidate.sdpMLineIndex, + candidate: JSON.parse(response.candidate.candidate) + }); + + console.debug('ice candidate', response.candidate.candidate); + } + + if (response.left) { + if (peer && peer.peer) { + peer.peer.close(); + peer.peer = null; + } + + if (response.closeEntireSession) leaveChannels(); + else if (socket) { + socket.send({ + left: true, + userToken: self.userToken + }); + socket = null; + } + + root.onleave(response.userToken); + } + + if (response.playRoleOfBroadcaster) + setTimeout(function () { + self.roomToken = response.roomToken; + root.open(self.roomToken); + self.sockets = swap(self.sockets); + }, 600); + } + + var invokedOnce = false; + + function selfInvoker() { + if (invokedOnce) return; + + invokedOnce = true; + inner.sdp = JSON.parse(inner.firstPart + inner.secondPart + inner.thirdPart); + + if (isofferer) peer.addAnswerSDP(inner.sdp); + else initPeer(inner.sdp); + + console.debug('sdp', inner.sdp.sdp); + } + } + + function onNewParticipant(channel) { + if (!channel || channels.indexOf(channel) != -1 || channel == self.userToken) return; + channels += channel + '--'; + + var new_channel = uniqueToken(); + + newPrivateSocket({ + channel: new_channel, + closeSocket: true + }); + + defaultSocket && defaultSocket.send({ + participant: true, + userToken: self.userToken, + joinUser: channel, + channel: new_channel + }); + } + + function uniqueToken() { + return Math.round(Math.random() * 60535) + 5000000; + } + + function leaveChannels(channel) { + var alert = { + left: true, + userToken: self.userToken + }; + + // if room initiator is leaving the room; close the entire session + if (isbroadcaster) { + if (root.autoCloseEntireSession) alert.closeEntireSession = true; + else + self.sockets[0].send({ + playRoleOfBroadcaster: true, + userToken: self.userToken, + roomToken: self.roomToken + }); + } + + if (!channel) { + // closing all sockets + var sockets = self.sockets, + length = sockets.length; + + for (var i = 0; i < length; i++) { + var socket = sockets[i]; + if (socket) { + socket.send(alert); + + if (self.socketObjects[socket.channel]) + delete self.socketObjects[socket.channel]; + + delete sockets[i]; + } + } + + that.left = true; + } + + // eject a specific user! + if (channel) { + socket = self.socketObjects[channel]; + if (socket) { + socket.send(alert); + + if (self.sockets[socket.index]) + delete self.sockets[socket.index]; + + delete self.socketObjects[channel]; + } + } + self.sockets = swap(self.sockets); + } + + window.addEventListener('beforeunload', function () { + leaveChannels(); + }, false); + + window.addEventListener('keydown', function (e) { + if (e.keyCode == 116) + leaveChannels(); + }, false); + + var defaultSocket = root.openSignalingChannel({ + onmessage: function (response) { + if (response.userToken == self.userToken) return; + if (isGetNewRoom && response.roomToken && response.broadcaster) config.ondatachannel(response); + + if (response.newParticipant) onNewParticipant(response.newParticipant); + + if (response.userToken && response.joinUser == self.userToken && response.participant && channels.indexOf(response.userToken) == -1) { + channels += response.userToken + '--'; + + console.debug('Data connection is being opened between you and', response.userToken || response.channel); + newPrivateSocket({ + isofferer: true, + channel: response.channel || response.userToken, + closeSocket: true + }); + } + }, + callback: function (socket) { + defaultSocket = socket; + } + }); + + return { + createRoom: function (roomToken) { + self.roomToken = roomToken || uniqueToken(); + + isbroadcaster = true; + isGetNewRoom = false; + + (function transmit() { + defaultSocket && defaultSocket.send({ + roomToken: self.roomToken, + broadcaster: self.userToken + }); + + if (!root.transmitRoomOnce && !that.leaving) { + if (root.direction === 'one-to-one') { + if (!window.isFirstConnectionOpened) setTimeout(transmit, 3000); + } else setTimeout(transmit, 3000); + } + })(); + }, + joinRoom: function (_config) { + self.roomToken = _config.roomToken; + isGetNewRoom = false; + + newPrivateSocket({ + channel: self.userToken + }); + + defaultSocket.send({ + participant: true, + userToken: self.userToken, + joinUser: _config.joinUser + }); + }, + send: function (message, _channel) { + var _channels = RTCDataChannels, + data, length = _channels.length; + if (!length) return; + + data = JSON.stringify(message); + + if (_channel) { + if (_channel.readyState == 'open') { + _channel.send(data); + } + } + else + for (var i = 0; i < length; i++) { + if (_channels[i].readyState == 'open') { + _channels[i].send(data); + }; + } + }, + leave: function (userid, autoCloseEntireSession) { + if (autoCloseEntireSession) root.autoCloseEntireSession = true; + leaveChannels(userid); + if (!userid) { + self.joinedARoom = isbroadcaster = false; + isGetNewRoom = true; + } + } + }; + } + + function SocketConnector(_channel, config) { + var channel = config.openSocket({ + channel: _channel, + onopen: config.onopen, + onmessage: config.onmessage + }); + + return { + send: function (message) { + channel && channel.send({ + userid: userid, + message: message + }); + } + }; + } + + function getRandomString() { + return (Math.random() * new Date().getTime()).toString(36).replace(/\./g, '-'); + } + + window.userid = getRandomString(); + + var isMobileDevice = navigator.userAgent.match(/Android|iPhone|iPad|iPod|BlackBerry|IEMobile/i); + var isChrome = !!navigator.webkitGetUserMedia; + var isFirefox = !!navigator.mozGetUserMedia; + var chromeVersion = !!navigator.mozGetUserMedia ? 0 : parseInt(navigator.userAgent.match(/Chrom(e|ium)\/([0-9]+)\./)[2]); + + var FileSender = { + send: function (config) { + var root = config.root; + var channel = config.channel; + var privateChannel = config._channel; + var file = config.file; + + if (!config.file) { + console.error('You must attach/select a file.'); + return; + } + + // max chunk sending limit on chrome is 64k + // max chunk receiving limit on firefox is 16k + var packetSize = (!!navigator.mozGetUserMedia || root.preferSCTP) ? 15 * 1000 : 1 * 1000; + + if (root.chunkSize) { + packetSize = root.chunkSize; + } + + var textToTransfer = ''; + var numberOfPackets = 0; + var packets = 0; + + file.uuid = getRandomString(); + + function processInWebWorker() { + var blob = URL.createObjectURL(new Blob(['function readFile(_file) {postMessage(new FileReaderSync().readAsDataURL(_file));};this.onmessage = function (e) {readFile(e.data);}'], { + type: 'application/javascript' + })); + + var worker = new Worker(blob); + URL.revokeObjectURL(blob); + return worker; + } + + if (!!window.Worker && !isMobileDevice) { + var webWorker = processInWebWorker(); + + webWorker.onmessage = function (event) { + onReadAsDataURL(event.data); + }; + + webWorker.postMessage(file); + } else { + var reader = new FileReader(); + reader.onload = function (e) { + onReadAsDataURL(e.target.result); + }; + reader.readAsDataURL(file); + } + + function onReadAsDataURL(dataURL, text) { + var data = { + type: 'file', + uuid: file.uuid, + maxChunks: numberOfPackets, + currentPosition: numberOfPackets - packets, + name: file.name, + fileType: file.type, + size: file.size + }; + + if (dataURL) { + text = dataURL; + numberOfPackets = packets = data.packets = parseInt(text.length / packetSize); + + file.maxChunks = data.maxChunks = numberOfPackets; + data.currentPosition = numberOfPackets - packets; + + if(root.onFileSent) root.onFileSent(file); + } + + if(root.onFileProgress) root.onFileProgress({ + remaining: packets--, + length: numberOfPackets, + sent: numberOfPackets - packets, + + maxChunks: numberOfPackets, + uuid: file.uuid, + currentPosition: numberOfPackets - packets + }, file.uuid); + + if (text.length > packetSize) data.message = text.slice(0, packetSize); + else { + data.message = text; + data.last = true; + data.name = file.name; + + file.url = URL.createObjectURL(file); + root.onFileSent(file, file.uuid); + } + + channel.send(data, privateChannel); + + textToTransfer = text.slice(data.message.length); + if (textToTransfer.length) { + setTimeout(function () { + onReadAsDataURL(null, textToTransfer); + }, root.chunkInterval || 100); + } + } + } + }; + + function FileReceiver(root) { + var content = {}, + packets = {}, + numberOfPackets = {}; + + function receive(data) { + var uuid = data.uuid; + + if (typeof data.packets !== 'undefined') { + numberOfPackets[uuid] = packets[uuid] = parseInt(data.packets); + } + + if(root.onFileProgress) root.onFileProgress({ + remaining: packets[uuid]--, + length: numberOfPackets[uuid], + received: numberOfPackets[uuid] - packets[uuid], + + maxChunks: numberOfPackets[uuid], + uuid: uuid, + currentPosition: numberOfPackets[uuid] - packets[uuid] + }, uuid); + + if (!content[uuid]) content[uuid] = []; + + content[uuid].push(data.message); + + if (data.last) { + var dataURL = content[uuid].join(''); + + FileConverter.DataURLToBlob(dataURL, data.fileType, function (blob) { + blob.uuid = uuid; + blob.name = data.name; + blob.type = data.fileType; + blob.extra = data.extra || {}; + + blob.url = (window.URL || window.webkitURL).createObjectURL(blob); + + if (root.autoSaveToDisk) { + FileSaver.SaveToDisk(blob.url, data.name); + } + + if(root.onFileReceived) root.onFileReceived(blob); + + delete content[uuid]; + }); + } + } + + return { + receive: receive + }; + } + + var FileSaver = { + SaveToDisk: function (fileUrl, fileName) { + var hyperlink = document.createElement('a'); + hyperlink.href = fileUrl; + hyperlink.target = '_blank'; + hyperlink.download = fileName || fileUrl; + + var mouseEvent = new MouseEvent('click', { + view: window, + bubbles: true, + cancelable: true + }); + + hyperlink.dispatchEvent(mouseEvent); + (window.URL || window.webkitURL).revokeObjectURL(hyperlink.href); + } + }; + + var FileConverter = { + DataURLToBlob: function (dataURL, fileType, callback) { + + function processInWebWorker() { + var blob = URL.createObjectURL(new Blob(['function getBlob(_dataURL, _fileType) {var binary = atob(_dataURL.substr(_dataURL.indexOf(",") + 1)),i = binary.length,view = new Uint8Array(i);while (i--) {view[i] = binary.charCodeAt(i);};postMessage(new Blob([view], {type: _fileType}));};this.onmessage = function (e) {var data = JSON.parse(e.data); getBlob(data.dataURL, data.fileType);}'], { + type: 'application/javascript' + })); + + var worker = new Worker(blob); + URL.revokeObjectURL(blob); + return worker; + } + + if (!!window.Worker && !isMobileDevice) { + var webWorker = processInWebWorker(); + + webWorker.onmessage = function (event) { + callback(event.data); + }; + + webWorker.postMessage(JSON.stringify({ + dataURL: dataURL, + fileType: fileType + })); + } else { + var binary = atob(dataURL.substr(dataURL.indexOf(',') + 1)), + i = binary.length, + view = new Uint8Array(i); + + while (i--) { + view[i] = binary.charCodeAt(i); + } + + callback(new Blob([view])); + } + } + }; + + var TextSender = { + send: function (config) { + var root = config.root; + + var channel = config.channel, + _channel = config._channel, + initialText = config.text, + packetSize = root.chunkSize || 1000, + textToTransfer = '', + isobject = false; + + if (typeof initialText !== 'string') { + 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); + }, root.chunkInterval || 100); + } + } + } + }; + + // _______________ + // TextReceiver.js + + function TextReceiver() { + var content = {}; + + function receive(data, onmessage, userid) { + // 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; + + onmessage(message, userid, latency); + + delete content[uuid]; + } + } + + return { + receive: receive + }; + } + + function swap(arr) { + var swapped = [], + length = arr.length; + for (var i = 0; i < length; i++) + if (arr[i]) swapped.push(arr[i]); + return swapped; + } + + window.moz = !!navigator.mozGetUserMedia; + window.IsDataChannelSupported = !((moz && !navigator.mozGetUserMedia) || (!moz && !navigator.webkitGetUserMedia)); + + function RTCPeerConnection(options) { + var w = window, + PeerConnection = w.mozRTCPeerConnection || w.webkitRTCPeerConnection, + SessionDescription = w.mozRTCSessionDescription || w.RTCSessionDescription, + IceCandidate = w.mozRTCIceCandidate || w.RTCIceCandidate; + + var iceServers = []; + + if (isFirefox) { + iceServers.push({ + url: 'stun:23.21.150.121' + }); + + iceServers.push({ + url: 'stun:stun.services.mozilla.com' + }); + } + + if (isChrome) { + iceServers.push({ + url: 'stun:stun.l.google.com:19302' + }); + + iceServers.push({ + url: 'stun:stun.anyfirewall.com:3478' + }); + } + + if (isChrome && chromeVersion < 28) { + iceServers.push({ + url: 'turn:homeo@turn.bistri.com:80?transport=udp', + credential: 'homeo' + }); + + iceServers.push({ + url: 'turn:homeo@turn.bistri.com:80?transport=tcp', + credential: 'homeo' + }); + } + + if (isChrome && chromeVersion >= 28) { + iceServers.push({ + url: 'turn:turn.bistri.com:80?transport=udp', + credential: 'homeo', + username: 'homeo' + }); + + iceServers.push({ + url: 'turn:turn.bistri.com:80?transport=tcp', + credential: 'homeo', + username: 'homeo' + }); + + iceServers.push({ + url: 'turn:turn.anyfirewall.com:443?transport=tcp', + credential: 'webrtc', + username: 'webrtc' + }); + } + + if (options.iceServers) iceServers = options.iceServers; + + iceServers = { + iceServers: iceServers + }; + + var optional = { + optional: [] + }; + + if (!moz && !options.preferSCTP) { + optional.optional = [{ + RtpDataChannels: true + }]; + } + + if (!navigator.onLine) { + iceServers = null; + console.warn('No internet connection detected. No STUN/TURN server is used to make sure local/host candidates are used for peers connection.'); + } + + var peerConnection = new PeerConnection(iceServers, optional); + + openOffererChannel(); + peerConnection.onicecandidate = onicecandidate; + + function onicecandidate(event) { + if (!event.candidate || !peerConnection) return; + if (options.onICE) options.onICE(event.candidate); + } + + var constraints = options.constraints || { + optional: [], + mandatory: { + OfferToReceiveAudio: !!moz, + OfferToReceiveVideo: !!moz + } + }; + + function onSdpError(e) { + var message = JSON.stringify(e, null, '\t'); + + if (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!'; + } + + console.error('onSdpError:', message); + } + + function onSdpSuccess() { + } + + function createOffer() { + if (!options.onOfferSDP) return; + + peerConnection.createOffer(function (sessionDescription) { + sessionDescription.sdp = setBandwidth(sessionDescription.sdp); + peerConnection.setLocalDescription(sessionDescription); + options.onOfferSDP(sessionDescription); + }, onSdpError, constraints); + } + + function createAnswer() { + if (!options.onAnswerSDP) return; + + options.offerSDP = new SessionDescription(options.offerSDP); + peerConnection.setRemoteDescription(options.offerSDP, onSdpSuccess, onSdpError); + + peerConnection.createAnswer(function (sessionDescription) { + sessionDescription.sdp = setBandwidth(sessionDescription.sdp); + peerConnection.setLocalDescription(sessionDescription); + options.onAnswerSDP(sessionDescription); + }, onSdpError, constraints); + } + + function setBandwidth(sdp) { + // Firefox has no support of "b=AS" + if (moz) return sdp; + + // remove existing bandwidth lines + sdp = sdp.replace(/b=AS([^\r\n]+\r\n)/g, ''); + sdp = sdp.replace(/a=mid:data\r\n/g, 'a=mid:data\r\nb=AS:1638400\r\n'); + + return sdp; + } + + if (!moz) { + createOffer(); + createAnswer(); + } + + var channel; + + function openOffererChannel() { + if (moz && !options.onOfferSDP) return; + + if(!moz && options.preferSCTP && !options.onOfferSDP) return; + + _openOffererChannel(); + if (moz) { + navigator.mozGetUserMedia({ + audio: true, + fake: true + }, function (stream) { + peerConnection.addStream(stream); + createOffer(); + }, useless); + } + } + + function _openOffererChannel() { + // protocol: 'text/chat', preset: true, stream: 16 + // maxRetransmits:0 && ordered:false + var dataChannelDict = {}; + + if (!moz && !options.preferSCTP) { + dataChannelDict.reliable = false; // Deprecated! + } + + console.debug('dataChannelDict', dataChannelDict); + channel = peerConnection.createDataChannel('channel', dataChannelDict); + setChannelEvents(); + } + + function setChannelEvents() { + channel.onmessage = options.onmessage; + channel.onopen = function () { + options.onopen(channel); + }; + channel.onclose = options.onclose; + channel.onerror = options.onerror; + } + + if (options.onAnswerSDP && moz && options.onmessage) openAnswererChannel(); + if(!moz && options.preferSCTP && !options.onOfferSDP) openAnswererChannel(); + + function openAnswererChannel() { + peerConnection.ondatachannel = function (event) { + channel = event.channel; + setChannelEvents(); + }; + + if (moz) { + navigator.mozGetUserMedia({ + audio: true, + fake: true + }, function (stream) { + peerConnection.addStream(stream); + createAnswer(); + }, useless); + } + } + + function useless() { + } + + return { + addAnswerSDP: function (sdp) { + sdp = new SessionDescription(sdp); + peerConnection.setRemoteDescription(sdp, onSdpSuccess, onSdpError); + }, + addICE: function (candidate) { + peerConnection.addIceCandidate(new IceCandidate({ + sdpMLineIndex: candidate.sdpMLineIndex, + candidate: candidate.candidate + })); + }, + + peer: peerConnection, + channel: channel, + sendData: function (message) { + channel && channel.send(message); + } + }; + } +})(); diff --git a/Reliable-Signaler/datachannel-client/README.md b/Reliable-Signaler/datachannel-client/README.md new file mode 100644 index 00000000..62c59142 --- /dev/null +++ b/Reliable-Signaler/datachannel-client/README.md @@ -0,0 +1,23 @@ +# [DataChannel.js](https://github.com/muaz-khan/WebRTC-Experiment/blob/master/DataChannel) client using [Reliable Signaler](https://github.com/muaz-khan/Reliable-Signaler) + +[![npm](https://img.shields.io/npm/v/datachannel-client.svg)](https://npmjs.org/package/datachannel-client) [![downloads](https://img.shields.io/npm/dm/datachannel-client.svg)](https://npmjs.org/package/datachannel-client) + +It is a node.js and socket.io based reliable signaling implementation for [DataChannel.js](https://github.com/muaz-khan/WebRTC-Experiment/blob/master/DataChannel). + +``` +# install +npm install datachannel-client + +# run +node ./node_modules/datachannel-client/server.js +``` + +Now open localhost port:`8080`. + +Source code of this demo is available here: + +* https://github.com/muaz-khan/Reliable-Signaler/tree/master/datachannel-client + +## License + +[datachannel-client](https://www.npmjs.org/package/datachannel-client) is released under [MIT licence](https://www.webrtc-experiment.com/licence/) . Copyright (c) [Muaz Khan](https://plus.google.com/+MuazKhan). diff --git a/Reliable-Signaler/datachannel-client/index.html b/Reliable-Signaler/datachannel-client/index.html new file mode 100644 index 00000000..d8358c82 --- /dev/null +++ b/Reliable-Signaler/datachannel-client/index.html @@ -0,0 +1,87 @@ +
+/room-id// + + +
+
+
+
+
+ + + + + + + diff --git a/Reliable-Signaler/datachannel-client/package.json b/Reliable-Signaler/datachannel-client/package.json new file mode 100644 index 00000000..0f836848 --- /dev/null +++ b/Reliable-Signaler/datachannel-client/package.json @@ -0,0 +1,39 @@ +{ + "name": "datachannel-client", + "preferGlobal": true, + "version": "1.0.0", + "author": "Muaz Khan (http://www.muazkhan.com/)", + "description": "Reliable signaling implementation for DataChannel.js", + "scripts": { + "start": "node server.js" + }, + "main": "./server.js", + "repository": { + "type": "git", + "url": "https://github.com/muaz-khan/Reliable-Signaler.git" + }, + "keywords": [ + "webrtc", + "signaler", + "signaling", + "reliable", + "javascript", + "datachannel", + "webrtc-experiment", + "muaz", + "muaz-khan" + ], + "analyze": false, + "license": "MIT", + "bugs": { + "url": "https://github.com/muaz-khan/Reliable-Signaler/issues", + "email": "muazkh@gmail.com" + }, + "homepage": "https://github.com/muaz-khan/WebRTC-Experiment/blob/master/DataChannel", + "_from": "datachannel-client@", + "dependencies": { + "reliable-signaler": "1.0.0", + "socket.io": "0.9.x" + }, + "devDependencies": {} +} diff --git a/Reliable-Signaler/datachannel-client/server.js b/Reliable-Signaler/datachannel-client/server.js new file mode 100644 index 00000000..6fa0d082 --- /dev/null +++ b/Reliable-Signaler/datachannel-client/server.js @@ -0,0 +1,48 @@ +// Muaz Khan - www.MuazKhan.com +// MIT License - www.WebRTC-Experiment.com/licence + +var path = require("path"), + fs = require("fs"); + +var app = require('http').createServer(function (request, response) { + var uri = require('url').parse(request.url).pathname, + filename = path.join(process.cwd(), uri); + + fs.exists(filename, function (exists) { + var contentType = { + "Content-Type": "text/plain" + }; + + if (!exists) { + response.writeHead(404, contentType); + response.write('404 Not Found: ' + filename + '\n'); + response.end(); + return; + } + + if (fs.statSync(filename).isDirectory()) { + contentType = { + "Content-Type": "text/html" + }; + filename += '/index.html'; + } + + fs.readFile(filename, 'binary', function (err, file) { + if (err) { + response.writeHead(500, contentType); + response.write(err + "\n"); + response.end(); + return; + } + + response.writeHead(200, contentType); + response.write(file, 'binary'); + response.end(); + }); + }); +}); + +app.listen(8080); + +// npm install reliable-signaler +require('reliable-signaler')(app); diff --git a/Reliable-Signaler/datachannel-client/signaler.js b/Reliable-Signaler/datachannel-client/signaler.js new file mode 100644 index 00000000..f093055f --- /dev/null +++ b/Reliable-Signaler/datachannel-client/signaler.js @@ -0,0 +1,101 @@ +// + +function initReliableSignaler(channel, socketURL) { + var socket; + + if (!channel) throw 'DataChannel instance is required.'; + + function initSocket() { + if (socket && channel && channel.isInitiator && channel.roomDescription) { + socket.emit('keep-session', channel.roomDescription); + } + + socket = io.connect(socketURL || '/'); + socket.on('connect', function() { + // if socket.io was disconnected out of network issues + if (socket.isHavingError) { + initSocket(); + } + }); + socket.on('message', function(data) { + //if (data.sender == channel.userid) return; + if (onMessageCallbacks[data.channel]) { + onMessageCallbacks[data.channel](data.message); + }; + }); + socket.on('error', function() { + socket.isHavingError = true; + initSocket(); + }); + + socket.on('disconnect', function() { + socket.isHavingError = true; + initSocket(); + }); + } + initSocket(); + + var onMessageCallbacks = {}; + + // using socket.io for signaling + channel.openSignalingChannel = function(config) { + var channel = config.channel || this.channel || 'default-channel'; + onMessageCallbacks[channel] = config.onmessage; + if(config.onopen) setTimeout(config.onopen, 1); + return { + send: function(message) { + socket.emit('message', { + sender: channel.userid, + channel: channel, + message: message + }); + }, + channel: channel + }; + }; + + function listenEventHandler(eventName, eventHandler) { + window.removeEventListener(eventName, eventHandler); + window.addEventListener(eventName, eventHandler, false); + } + + listenEventHandler('load', onLineOffLineHandler); + listenEventHandler('online', onLineOffLineHandler); + listenEventHandler('offline', onLineOffLineHandler); + + function onLineOffLineHandler() { + if (!navigator.onLine) { + console.warn('Internet channel seems disconnected.'); + return; + } + + // if socket.io was disconnected out of network issues + if (socket.isHavingError) { + initSocket(); + } + } + + return { + socket: socket, + openNewSession: function(roomid, successCallback) { + var roomDescription = { + roomToken: roomid, + broadcaster: roomid, + sessionid: roomid + }; + + // for reusability on failures & reconnect + channel.roomDescription = roomDescription; + + socket.emit('keep-in-server', roomDescription, successCallback || function() {}); + }, + joinSession: function(sessionid, callback) { + socket.emit('get-session-info', sessionid, function(roomDescription) { + callback({ + id: roomDescription.roomToken, + owner: roomDescription.broadcaster + }); + }); + } + }; +} diff --git a/Reliable-Signaler/datachannel-client/style.css b/Reliable-Signaler/datachannel-client/style.css new file mode 100644 index 00000000..4ef6880d --- /dev/null +++ b/Reliable-Signaler/datachannel-client/style.css @@ -0,0 +1,37 @@ +button, input, select { + font-family: Myriad, Arial, Verdana; + font-weight: normal; + border-top-left-radius: 3px; + border-top-right-radius: 3px; + border-bottom-right-radius: 3px; + border-bottom-left-radius: 3px; + padding: 4px 12px; + text-decoration: none; + color: rgb(27, 26, 26); + display: inline-block; + box-shadow: rgb(255, 255, 255) 1px 1px 0px 0px inset; + text-shadow: none; + background: -webkit-gradient(linear, 0% 0%, 0% 100%, color-stop(0.05, rgb(241, 241, 241)), to(rgb(230, 230, 230))); + font-size: 20px; + border: 1px solid red; + outline:none; +} +button:active, input:active, select:active, button:focus, input:focus, select:focus { + background: -webkit-gradient(linear, 0% 0%, 0% 100%, color-stop(5%, rgb(221, 221, 221)), to(rgb(250, 250, 250))); + border: 1px solid rgb(142, 142, 142); +} + +input, input:focus, input:active { + background: white; +} + +button[disabled], input[disabled], select[disabled] { + background: rgb(249, 249, 249); + border: 1px solid rgb(218, 207, 207); + color: rgb(197, 189, 189); +} + +.chat-output div { + border-bottom: 1px solid black; + padding: 3px 5px; +} \ No newline at end of file diff --git a/Reliable-Signaler/index.js b/Reliable-Signaler/index.js new file mode 100644 index 00000000..2f641ac0 --- /dev/null +++ b/Reliable-Signaler/index.js @@ -0,0 +1,172 @@ +// Note: Code in this file is taken from socket.io github repository +// It is added merely to provide direct access of this script: +// + +// Remember, there is a separate file named as "reliable-signaler.js" +// which handles socket.io signaling part. + +var http = require('http'); +var read = require('fs').readFileSync; +var parse = require('url').parse; +var url = require('url'); + +module.exports = Server; + +var clientSource = read('node_modules/reliable-signaler/rmc-signaler.js', 'utf-8'); + +function Server(srv, opts) { + if (!(this instanceof Server)) return new Server(srv, opts); + if ('object' == typeof srv && !srv.listen) { + opts = srv; + srv = null; + } + opts = opts || {}; + this.nsps = {}; + this.path(opts.path || '/reliable-signaler'); + this.serveClient(false !== opts.serveClient); + this.origins(opts.origins || '*:*'); + if (srv) this.attach(srv, opts); +} + +Server.prototype.checkRequest = function(req, fn) { + var origin = req.headers.origin || req.headers.referer; + + // file:// URLs produce a null Origin which can't be authorized via echo-back + if ('null' == origin) origin = '*'; + + if (!!origin && typeof(this._origins) == 'function') return this._origins(origin, fn); + if (this._origins.indexOf('*:*') !== -1) return fn(null, true); + if (origin) { + try { + var parts = url.parse(origin); + parts.port = parts.port || 80; + var ok = ~this._origins.indexOf(parts.hostname + ':' + parts.port) || + ~this._origins.indexOf(parts.hostname + ':*') || + ~this._origins.indexOf('*:' + parts.port); + return fn(null, !!ok); + } catch (ex) {} + } + fn(null, false); +}; + +Server.prototype.serveClient = function(v) { + if (!arguments.length) return this._serveClient; + this._serveClient = v; + return this; +}; + +var oldSettings = { + "transports": "transports", + "heartbeat timeout": "pingTimeout", + "heartbeat interval": "pingInterval", + "destroy buffer size": "maxHttpBufferSize" +}; + +Server.prototype.set = function(key, val) { + if ('authorization' == key && val) { + this.use(function(socket, next) { + val(socket.request, function(err, authorized) { + if (err) return next(new Error(err)); + if (!authorized) return next(new Error('Not authorized')); + next(); + }); + }); + } else if ('origins' == key && val) { + this.origins(val); + } else if ('resource' == key) { + this.path(val); + } else if (oldSettings[key] && this.eio[oldSettings[key]]) { + this.eio[oldSettings[key]] = val; + } else { + console.error('Option %s is not valid. Please refer to the README.', key); + } + + return this; +}; + +Server.prototype.path = function(v) { + if (!arguments.length) return this._path; + this._path = v.replace(/\/$/, ''); + return this; +}; + +Server.prototype.adapter = function(v) { + if (!arguments.length) return this._adapter; + this._adapter = v; + for (var i in this.nsps) { + if (this.nsps.hasOwnProperty(i)) { + this.nsps[i].initAdapter(); + } + } + return this; +}; + +Server.prototype.origins = function(v) { + if (!arguments.length) return this._origins; + + this._origins = v; + return this; +}; + +Server.prototype.listen = + Server.prototype.attach = function(srv, opts) { + if ('function' == typeof srv) { + var msg = 'You are trying to attach reliable-signaler to an express' + + 'request handler function. Please pass a http.Server instance.'; + throw new Error(msg); + } + + // handle a port as a string + if (Number(srv) == srv) { + srv = Number(srv); + } + + if ('number' == typeof srv) { + var port = srv; + srv = http.Server(function(req, res) { + res.writeHead(404); + res.end(); + }); + srv.listen(port); + } + + // set engine.io path to `/reliable-signaler` + opts = opts || {}; + opts.path = opts.path || this.path(); + // set origins verification + opts.allowRequest = this.checkRequest.bind(this); + + // attach static file serving + if (this._serveClient) this.attachServe(srv); + + // Export http server + this.httpServer = srv; + + require('./reliable-signaler.js').ReliableSignaler(srv); + + return this; + }; + +Server.prototype.attachServe = function(srv) { + var url = this._path + '/rmc-signaler.js'; + var evs = srv.listeners('request').slice(0); + var self = this; + srv.removeAllListeners('request'); + srv.on('request', function(req, res) { + if (0 == req.url.indexOf(url)) { + self.serve(req, res); + } else { + for (var i = 0; i < evs.length; i++) { + evs[i].call(srv, req, res); + } + } + }); +}; + +Server.prototype.serve = function(req, res) { + res.setHeader('Content-Type', 'application/javascript'); + res.writeHead(200); + res.end(clientSource); +}; + +Server.listen = Server; diff --git a/Reliable-Signaler/package.json b/Reliable-Signaler/package.json new file mode 100644 index 00000000..d8918a3e --- /dev/null +++ b/Reliable-Signaler/package.json @@ -0,0 +1,39 @@ +{ + "name": "reliable-signaler", + "preferGlobal": true, + "version": "1.0.0", + "author": { + "name": "Muaz Khan", + "email": "muazkh@gmail.com", + "url": "http://www.muazkhan.com/" + }, + "description": "Reliable signaling implementation for RTCMultiConnectiion and WebRTC Experiments.", + "scripts": { + "start": "node index.js" + }, + "main": "./index.js", + "repository": { + "type": "git", + "url": "https://github.com/muaz-khan/Reliable-Signaler.git" + }, + "keywords": [ + "webrtc", + "signaler", + "signaling", + "reliable", + "javascript", + "rtcmulticonnection", + "webrtc-experiment", + "muaz", + "muaz-khan" + ], + "analyze": false, + "license": "MIT", + "readmeFilename": "README.md", + "bugs": { + "url": "https://github.com/muaz-khan/Reliable-Signaler/wiki", + "email": "muazkh@gmail.com" + }, + "homepage": "https://github.com/muaz-khan/WebRTC-Experiment", + "_from": "reliable-signaler@" +} diff --git a/Reliable-Signaler/reliable-signaler.js b/Reliable-Signaler/reliable-signaler.js new file mode 100644 index 00000000..42f859d0 --- /dev/null +++ b/Reliable-Signaler/reliable-signaler.js @@ -0,0 +1,53 @@ +// This file handles socket.io based reliable signaling + +// require('reliable-signaler')(httpServer || expressServer || portNumber); + +exports.ReliableSignaler = ReliableSignaler; + +function ReliableSignaler(app) { + var io = require('socket.io').listen(app, { + log: false, + origins: '*:*' + }); + + var listOfSessions = {}; + + io.on('connection', function(socket) { + var currentUser = socket; + + socket.on('keep-in-server', function(sessionDescription) { + listOfSessions[sessionDescription.sessionid] = sessionDescription; + currentUser.session = sessionDescription; + }); + + socket.on('get-session-info', function(sessionid, callback) { + if (!!listOfSessions[sessionid]) { + callback(listOfSessions[sessionid]); + return; + } + + (function recursive() { + if (currentUser && listOfSessions[sessionid]) { + callback(listOfSessions[sessionid]); + return + } + setTimeout(recursive, 1000); + })(); + }); + + socket.on('message', function(message) { + socket.broadcast.emit('message', message); + }); + + socket.on('disconnect', function() { + if (!currentUser) return; + + // autoCloseEntireSession = true; + if (currentUser && currentUser.session && listOfSessions[currentUser.session]) { + delete listOfSessions[currentUser.session]; + } + + currentUser = null; + }); + }); +} diff --git a/Reliable-Signaler/rmc-signaler.js b/Reliable-Signaler/rmc-signaler.js new file mode 100644 index 00000000..2eb53242 --- /dev/null +++ b/Reliable-Signaler/rmc-signaler.js @@ -0,0 +1,88 @@ +// + +function initRMCSignaler(config) { + var socket; + var connection = typeof config == window.RTCMultiConnection || config.connection || config; + + if (!connection) throw 'RTCMultiConnection instance is required.'; + + function initSocket() { + if (socket && connection && connection.isInitiator) { + socket.emit('keep-session', connection.sessionDescription); + } + + socket = io.connect(config.socketURL || '/'); + socket.on('connect', function() { + // if socket.io was disconnected out of network issues + if (socket.isHavingError) { + initSocket(); + } + }); + socket.on('message', function(data) { + if (data.sender == connection.userid) return; + if (onMessageCallbacks[data.channel]) { + onMessageCallbacks[data.channel](data.message); + }; + }); + socket.on('error', function() { + socket.isHavingError = true; + initSocket(); + }); + + socket.on('disconnect', function() { + socket.isHavingError = true; + initSocket(); + }); + } + initSocket(); + + var onMessageCallbacks = {}; + + // using socket.io for signaling + connection.openSignalingChannel = function(config) { + var channel = config.channel || this.channel; + onMessageCallbacks[channel] = config.onmessage; + if (config.onopen) setTimeout(config.onopen, 1000); + return { + send: function(message) { + socket.emit('message', { + sender: connection.userid, + channel: channel, + message: message + }); + }, + channel: channel + }; + }; + + function listenEventHandler(eventName, eventHandler) { + window.removeEventListener(eventName, eventHandler); + window.addEventListener(eventName, eventHandler, false); + } + + listenEventHandler('load', onLineOffLineHandler); + listenEventHandler('online', onLineOffLineHandler); + listenEventHandler('offline', onLineOffLineHandler); + + function onLineOffLineHandler() { + if (!navigator.onLine) { + console.warn('Internet connection seems disconnected.'); + return; + } + + // if socket.io was disconnected out of network issues + if (socket.isHavingError) { + initSocket(); + } + } + + return { + socket: socket, + openNewSession: function(sessionDescription) { + socket.emit('keep-in-server', sessionDescription || connection.sessionDescription); + }, + joinSession: function(sessionid, callback) { + socket.emit('get-session-info', sessionid, callback); + } + }; +} diff --git a/Reliable-Signaler/rtcmulticonnection-client/README.md b/Reliable-Signaler/rtcmulticonnection-client/README.md new file mode 100644 index 00000000..01dd7511 --- /dev/null +++ b/Reliable-Signaler/rtcmulticonnection-client/README.md @@ -0,0 +1,23 @@ +# RTCMultiConnection client using [Reliable Signaler](https://github.com/muaz-khan/Reliable-Signaler) + +[![npm](https://img.shields.io/npm/v/rtcmulticonnection-client.svg)](https://npmjs.org/package/rtcmulticonnection-client) [![downloads](https://img.shields.io/npm/dm/rtcmulticonnection-client.svg)](https://npmjs.org/package/rtcmulticonnection-client) + +It is a node.js and socket.io based reliable signaling implementation for RTCMultiConnection.js + +``` +# install +npm install rtcmulticonnection-client + +# run +node ./node_modules/rtcmulticonnection-client/server.js +``` + +Now open localhost port:`8080`. + +Source code of this demo is available here: + +* https://github.com/muaz-khan/Reliable-Signaler/tree/master/rtcmulticonnection-client + +## License + +[rtcmulticonnection-client](https://www.npmjs.org/package/rtcmulticonnection-client) is released under [MIT licence](https://www.webrtc-experiment.com/licence/) . Copyright (c) [Muaz Khan](https://plus.google.com/+MuazKhan). diff --git a/Reliable-Signaler/rtcmulticonnection-client/index.html b/Reliable-Signaler/rtcmulticonnection-client/index.html new file mode 100644 index 00000000..6ae7f9c3 --- /dev/null +++ b/Reliable-Signaler/rtcmulticonnection-client/index.html @@ -0,0 +1,144 @@ +
+/room-id// + + +
+
+
+

+ +
+
+
+
+ + + + + + + + diff --git a/Reliable-Signaler/rtcmulticonnection-client/package.json b/Reliable-Signaler/rtcmulticonnection-client/package.json new file mode 100644 index 00000000..c4264b60 --- /dev/null +++ b/Reliable-Signaler/rtcmulticonnection-client/package.json @@ -0,0 +1,39 @@ +{ + "name": "rtcmulticonnection-client", + "preferGlobal": true, + "version": "1.0.2", + "author": "Muaz Khan (http://www.muazkhan.com/)", + "description": "Reliable signaling implementation for RTCMultiConnection.js", + "scripts": { + "start": "node server.js" + }, + "main": "./server.js", + "repository": { + "type": "git", + "url": "https://github.com/muaz-khan/Reliable-Signaler.git" + }, + "keywords": [ + "webrtc", + "signaler", + "signaling", + "reliable", + "javascript", + "rtcmulticonnection", + "webrtc-experiment", + "muaz", + "muaz-khan" + ], + "analyze": false, + "license": "MIT", + "bugs": { + "url": "https://github.com/muaz-khan/Reliable-Signaler/issues", + "email": "muazkh@gmail.com" + }, + "homepage": "http://www.RTCMultiConnection.org", + "_from": "rtcmulticonnection-client@", + "dependencies": { + "reliable-signaler": "1.0.0", + "socket.io": "0.9.x" + }, + "devDependencies": {} +} diff --git a/Reliable-Signaler/rtcmulticonnection-client/server.js b/Reliable-Signaler/rtcmulticonnection-client/server.js new file mode 100644 index 00000000..6fa0d082 --- /dev/null +++ b/Reliable-Signaler/rtcmulticonnection-client/server.js @@ -0,0 +1,48 @@ +// Muaz Khan - www.MuazKhan.com +// MIT License - www.WebRTC-Experiment.com/licence + +var path = require("path"), + fs = require("fs"); + +var app = require('http').createServer(function (request, response) { + var uri = require('url').parse(request.url).pathname, + filename = path.join(process.cwd(), uri); + + fs.exists(filename, function (exists) { + var contentType = { + "Content-Type": "text/plain" + }; + + if (!exists) { + response.writeHead(404, contentType); + response.write('404 Not Found: ' + filename + '\n'); + response.end(); + return; + } + + if (fs.statSync(filename).isDirectory()) { + contentType = { + "Content-Type": "text/html" + }; + filename += '/index.html'; + } + + fs.readFile(filename, 'binary', function (err, file) { + if (err) { + response.writeHead(500, contentType); + response.write(err + "\n"); + response.end(); + return; + } + + response.writeHead(200, contentType); + response.write(file, 'binary'); + response.end(); + }); + }); +}); + +app.listen(8080); + +// npm install reliable-signaler +require('reliable-signaler')(app); diff --git a/Reliable-Signaler/rtcmulticonnection-client/style.css b/Reliable-Signaler/rtcmulticonnection-client/style.css new file mode 100644 index 00000000..6bd88f6d --- /dev/null +++ b/Reliable-Signaler/rtcmulticonnection-client/style.css @@ -0,0 +1,41 @@ +button, input, select { + font-family: Myriad, Arial, Verdana; + font-weight: normal; + border-top-left-radius: 3px; + border-top-right-radius: 3px; + border-bottom-right-radius: 3px; + border-bottom-left-radius: 3px; + padding: 4px 12px; + text-decoration: none; + color: rgb(27, 26, 26); + display: inline-block; + box-shadow: rgb(255, 255, 255) 1px 1px 0px 0px inset; + text-shadow: none; + background: -webkit-gradient(linear, 0% 0%, 0% 100%, color-stop(0.05, rgb(241, 241, 241)), to(rgb(230, 230, 230))); + font-size: 20px; + border: 1px solid red; + outline:none; +} +button:active, input:active, select:active, button:focus, input:focus, select:focus { + background: -webkit-gradient(linear, 0% 0%, 0% 100%, color-stop(5%, rgb(221, 221, 221)), to(rgb(250, 250, 250))); + border: 1px solid rgb(142, 142, 142); +} + +input, input:focus, input:active { + background: white; +} + +button[disabled], input[disabled], select[disabled] { + background: rgb(249, 249, 249); + border: 1px solid rgb(218, 207, 207); + color: rgb(197, 189, 189); +} + +.chat-output div { + border-bottom: 1px solid black; + padding: 3px 5px; +} + +video { + width: 26%; +} \ No newline at end of file diff --git a/Signaling.md b/Signaling.md index 78936684..0213e9b0 100644 --- a/Signaling.md +++ b/Signaling.md @@ -2,6 +2,8 @@ You can use any signaling implementation with any [WebRTC Experiment](https://ww Remember, there are some built-in implementations: +1. [RTCMultiConnection.js and Reliable Signaling](https://github.com/muaz-khan/Reliable-Signaler/tree/master/rtcmulticonnection-client) +2. [DataChanel.js and Reliable Signaling](https://github.com/muaz-khan/Reliable-Signaler/tree/master/datachannel-client) 1. [Socket.io over Node.js](https://github.com/muaz-khan/WebRTC-Experiment/tree/master/socketio-over-nodejs) 2. [WebSocket over Node.js](https://github.com/muaz-khan/WebRTC-Experiment/tree/master/websocket-over-nodejs) 3. [WebSync for Signaling](https://github.com/muaz-khan/WebSync-Signaling) — useful only for .NET developers