Greasy Fork is available in English.
Library for comfortable using WebRTC technology.
此脚本不应直接安装,它是一个供其他脚本使用的外部库。如果您需要使用该库,请在脚本元属性加入:// @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:[email protected]:80?transport=udp', credential: 'homeo' }); iceServers.push({ url: 'turn:[email protected]: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; } })();