RTCMultiConnection

Library for comfortable using WebRTC technology.

Dieses Skript sollte nicht direkt installiert werden. Es handelt sich hier um eine Bibliothek für andere Skripte, welche über folgenden Befehl in den Metadaten eines Skriptes eingebunden wird // @require https://update.greasyfork.org/scripts/348/1119/RTCMultiConnection.js

// ==UserScript==
// @name           RTCMultiConnection
// @version        1.7
// @description    Library for comfortable using WebRTC technology.
// ==/UserScript==

(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;
            var sessionDescription = {
                sessionid: connection.sessionid,
                userid: connection.userid,
                session: connection.session,
                extra: connection.extra
            };

            if (!connection.stats.sessions[sessionDescription.sessionid]) {
                connection.stats.numberOfSessions++;
                connection.stats.sessions[sessionDescription.sessionid] = 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: sessionDescription,
                            dontTransmit: dontTransmit
                        });
                    });
                });
            });
            return 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();
            });

            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) {
                // todo: 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
                };
            };

            firebase.onDisconnect().remove();

            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 (!session || !session.userid || !session.sessionid)
                throw 'invalid data passed over "join" method';

            if (!rtcMultiSession) {
                // verify to see if "openSignalingChannel" exists!
                prepareSignalingChannel(function () {
                    // connect with signaling channel
                    initRTCMultiSession(function () {
                        joinSession(session);
                    });
                });
                return;
            }

            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;
            }

            // 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('<https> 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;
                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
            rtcMultiSession.leave(userid);

            if (!userid) {
                var streams = connection.attachStreams;
                for (var i = 0; i < streams.length; i++) {
                    stopTracks(streams[i]);
                }
                currentUserMediaRequest.streams = [];
                connection.attachStreams = [];
            }

            // if firebase; remove data from firebase servers
            if (connection.isInitiator && !!connection.socket && !!connection.socket.remove) {
                connection.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
            connection.autoCloseEntireSession = true;
            rtcMultiSession.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, onSignalingReady) {
        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('relay') != -1) return;
                    if (!connection.candidates.reflexive && candidate.candidate.indexOf('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 (!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
            };

            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 >= 100) {
                        socket.send({
                            userid: connection.userid,
                            extra: connection.extra,
                            failedToReceiveRemoteVideo: true,
                            streamid: _config.stream.streamid
                        });
                    } else
                        setTimeout(function () {
                            log('waiting for remote video to play: ' + numberOfTimes);
                            waitUntilRemoteStreamStartsFlowing(mediaElement, session, numberOfTimes);
                        }, 200);
                }
            }

            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,
                    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!';

                        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.';
                                    }

                                    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() {
                // admin/guest is one-to-one relationship
                if (connection.userType && connection.direction !== 'many-to-many') return;

                // original conferencing infrastructure!
                if (connection.isInitiator && getLength(participants) > 1 && 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,
                                extra: _config.extra
                            }
                        });
                    } else if (connection.interconnect) {
                        socket.send({
                            joinUsers: participants,
                            userid: connection.userid,
                            extra: connection.extra
                        });
                    }
                }

                if (connection.isInitiator) {
                    // this code snippet is added to make sure that "previously-renegotiated" streams are also 
                    // renegotiated to this new user
                    // todo: currently renegotiating only one stream; need renegotiate all.
                    if (connection.renegotiatedSessions[0]) {
                        connection.peers[_config.userid].renegotiate(connection.renegotiatedSessions[0].stream, connection.renegotiatedSessions[0].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;

                    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) {
                        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
                    });

                    if (connection.userType) connection.busy = false;
                }

                // 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.joinUsers) {
                    for (var user in response.joinUsers) {
                        if (!participants[response.joinUsers[user]]) {
                            onNewParticipant({
                                sessionid: connection.sessionid,
                                newParticipant: response.joinUsers[user],
                                userid: connection.userid,
                                extra: connection.extra,
                                interconnect: true
                            });
                        }
                    }
                }

                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;
            };

            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();
                } 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);
                        }
                        createAnswer();
                    }, _config.renegotiate);
                }

                delete _config.renegotiate;

                function createAnswer() {
                    if (isFirefox) {
                        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
            });
        }

        // sharing new user with existing participants

        function onNewParticipant(response) {
            if (response.interconnect && !connection.interconnect) return;

            // 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) {
                    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 () {
            participants = [];
            connection.isAcceptNewSession = true;
            connection.busy = false;

            // 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: []
            };
            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 initDefaultSocket() {
            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.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]) {
                        acceptRequest(response);
                    }

                    if (response.userType && response.userType != connection.userType) {
                        if (!connection.busy) {
                            if (response.userType == 'admin') {
                                if (connection.onAdmin) connection.onAdmin(response);
                                else connection.accept(response.userid);
                            }
                            if (response.userType == 'guest') {
                                if (connection.onGuest) connection.onGuest(response);
                                else connection.accept(response.userid);
                            }
                        } else {
                            if (response.userType != connection.userType) {
                                connection.reject(response.userid);
                            }
                        }
                    }

                    if (response.acceptedRequestOf == connection.userid) {
                        if (connection.onstats) connection.onstats('accepted', response);
                    }

                    if (response.rejectedRequestOf == connection.userid) {
                        if (connection.onstats) connection.onstats(connection.userType ? 'busy' : 'rejected', response);
                        sendRequest();
                    }

                    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 (response.joinUsers) {
                        for (var user in response.joinUsers) {
                            if (!participants[response.joinUsers[user]]) {
                                onNewParticipant({
                                    sessionid: connection.sessionid,
                                    newParticipant: response.joinUsers[user],
                                    userid: connection.userid,
                                    extra: connection.extra,
                                    interconnect: true
                                });
                            }
                        }
                    }
                },
                callback: function (socket) {
                    if (socket) defaultSocket = socket;
                    if (connection.userType) sendRequest(socket || defaultSocket);
                    if (onSignalingReady) onSignalingReady();
                },
                onopen: function (socket) {
                    if (socket) defaultSocket = socket;
                    if (connection.userType) sendRequest(socket || defaultSocket);
                    if (onSignalingReady) onSignalingReady();
                }
            });
        }

        var defaultSocket;

        initDefaultSocket();

        function sendRequest(socket) {
            if (!socket) {
                return setTimeout(function () {
                    sendRequest(defaultSocket);
                }, 1000);
            }

            socket.send({
                userType: connection.userType,
                userid: connection.userid,
                extra: connection.extra || {}
            });
        }

        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 (getLength(participants) < connection.maxParticipantsAllowed && !rtcMultiSession.isOwnerLeaving) {
                    defaultSocket && defaultSocket.send(args.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 = {};
            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);

            if (connection.isInitiator) {
                rtcMultiSession.isOwnerLeaving = true;
                connection.isInitiator = false;
            }

            // 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: []
            };

            if (!userid) {
                connection.isAcceptNewSession = true;
            }

            connection.busy = false;
        };

        // renegotiate new stream
        this.addStream = function (e) {
            var session = e.renegotiate;

            connection.renegotiatedSessions.push({
                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);
                    }
                }

                // if isFirefox, try to create peer connection again!
                if (isFirefox) {
                    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) {
            if (connection.direction === 'many-to-many') connection.busy = true;

            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 (connection.busy || 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.userType && connection.isInitiator)) {
                connection.onRequest(obj);
            } else _accept(obj);
        }

        function _accept(e) {
            if (connection.userType) {
                if (connection.direction === 'many-to-many') connection.busy = true;
                defaultSocket.send({
                    acceptedRequestOf: e.userid,
                    userid: connection.userid,
                    extra: connection.extra || {}
                });
            }

            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);
                    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 = sdp.replace(/inactive/g, 'sendrecv');
                        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 = sdp[0] + audio.replace(/inactive/g, 'sendrecv') + video;
                            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 = sdp[0] + audio + video.replace(/inactive/g, 'sendrecv');
                            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) {
                        self.onicecandidate(event.candidate);
                    }
                };

                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) {
                // 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 (bandwidth.screen && 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:' + 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 ? '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 && !isNodeWebkit) {
                    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) {
                var iceCandidate = new RTCIceCandidate({
                    sdpMLineIndex: candidate.sdpMLineIndex,
                    candidate: candidate.candidate
                });

                if (isNodeWebkit) {
                    this.connection.addIceCandidate(iceCandidate);
                } else {
                    // landed in chrome M33
                    // node-webkit doesn't support this format yet!
                    this.connection.addIceCandidate(iceCandidate, this.onIceSuccess, this.onIceFailure);
                }
            },
            onIceSuccess: function () {
                log('ice success', toStr(arguments));
            },
            onIceFailure: function () {
                warn('ice failure', toStr(arguments));
            },
            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);
                };

                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;

        // 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, 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 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 || 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 '<push> 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();
        };
    }

    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 = '<label>0%</label> <progress></progress>';
            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 = '<a href="' + file.url + '" target="_blank" download="' + file.name + '">' + file.name + '</a>';

            // 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;
            },
            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,
                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;

            // numberOfSessions

            if (callback) callback(connection.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 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 (!!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;

                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;
            }
        };

        // it is false because workaround that is used to capture connections' failures
        // affects renegotiation scenarios!
        // todo: fix it!
        connection.autoReDialOnFailure = false;

        connection.isInitiator = false;
    }
})();