1 // Last time updated at Monday, December 21st, 2015, 5:25:26 PM
3 // Quick-Demo for newbies: http://jsfiddle.net/c46de0L8/
4 // Another simple demo: http://jsfiddle.net/zar6fg60/
6 // Latest file can be found here: https://cdn.webrtc-experiment.com/RTCMultiConnection.js
8 // Muaz Khan - www.MuazKhan.com
9 // MIT License - www.WebRTC-Experiment.com/licence
10 // Documentation - www.RTCMultiConnection.org/docs
11 // FAQ - www.RTCMultiConnection.org/FAQ
12 // Changes log - www.RTCMultiConnection.org/changes-log/
13 // Demos - www.WebRTC-Experiment.com/RTCMultiConnection
15 // _________________________
16 // RTCMultiConnection-v2.2.2
20 // RMC == RTCMultiConnection
21 // usually page-URL is used as channel-id
22 // you can always override it!
23 // www.RTCMultiConnection.org/docs/channel-id/
24 window.RMCDefaultChannel = location.href.replace(/\/|:|#|\?|\$|\^|%|\.|`|~|!|\+|@|\[|\||]|\|*. /g, '').split('\n').join('').split('\r').join('');
26 // www.RTCMultiConnection.org/docs/constructor/
27 window.RTCMultiConnection = function(channel) {
28 // an instance of constructor
29 var connection = this;
31 // a reference to RTCMultiSession
34 // setting default channel or channel passed through constructor
35 connection.channel = channel || RMCDefaultChannel;
37 // to allow single user to join multiple rooms;
38 // you can change this property at runtime!
39 connection.isAcceptNewSession = true;
41 // www.RTCMultiConnection.org/docs/open/
42 connection.open = function(args) {
43 connection.isAcceptNewSession = false;
45 // www.RTCMultiConnection.org/docs/session-initiator/
46 // you can always use this property to determine room owner!
47 connection.isInitiator = true;
49 var dontTransmit = false;
51 // a channel can contain multiple rooms i.e. sessions
54 connection.sessionid = args;
56 if (!isNull(args.transmitRoomOnce)) {
57 connection.transmitRoomOnce = args.transmitRoomOnce;
60 if (!isNull(args.dontTransmit)) {
61 dontTransmit = args.dontTransmit;
64 if (!isNull(args.sessionid)) {
65 connection.sessionid = args.sessionid;
70 // if firebase && if session initiator
71 if (connection.socket && connection.socket.remove) {
72 connection.socket.remove();
75 if (!connection.sessionid) connection.sessionid = connection.channel;
76 connection.sessionDescription = {
77 sessionid: connection.sessionid,
78 userid: connection.userid,
79 session: connection.session,
80 extra: connection.extra
83 if (!connection.sessionDescriptions[connection.sessionDescription.sessionid]) {
84 connection.numberOfSessions++;
85 connection.sessionDescriptions[connection.sessionDescription.sessionid] = connection.sessionDescription;
88 // connect with signaling channel
89 initRTCMultiSession(function() {
90 // "captureUserMediaOnDemand" is disabled by default.
91 // invoke "getUserMedia" only when first participant found.
92 rtcMultiSession.captureUserMediaOnDemand = args ? !!args.captureUserMediaOnDemand : false;
94 if (args && args.onMediaCaptured) {
95 connection.onMediaCaptured = args.onMediaCaptured;
98 // for session-initiator, user-media is captured as soon as "open" is invoked.
99 if (!rtcMultiSession.captureUserMediaOnDemand) captureUserMedia(function() {
100 rtcMultiSession.initSession({
101 sessionDescription: connection.sessionDescription,
102 dontTransmit: dontTransmit
105 invokeMediaCaptured(connection);
108 if (rtcMultiSession.captureUserMediaOnDemand) {
109 rtcMultiSession.initSession({
110 sessionDescription: connection.sessionDescription,
111 dontTransmit: dontTransmit
115 return connection.sessionDescription;
118 // www.RTCMultiConnection.org/docs/connect/
119 connection.connect = function(sessionid) {
120 // a channel can contain multiple rooms i.e. sessions
122 connection.sessionid = sessionid;
125 // connect with signaling channel
126 initRTCMultiSession(function() {
127 log('Signaling channel is ready.');
133 // www.RTCMultiConnection.org/docs/join/
134 connection.join = joinSession;
136 // www.RTCMultiConnection.org/docs/send/
137 connection.send = function(data, _channel) {
138 if (connection.numberOfConnectedUsers <= 0) {
140 setTimeout(function() {
142 connection.send(data, _channel);
147 // send file/data or /text
149 throw 'No file, data or text message to share.';
151 // connection.send([file1, file2, file3])
152 // you can share multiple files, strings or data objects using "send" method!
153 if (data instanceof Array && !isNull(data[0].size) && !isNull(data[0].type)) {
154 // this mechanism can cause failure for subsequent packets/data
155 // on Firefox especially; and on chrome as well!
156 // todo: need to use setTimeout instead.
157 for (var i = 0; i < data.length; i++) {
158 data[i].size && data[i].type && connection.send(data[i], _channel);
163 // File or Blob object MUST have "type" and "size" properties
164 if (!isNull(data.size) && !isNull(data.type)) {
165 if (!connection.enableFileSharing) {
166 throw '"enableFileSharing" boolean MUST be "true" to support file sharing.';
169 if (!rtcMultiSession.fileBufferReader) {
170 initFileBufferReader(connection, function(fbr) {
171 rtcMultiSession.fileBufferReader = fbr;
172 connection.send(data, _channel);
178 userid: connection.userid
179 }, data.extra || connection.extra);
181 rtcMultiSession.fileBufferReader.readAsArrayBuffer(data, function(uuid) {
182 rtcMultiSession.fileBufferReader.getNextChunk(uuid, function(nextChunk, isLastChunk, extra) {
183 if (_channel) _channel.send(nextChunk);
184 else rtcMultiSession.send(nextChunk);
188 // to allow longest string messages
189 // and largest data objects
190 // or anything of any size!
191 // to send multiple data objects concurrently!
195 channel: rtcMultiSession,
197 connection: connection
202 function initRTCMultiSession(onSignalingReady) {
207 // RTCMultiSession is the backbone object;
208 // this object MUST be initialized once!
209 if (rtcMultiSession) return onSignalingReady();
211 // your everything is passed over RTCMultiSession constructor!
212 rtcMultiSession = new RTCMultiSession(connection, onSignalingReady);
215 connection.disconnect = function() {
216 if (rtcMultiSession) rtcMultiSession.disconnect();
217 rtcMultiSession = null;
220 function joinSession(session, joinAs) {
221 if (isString(session)) {
222 connection.skipOnNewSession = true;
225 if (!rtcMultiSession) {
226 log('Signaling channel is not ready. Connecting...');
227 // connect with signaling channel
228 initRTCMultiSession(function() {
229 log('Signaling channel is connected. Joining the session again...');
230 setTimeout(function() {
231 joinSession(session, joinAs);
237 // connection.join('sessionid');
238 if (isString(session)) {
239 if (connection.sessionDescriptions[session]) {
240 session = connection.sessionDescriptions[session];
242 return setTimeout(function() {
243 log('Session-Descriptions not found. Rechecking..');
244 joinSession(session, joinAs);
248 // connection.join('sessionid', { audio: true });
250 return captureUserMedia(function() {
251 session.oneway = true;
252 joinSession(session);
256 if (!session || !session.userid || !session.sessionid) {
257 error('missing arguments', arguments);
259 var error = 'Invalid data passed over "connection.join" method.';
260 connection.onstatechange({
263 name: 'Unexpected data detected.',
270 if (!connection.dontOverrideSession) {
271 connection.session = session.session;
274 var extra = connection.extra || session.extra || {};
276 // todo: need to verify that if-block statement works as expected.
277 // expectations: if it is oneway streaming; or if it is data-only connection
278 // then, it shouldn't capture user-media on participant's side.
279 if (session.oneway || isData(session)) {
280 rtcMultiSession.joinSession(session, extra);
282 captureUserMedia(function() {
283 rtcMultiSession.joinSession(session, extra);
288 var isFirstSession = true;
290 // www.RTCMultiConnection.org/docs/captureUserMedia/
292 function captureUserMedia(callback, _session, dontCheckChromExtension) {
293 // capture user's media resources
294 var session = _session || connection.session;
296 if (isEmpty(session)) {
297 if (callback) callback();
301 // you can force to skip media capturing!
302 if (connection.dontCaptureUserMedia) {
306 // if it is data-only connection
307 // if it is one-way connection and current user is participant
308 if (isData(session) || (!connection.isInitiator && session.oneway)) {
309 // www.RTCMultiConnection.org/docs/attachStreams/
310 connection.attachStreams = [];
315 audio: !!session.audio ? {
318 chromeRenderToAssociatedSink: true
321 video: !!session.video
324 // if custom audio device is selected
325 if (connection._mediaSources.audio) {
326 constraints.audio.optional.push({
327 sourceId: connection._mediaSources.audio
331 // if custom video device is selected
332 if (connection._mediaSources.video) {
333 constraints.video = {
335 sourceId: connection._mediaSources.video
340 // for connection.session = {};
341 if (!session.screen && !constraints.audio && !constraints.video) {
345 var screen_constraints = {
349 chromeMediaSource: DetectRTC.screen.chromeMediaSource,
350 maxWidth: screen.width > 1920 ? screen.width : 1920,
351 maxHeight: screen.height > 1080 ? screen.height : 1080
357 if (isFirefox && session.screen) {
358 if (location.protocol !== 'https:') {
359 return error(SCREEN_COMMON_FAILURE);
361 warn(Firefox_Screen_Capturing_Warning);
363 screen_constraints.video = merge(screen_constraints.video.mandatory, {
364 mozMediaSource: 'window', // mozMediaSource is redundant here
365 mediaSource: 'window' // 'screen' || 'window'
368 // Firefox is supporting audio+screen from single getUserMedia request
369 // audio+video+screen will become audio+screen for Firefox
370 // because Firefox isn't supporting multi-streams feature version < 38
371 // version >38 supports multi-stream sharing.
372 // we can use: firefoxVersion < 38
373 // however capturing audio and screen using single getUserMedia is a better option
374 if (constraints.audio /* && !session.video */ ) {
375 screen_constraints.audio = true;
379 delete screen_constraints.video.chromeMediaSource;
382 // if screen is prompted
383 if (session.screen) {
384 if (isChrome && DetectRTC.screen.extensionid != ReservedExtensionID) {
385 useCustomChromeExtensionForScreenCapturing = true;
388 if (isChrome && !useCustomChromeExtensionForScreenCapturing && !dontCheckChromExtension && !DetectRTC.screen.sourceId) {
389 listenEventHandler('message', onIFrameCallback);
391 function onIFrameCallback(event) {
392 if (event.data && event.data.chromeMediaSourceId) {
393 // this event listener is no more needed
394 window.removeEventListener('message', onIFrameCallback);
396 var sourceId = event.data.chromeMediaSourceId;
398 DetectRTC.screen.sourceId = sourceId;
399 DetectRTC.screen.chromeMediaSource = 'desktop';
401 if (sourceId == 'PermissionDeniedError') {
402 var mediaStreamError = {
403 message: location.protocol == 'https:' ? 'User denied to share content of his screen.' : SCREEN_COMMON_FAILURE,
404 name: 'PermissionDeniedError',
405 constraintName: screen_constraints,
408 currentUserMediaRequest.mutex = false;
409 DetectRTC.screen.sourceId = null;
410 return connection.onMediaError(mediaStreamError);
413 captureUserMedia(callback, _session);
416 if (event.data && event.data.chromeExtensionStatus) {
417 warn('Screen capturing extension status is:', event.data.chromeExtensionStatus);
418 DetectRTC.screen.chromeMediaSource = 'screen';
419 captureUserMedia(callback, _session, true);
427 screenFrame.postMessage();
431 // check if screen capturing extension is installed.
432 if (isChrome && useCustomChromeExtensionForScreenCapturing && !dontCheckChromExtension && DetectRTC.screen.chromeMediaSource == 'screen' && DetectRTC.screen.extensionid) {
433 if (DetectRTC.screen.extensionid == ReservedExtensionID && document.domain.indexOf('webrtc-experiment.com') == -1) {
434 return captureUserMedia(callback, _session, true);
437 log('checking if chrome extension is installed.');
438 DetectRTC.screen.getChromeExtensionStatus(function(status) {
439 if (status == 'installed-enabled') {
440 DetectRTC.screen.chromeMediaSource = 'desktop';
443 captureUserMedia(callback, _session, true);
444 log('chrome extension is installed?', DetectRTC.screen.chromeMediaSource == 'desktop');
449 if (isChrome && useCustomChromeExtensionForScreenCapturing && DetectRTC.screen.chromeMediaSource == 'desktop' && !DetectRTC.screen.sourceId) {
450 DetectRTC.screen.getSourceId(function(sourceId) {
451 if (sourceId == 'PermissionDeniedError') {
452 var mediaStreamError = {
453 message: 'User denied to share content of his screen.',
454 name: 'PermissionDeniedError',
455 constraintName: screen_constraints,
458 currentUserMediaRequest.mutex = false;
459 DetectRTC.screen.chromeMediaSource = 'desktop';
460 return connection.onMediaError(mediaStreamError);
463 if (sourceId == 'No-Response') {
464 error('Chrome extension seems not available. Make sure that manifest.json#L16 has valid content-script matches pointing to your URL.');
465 DetectRTC.screen.chromeMediaSource = 'screen';
466 return captureUserMedia(callback, _session, true);
469 captureUserMedia(callback, _session, true);
474 if (isChrome && DetectRTC.screen.chromeMediaSource == 'desktop') {
475 screen_constraints.video.mandatory.chromeMediaSourceId = DetectRTC.screen.sourceId;
478 var _isFirstSession = isFirstSession;
480 _captureUserMedia(screen_constraints, constraints.audio || constraints.video ? function() {
482 if (_isFirstSession) isFirstSession = true;
484 _captureUserMedia(constraints, callback);
486 } else _captureUserMedia(constraints, callback, session.audio && !session.video);
488 function _captureUserMedia(forcedConstraints, forcedCallback, isRemoveVideoTracks, dontPreventSSLAutoAllowed) {
489 connection.onstatechange({
492 name: 'fetching-usermedia',
493 reason: 'About to capture user-media with constraints: ' + toStr(forcedConstraints)
497 if (connection.preventSSLAutoAllowed && !dontPreventSSLAutoAllowed && isChrome) {
498 // if navigator.customGetUserMediaBar.js is missing
499 if (!navigator.customGetUserMediaBar) {
500 loadScript(connection.resources.customGetUserMediaBar, function() {
501 _captureUserMedia(forcedConstraints, forcedCallback, isRemoveVideoTracks, dontPreventSSLAutoAllowed);
506 navigator.customGetUserMediaBar(forcedConstraints, function() {
507 _captureUserMedia(forcedConstraints, forcedCallback, isRemoveVideoTracks, true);
509 connection.onMediaError({
510 name: 'PermissionDeniedError',
511 message: 'User denied permission.',
512 constraintName: forcedConstraints,
520 onsuccess: function(stream, returnBack, idInstance, streamid) {
521 onStreamSuccessCallback(stream, returnBack, idInstance, streamid, forcedConstraints, forcedCallback, isRemoveVideoTracks, screen_constraints, constraints, session);
523 onerror: function(e, constraintUsed) {
524 // http://goo.gl/hrwF1a
526 if (e == 'PERMISSION_DENIED') {
529 name: 'PermissionDeniedError',
530 constraintName: constraintUsed,
536 if (isFirefox && constraintUsed.video && constraintUsed.video.mozMediaSource) {
538 message: Firefox_Screen_Capturing_Warning,
539 name: e.name || 'PermissionDeniedError',
540 constraintName: constraintUsed,
544 connection.onMediaError(mediaStreamError);
549 return connection.onMediaError({
550 message: 'Unknown Error',
552 constraintName: constraintUsed,
557 // it seems that chrome 35+ throws "DevicesNotFoundError" exception
558 // when any of the requested media is either denied or absent
559 if (e.name && (e.name == 'PermissionDeniedError' || e.name == 'DevicesNotFoundError')) {
560 var mediaStreamError = 'Either: ';
561 mediaStreamError += '\n Media resolutions are not permitted.';
562 mediaStreamError += '\n Another application is using same media device.';
563 mediaStreamError += '\n Media device is not attached or drivers not installed.';
564 mediaStreamError += '\n You denied access once and it is still denied.';
566 if (e.message && e.message.length) {
567 mediaStreamError += '\n ' + e.message;
571 message: mediaStreamError,
573 constraintName: constraintUsed,
577 connection.onMediaError(mediaStreamError);
579 if (isChrome && (session.audio || session.video)) {
580 // todo: this snippet fails if user has two or more
581 // microphone/webcam attached.
582 DetectRTC.load(function() {
583 // it is possible to check presence of the microphone before using it!
584 if (session.audio && !DetectRTC.hasMicrophone) {
585 warn('It seems that you have no microphone attached to your device/system.');
586 session.audio = session.audio = false;
588 if (!session.video) {
589 alert('It seems that you are capturing microphone and there is no device available or access is denied. Reloading...');
594 // it is possible to check presence of the webcam before using it!
595 if (session.video && !DetectRTC.hasWebcam) {
596 warn('It seems that you have no webcam attached to your device/system.');
597 session.video = session.video = false;
599 if (!session.audio) {
600 alert('It seems that you are capturing webcam and there is no device available or access is denied. Reloading...');
605 if (!DetectRTC.hasMicrophone && !DetectRTC.hasWebcam) {
606 alert('It seems that either both microphone/webcam are not available or access is denied. Reloading...');
608 } else if (!connection.getUserMediaPromptedOnce) {
609 // make maximum two tries!
610 connection.getUserMediaPromptedOnce = true;
611 captureUserMedia(callback, session);
617 if (e.name && e.name == 'ConstraintNotSatisfiedError') {
618 var mediaStreamError = 'Either: ';
619 mediaStreamError += '\n You are prompting unknown media resolutions.';
620 mediaStreamError += '\n You are using invalid media constraints.';
622 if (e.message && e.message.length) {
623 mediaStreamError += '\n ' + e.message;
627 message: mediaStreamError,
629 constraintName: constraintUsed,
633 connection.onMediaError(mediaStreamError);
636 if (session.screen) {
638 error(Firefox_Screen_Capturing_Warning);
639 } else if (location.protocol !== 'https:') {
640 if (!isNodeWebkit && (location.protocol == 'file:' || location.protocol == 'http:')) {
641 error('You cannot use HTTP or file protocol for screen capturing. You must either use HTTPs or chrome extension page or Node-Webkit page.');
644 error('Unable to detect actual issue. Maybe "deprecated" screen capturing flag was not set using command line or maybe you clicked "No" button or maybe chrome extension returned invalid "sourceId". Please install chrome-extension: http://bit.ly/webrtc-screen-extension');
648 currentUserMediaRequest.mutex = false;
650 // to make sure same stream can be captured again!
651 var idInstance = JSON.stringify(constraintUsed);
652 if (currentUserMediaRequest.streams[idInstance]) {
653 delete currentUserMediaRequest.streams[idInstance];
656 mediaConstraints: connection.mediaConstraints || {}
659 mediaConfig.constraints = forcedConstraints || constraints;
660 mediaConfig.connection = connection;
661 getUserMedia(mediaConfig);
665 function onStreamSuccessCallback(stream, returnBack, idInstance, streamid, forcedConstraints, forcedCallback, isRemoveVideoTracks, screen_constraints, constraints, session) {
666 if (!streamid) streamid = getRandomString();
668 connection.onstatechange({
671 name: 'usermedia-fetched',
672 reason: 'Captured user media using constraints: ' + toStr(forcedConstraints)
675 if (isRemoveVideoTracks) {
676 stream = convertToAudioStream(stream);
679 connection.localStreamids.push(streamid);
680 stream.onended = function() {
681 if (streamedObject.mediaElement && !streamedObject.mediaElement.parentNode && document.getElementById(stream.streamid)) {
682 streamedObject.mediaElement = document.getElementById(stream.streamid);
685 // when a stream is stopped; it must be removed from "attachStreams" array
686 connection.attachStreams.forEach(function(_stream, index) {
687 if (_stream == stream) {
688 delete connection.attachStreams[index];
689 connection.attachStreams = swap(connection.attachStreams);
693 onStreamEndedHandler(streamedObject, connection);
695 if (connection.streams[streamid]) {
696 connection.removeStream(streamid);
699 // if user clicks "stop" button to close screen sharing
700 var _stream = connection.streams[streamid];
701 if (_stream && _stream.sockets.length) {
702 _stream.sockets.forEach(function(socket) {
704 streamid: _stream.streamid,
710 currentUserMediaRequest.mutex = false;
711 // to make sure same stream can be captured again!
712 if (currentUserMediaRequest.streams[idInstance]) {
713 delete currentUserMediaRequest.streams[idInstance];
716 // to allow re-capturing of the screen
717 DetectRTC.screen.sourceId = null;
721 stream.streamid = streamid;
722 stream.isScreen = forcedConstraints == screen_constraints;
723 stream.isVideo = forcedConstraints == constraints && !!constraints.video;
724 stream.isAudio = forcedConstraints == constraints && !!constraints.audio && !constraints.video;
726 // if muted stream is negotiated
728 audio: stream.getAudioTracks().length && !stream.getAudioTracks()[0].enabled,
729 video: stream.getVideoTracks().length && !stream.getVideoTracks()[0].enabled
733 var mediaElement = createMediaElement(stream, session);
734 mediaElement.muted = true;
736 var streamedObject = {
739 mediaElement: mediaElement,
740 blobURL: mediaElement.mozSrcObject ? URL.createObjectURL(stream) : mediaElement.src,
742 userid: connection.userid,
743 extra: connection.extra,
745 isVideo: !!stream.isVideo,
746 isAudio: !!stream.isAudio,
747 isScreen: !!stream.isScreen,
748 isInitiator: !!connection.isInitiator,
749 rtcMultiConnection: connection
752 if (isFirstSession) {
753 connection.attachStreams.push(stream);
755 isFirstSession = false;
757 connection.streams[streamid] = connection._getStream(streamedObject);
760 connection.onstream(streamedObject);
763 if (connection.setDefaultEventsForMediaElement) {
764 connection.setDefaultEventsForMediaElement(mediaElement, streamid);
767 if (forcedCallback) forcedCallback(stream, streamedObject);
769 if (connection.onspeaking) {
772 streamedObject: streamedObject,
773 connection: connection
778 // www.RTCMultiConnection.org/docs/captureUserMedia/
779 connection.captureUserMedia = captureUserMedia;
781 // www.RTCMultiConnection.org/docs/leave/
782 connection.leave = function(userid) {
783 if (!rtcMultiSession) return;
785 isFirstSession = true;
788 connection.eject(userid);
792 rtcMultiSession.leave();
795 // www.RTCMultiConnection.org/docs/eject/
796 connection.eject = function(userid) {
797 if (!connection.isInitiator) throw 'Only session-initiator can eject a user.';
798 if (!connection.peers[userid]) throw 'You ejected invalid user.';
799 connection.peers[userid].sendCustomMessage({
804 // www.RTCMultiConnection.org/docs/close/
805 connection.close = function() {
806 // close entire session
807 connection.autoCloseEntireSession = true;
811 // www.RTCMultiConnection.org/docs/renegotiate/
812 connection.renegotiate = function(stream, session) {
813 if (connection.numberOfConnectedUsers <= 0) {
815 setTimeout(function() {
817 connection.renegotiate(stream, session);
822 rtcMultiSession.addStream({
823 renegotiate: session || merge({
825 }, connection.session),
830 connection.attachExternalStream = function(stream, isScreen) {
831 var constraints = {};
832 if (stream.getAudioTracks && stream.getAudioTracks().length) {
833 constraints.audio = true;
835 if (stream.getVideoTracks && stream.getVideoTracks().length) {
836 constraints.video = true;
839 var screen_constraints = {
841 chromeMediaSource: 'fake'
844 var forcedConstraints = isScreen ? screen_constraints : constraints;
845 onStreamSuccessCallback(stream, false, '', null, forcedConstraints, false, false, screen_constraints, constraints, constraints);
848 // www.RTCMultiConnection.org/docs/addStream/
849 connection.addStream = function(session, socket) {
850 // www.RTCMultiConnection.org/docs/renegotiation/
852 if (connection.numberOfConnectedUsers <= 0) {
854 setTimeout(function() {
856 connection.addStream(session, socket);
861 // renegotiate new media stream
863 var isOneWayStreamFromParticipant;
864 if (!connection.isInitiator && session.oneway) {
865 session.oneway = false;
866 isOneWayStreamFromParticipant = true;
869 captureUserMedia(function(stream) {
870 if (isOneWayStreamFromParticipant) {
871 session.oneway = true;
877 function addStream(stream) {
878 rtcMultiSession.addStream({
880 renegotiate: session || connection.session,
886 // www.RTCMultiConnection.org/docs/removeStream/
887 connection.removeStream = function(streamid, dontRenegotiate) {
888 if (connection.numberOfConnectedUsers <= 0) {
890 setTimeout(function() {
892 connection.removeStream(streamid, dontRenegotiate);
897 if (!streamid) streamid = 'all';
898 if (!isString(streamid) || streamid.search(/all|audio|video|screen/gi) != -1) {
899 function _detachStream(_stream, config) {
900 if (config.local && _stream.type != 'local') return;
901 if (config.remote && _stream.type != 'remote') return;
903 // connection.removeStream({screen:true});
904 if (config.screen && !!_stream.isScreen) {
905 connection.detachStreams.push(_stream.streamid);
908 // connection.removeStream({audio:true});
909 if (config.audio && !!_stream.isAudio) {
910 connection.detachStreams.push(_stream.streamid);
913 // connection.removeStream({video:true});
914 if (config.video && !!_stream.isVideo) {
915 connection.detachStreams.push(_stream.streamid);
918 // connection.removeStream({});
919 if (!config.audio && !config.video && !config.screen) {
920 connection.detachStreams.push(_stream.streamid);
923 if (connection.detachStreams.indexOf(_stream.streamid) != -1) {
924 log('removing stream', _stream.streamid);
925 onStreamEndedHandler(_stream, connection);
928 connection.stopMediaStream(_stream.stream);
933 for (var stream in connection.streams) {
934 if (connection._skip.indexOf(stream) == -1) {
935 _stream = connection.streams[stream];
937 if (streamid == 'all') _detachStream(_stream, {
943 else if (isString(streamid)) {
944 // connection.removeStream('screen');
946 config[streamid] = true;
947 _detachStream(_stream, config);
948 } else _detachStream(_stream, streamid);
952 if (!dontRenegotiate && connection.detachStreams.length) {
953 connection.renegotiate();
959 var stream = connection.streams[streamid];
961 // detach pre-attached streams
962 if (!stream) return warn('No such stream exists. Stream-id:', streamid);
964 // www.RTCMultiConnection.org/docs/detachStreams/
965 connection.detachStreams.push(stream.streamid);
967 log('removing stream', stream.streamid);
968 onStreamEndedHandler(stream, connection);
970 // todo: how to allow "stop" function?
971 // connection.stopMediaStream(stream.stream)
973 if (!dontRenegotiate) {
974 connection.renegotiate();
978 connection.switchStream = function(session) {
979 if (connection.numberOfConnectedUsers <= 0) {
981 setTimeout(function() {
983 connection.switchStream(session);
988 connection.removeStream('all', true);
989 connection.addStream(session);
992 // www.RTCMultiConnection.org/docs/sendCustomMessage/
993 connection.sendCustomMessage = function(message) {
994 if (!rtcMultiSession || !rtcMultiSession.defaultSocket) {
995 return setTimeout(function() {
996 connection.sendCustomMessage(message);
1000 rtcMultiSession.defaultSocket.send({
1001 customMessage: true,
1006 // set RTCMultiConnection defaults on constructor invocation
1007 setDefaults(connection);
1010 function RTCMultiSession(connection, callbackForSignalingReady) {
1011 var socketObjects = {};
1013 var rtcMultiSession = this;
1014 var participants = {};
1016 if (!rtcMultiSession.fileBufferReader && connection.session.data && connection.enableFileSharing) {
1017 initFileBufferReader(connection, function(fbr) {
1018 rtcMultiSession.fileBufferReader = fbr;
1022 var textReceiver = new TextReceiver(connection);
1024 function onDataChannelMessage(e) {
1025 if (e.data.checkingPresence && connection.channels[e.userid]) {
1026 connection.channels[e.userid].send({
1027 presenceDetected: true
1032 if (e.data.presenceDetected && connection.peers[e.userid]) {
1033 connection.peers[e.userid].connected = true;
1037 if (e.data.type === 'text') {
1038 textReceiver.receive(e.data, e.userid, e.extra);
1040 if (connection.autoTranslateText) {
1041 e.original = e.data;
1042 connection.Translator.TranslateText(e.data, function(translatedText) {
1043 e.data = translatedText;
1044 connection.onmessage(e);
1046 } else connection.onmessage(e);
1050 function onNewSession(session) {
1051 if (connection.skipOnNewSession) return;
1053 if (!session.session) session.session = {};
1054 if (!session.extra) session.extra = {};
1056 // todo: make sure this works as expected.
1057 // i.e. "onNewSession" should be fired only for
1058 // sessionid that is passed over "connect" method.
1059 if (connection.sessionid && session.sessionid != connection.sessionid) return;
1061 if (connection.onNewSession) {
1062 session.join = function(forceSession) {
1063 if (!forceSession) return connection.join(session);
1065 for (var f in forceSession) {
1066 session.session[f] = forceSession[f];
1069 // keeping previous state
1070 var isDontCaptureUserMedia = connection.dontCaptureUserMedia;
1072 connection.dontCaptureUserMedia = false;
1073 connection.captureUserMedia(function() {
1074 connection.dontCaptureUserMedia = true;
1075 connection.join(session);
1077 // returning back previous state
1078 connection.dontCaptureUserMedia = isDontCaptureUserMedia;
1081 if (!session.extra) session.extra = {};
1083 return connection.onNewSession(session);
1086 connection.join(session);
1089 function updateSocketForLocalStreams(socket) {
1090 for (var i = 0; i < connection.localStreamids.length; i++) {
1091 var streamid = connection.localStreamids[i];
1092 if (connection.streams[streamid]) {
1093 // using "sockets" array to keep references of all sockets using
1094 // this media stream; so we can fire "onStreamEndedHandler" among all users.
1095 connection.streams[streamid].sockets.push(socket);
1100 function newPrivateSocket(_config) {
1101 var socketConfig = {
1102 channel: _config.channel,
1103 onmessage: socketResponse,
1104 onopen: function(_socket) {
1105 if (_socket) socket = _socket;
1107 if (isofferer && !peer) {
1108 peerConfig.session = connection.session;
1109 if (!peer) peer = new PeerConnection();
1110 peer.create('offer', peerConfig);
1113 _config.socketIndex = socket.index = sockets.length;
1114 socketObjects[socketConfig.channel] = socket;
1115 sockets[_config.socketIndex] = socket;
1117 updateSocketForLocalStreams(socket);
1119 if (!socket.__push) {
1120 socket.__push = socket.send;
1121 socket.send = function(message) {
1122 message.userid = message.userid || connection.userid;
1123 message.extra = message.extra || connection.extra || {};
1125 socket.__push(message);
1131 socketConfig.callback = function(_socket) {
1133 socketConfig.onopen();
1136 var socket = connection.openSignalingChannel(socketConfig);
1137 if (socket) socketConfig.onopen(socket);
1139 var isofferer = _config.isofferer,
1143 onopen: onChannelOpened,
1144 onicecandidate: function(candidate) {
1145 if (!connection.candidates) throw 'ICE candidates are mandatory.';
1146 if (!connection.iceProtocols) throw 'At least one must be true; UDP or TCP.';
1148 var iceCandidates = connection.candidates;
1150 var stun = iceCandidates.stun;
1151 var turn = iceCandidates.turn;
1153 if (!isNull(iceCandidates.reflexive)) stun = iceCandidates.reflexive;
1154 if (!isNull(iceCandidates.relay)) turn = iceCandidates.relay;
1156 if (!iceCandidates.host && !!candidate.candidate.match(/a=candidate.*typ host/g)) return;
1157 if (!turn && !!candidate.candidate.match(/a=candidate.*typ relay/g)) return;
1158 if (!stun && !!candidate.candidate.match(/a=candidate.*typ srflx/g)) return;
1160 var protocol = connection.iceProtocols;
1162 if (!protocol.udp && !!candidate.candidate.match(/a=candidate.* udp/g)) return;
1163 if (!protocol.tcp && !!candidate.candidate.match(/a=candidate.* tcp/g)) return;
1165 if (!window.selfNPObject) window.selfNPObject = candidate;
1167 socket && socket.send({
1168 candidate: JSON.stringify({
1169 candidate: candidate.candidate,
1170 sdpMid: candidate.sdpMid,
1171 sdpMLineIndex: candidate.sdpMLineIndex
1175 onmessage: function(data) {
1178 var abToStr = ab2str(data);
1179 if (abToStr.indexOf('"userid":') != -1) {
1180 abToStr = JSON.parse(abToStr);
1181 onDataChannelMessage(abToStr);
1182 } else if (data instanceof ArrayBuffer || data instanceof DataView) {
1183 if (!connection.enableFileSharing) {
1184 throw 'It seems that receiving data is either "Blob" or "File" but file sharing is disabled.';
1187 if (!rtcMultiSession.fileBufferReader) {
1189 initFileBufferReader(connection, function(fbr) {
1190 rtcMultiSession.fileBufferReader = fbr;
1191 that.onmessage(data);
1196 var fileBufferReader = rtcMultiSession.fileBufferReader;
1198 fileBufferReader.convertToObject(data, function(chunk) {
1199 if (chunk.maxChunks || chunk.readyForNextChunk) {
1200 // if target peer requested next chunk
1201 if (chunk.readyForNextChunk) {
1202 fileBufferReader.getNextChunk(chunk.uuid, function(nextChunk, isLastChunk, extra) {
1203 rtcMultiSession.send(nextChunk);
1208 // if chunk is received
1209 fileBufferReader.addChunk(chunk, function(promptNextChunk) {
1210 // request next chunk
1211 rtcMultiSession.send(promptNextChunk);
1216 connection.onmessage({
1218 userid: _config.userid,
1219 extra: _config.extra
1225 onaddstream: function(stream, session) {
1226 session = session || _config.renegotiate || connection.session;
1228 // if it is data-only connection; then return.
1229 if (isData(session)) return;
1231 if (session.screen && (session.audio || session.video)) {
1232 if (!_config.gotAudioOrVideo) {
1233 // audio/video are fired earlier than screen
1234 _config.gotAudioOrVideo = true;
1235 session.screen = false;
1237 // screen stream is always fired later
1238 session.audio = false;
1239 session.video = false;
1245 if (_config.streaminfo) {
1246 var streaminfo = _config.streaminfo.split('----');
1247 var strInfo = JSON.parse(streaminfo[streaminfo.length - 1]);
1250 stream.streamid = strInfo.streamid;
1251 stream.isScreen = !!strInfo.isScreen;
1252 stream.isVideo = !!strInfo.isVideo;
1253 stream.isAudio = !!strInfo.isAudio;
1254 preMuted = strInfo.preMuted;
1258 _config.streaminfo = streaminfo.join('----');
1261 var mediaElement = createMediaElement(stream, merge({
1265 if (connection.setDefaultEventsForMediaElement) {
1266 connection.setDefaultEventsForMediaElement(mediaElement, stream.streamid);
1269 if (!isPluginRTC && !stream.getVideoTracks().length) {
1270 function eventListener() {
1271 setTimeout(function() {
1272 mediaElement.muted = false;
1273 afterRemoteStreamStartedFlowing({
1274 mediaElement: mediaElement,
1281 mediaElement.removeEventListener('play', eventListener);
1283 return mediaElement.addEventListener('play', eventListener, false);
1286 waitUntilRemoteStreamStartsFlowing({
1287 mediaElement: mediaElement,
1294 onremovestream: function(stream) {
1295 if (stream && stream.streamid) {
1296 stream = connection.streams[stream.streamid];
1298 log('on:stream:ended via on:remove:stream', stream);
1299 onStreamEndedHandler(stream, connection);
1301 } else log('on:remove:stream', stream);
1304 onclose: function(e) {
1305 e.extra = _config.extra;
1306 e.userid = _config.userid;
1307 connection.onclose(e);
1309 // suggested in #71 by "efaj"
1310 if (connection.channels[e.userid]) {
1311 delete connection.channels[e.userid];
1314 onerror: function(e) {
1315 e.extra = _config.extra;
1316 e.userid = _config.userid;
1317 connection.onerror(e);
1320 oniceconnectionstatechange: function(event) {
1321 log('oniceconnectionstatechange', toStr(event));
1323 if (peer.connection && peer.connection.iceConnectionState == 'connected' && peer.connection.iceGatheringState == 'complete' && peer.connection.signalingState == 'stable' && connection.numberOfConnectedUsers == 1) {
1324 connection.onconnected({
1325 userid: _config.userid,
1326 extra: _config.extra,
1327 peer: connection.peers[_config.userid],
1328 targetuser: _config.userinfo
1332 if (!connection.isInitiator && peer.connection && peer.connection.iceConnectionState == 'connected' && peer.connection.iceGatheringState == 'complete' && peer.connection.signalingState == 'stable' && connection.numberOfConnectedUsers == 1) {
1333 connection.onstatechange({
1334 userid: _config.userid,
1335 extra: _config.extra,
1336 name: 'connected-with-initiator',
1337 reason: 'ICE connection state seems connected; gathering state is completed; and signaling state is stable.'
1341 if (connection.peers[_config.userid] && connection.peers[_config.userid].oniceconnectionstatechange) {
1342 connection.peers[_config.userid].oniceconnectionstatechange(event);
1345 // if ICE connectivity check is failed; renegotiate or redial
1346 if (connection.peers[_config.userid] && connection.peers[_config.userid].peer.connection.iceConnectionState == 'failed') {
1347 connection.onfailed({
1348 userid: _config.userid,
1349 extra: _config.extra,
1350 peer: connection.peers[_config.userid],
1351 targetuser: _config.userinfo
1355 if (connection.peers[_config.userid] && connection.peers[_config.userid].peer.connection.iceConnectionState == 'disconnected') {
1356 !peer.connection.renegotiate && connection.ondisconnected({
1357 userid: _config.userid,
1358 extra: _config.extra,
1359 peer: connection.peers[_config.userid],
1360 targetuser: _config.userinfo
1362 peer.connection.renegotiate = false;
1365 if (!connection.autoReDialOnFailure) return;
1367 if (connection.peers[_config.userid]) {
1368 if (connection.peers[_config.userid].peer.connection.iceConnectionState != 'disconnected') {
1369 _config.redialing = false;
1372 if (connection.peers[_config.userid].peer.connection.iceConnectionState == 'disconnected' && !_config.redialing) {
1373 _config.redialing = true;
1374 warn('Peer connection is closed.', toStr(connection.peers[_config.userid].peer.connection), 'ReDialing..');
1375 connection.peers[_config.userid].socket.send({
1379 // to make sure all old "remote" streams are also removed!
1380 connection.streams.remove({
1382 userid: _config.userid
1388 onsignalingstatechange: function(event) {
1389 log('onsignalingstatechange', toStr(event));
1392 attachStreams: connection.dontAttachStream ? [] : connection.attachStreams,
1393 iceServers: connection.iceServers,
1394 rtcConfiguration: connection.rtcConfiguration,
1395 bandwidth: connection.bandwidth,
1396 sdpConstraints: connection.sdpConstraints,
1397 optionalArgument: connection.optionalArgument,
1398 disableDtlsSrtp: connection.disableDtlsSrtp,
1399 dataChannelDict: connection.dataChannelDict,
1400 preferSCTP: connection.preferSCTP,
1402 onSessionDescription: function(sessionDescription, streaminfo) {
1404 sdp: sessionDescription,
1406 streaminfo: streaminfo
1409 trickleIce: connection.trickleIce,
1410 processSdp: connection.processSdp,
1411 sendStreamId: function(stream) {
1412 socket && socket.send({
1413 streamid: stream.streamid,
1414 isScreen: !!stream.isScreen,
1415 isAudio: !!stream.isAudio,
1416 isVideo: !!stream.isVideo
1419 rtcMultiConnection: connection
1422 function waitUntilRemoteStreamStartsFlowing(args) {
1423 // chrome for android may have some features missing
1424 if (isMobileDevice || isPluginRTC || (isNull(connection.waitUntilRemoteStreamStartsFlowing) || !connection.waitUntilRemoteStreamStartsFlowing)) {
1425 return afterRemoteStreamStartedFlowing(args);
1428 if (!args.numberOfTimes) args.numberOfTimes = 0;
1429 args.numberOfTimes++;
1431 if (!(args.mediaElement.readyState <= HTMLMediaElement.HAVE_CURRENT_DATA || args.mediaElement.paused || args.mediaElement.currentTime <= 0)) {
1432 return afterRemoteStreamStartedFlowing(args);
1435 if (args.numberOfTimes >= 60) { // wait 60 seconds while video is delivered!
1436 return socket.send({
1437 failedToReceiveRemoteVideo: true,
1438 streamid: args.stream.streamid
1442 setTimeout(function() {
1443 log('Waiting for incoming remote stream to be started flowing: ' + args.numberOfTimes + ' seconds.');
1444 waitUntilRemoteStreamStartsFlowing(args);
1448 function initFakeChannel() {
1449 if (!connection.fakeDataChannels || connection.channels[_config.userid]) return;
1451 // for non-data connections; allow fake data sender!
1452 if (!connection.session.data) {
1454 send: function(data) {
1461 // connection.channels['user-id'].send(data);
1462 connection.channels[_config.userid] = {
1463 channel: fakeChannel,
1464 send: function(data) {
1465 this.channel.send(data);
1468 peerConfig.onopen(fakeChannel);
1472 function afterRemoteStreamStartedFlowing(args) {
1473 var mediaElement = args.mediaElement;
1474 var session = args.session;
1475 var stream = args.stream;
1477 stream.onended = function() {
1478 if (streamedObject.mediaElement && !streamedObject.mediaElement.parentNode && document.getElementById(stream.streamid)) {
1479 streamedObject.mediaElement = document.getElementById(stream.streamid);
1482 onStreamEndedHandler(streamedObject, connection);
1485 var streamedObject = {
1486 mediaElement: mediaElement,
1489 streamid: stream.streamid,
1490 session: session || connection.session,
1492 blobURL: isPluginRTC ? '' : mediaElement.mozSrcObject ? URL.createObjectURL(stream) : mediaElement.src,
1495 extra: _config.extra,
1496 userid: _config.userid,
1498 isVideo: isPluginRTC ? !!session.video : !!stream.isVideo,
1499 isAudio: isPluginRTC ? !!session.audio && !session.video : !!stream.isAudio,
1500 isScreen: !!stream.isScreen,
1501 isInitiator: !!_config.isInitiator,
1503 rtcMultiConnection: connection,
1507 // connection.streams['stream-id'].mute({audio:true})
1508 connection.streams[stream.streamid] = connection._getStream(streamedObject);
1509 connection.onstream(streamedObject);
1511 if (!isEmpty(args.preMuted) && (args.preMuted.audio || args.preMuted.video)) {
1512 var fakeObject = merge({}, streamedObject);
1513 fakeObject.session = merge(fakeObject.session, args.preMuted);
1515 fakeObject.isAudio = !!fakeObject.session.audio && !fakeObject.session.video;
1516 fakeObject.isVideo = !!fakeObject.session.video;
1517 fakeObject.isScreen = false;
1519 connection.onmute(fakeObject);
1522 log('on:add:stream', streamedObject);
1526 if (connection.onspeaking) {
1529 streamedObject: streamedObject,
1530 connection: connection
1535 function onChannelOpened(channel) {
1536 _config.channel = channel;
1538 // connection.channels['user-id'].send(data);
1539 connection.channels[_config.userid] = {
1540 channel: _config.channel,
1541 send: function(data) {
1542 connection.send(data, this.channel);
1547 extra: _config.extra,
1548 userid: _config.userid,
1552 // fetch files from file-queue
1553 for (var q in connection.fileQueue) {
1554 connection.send(connection.fileQueue[q], channel);
1557 if (isData(connection.session)) onSessionOpened();
1559 if (connection.partOfScreen && connection.partOfScreen.sharing) {
1560 connection.peers[_config.userid].sharePartOfScreen(connection.partOfScreen);
1564 function updateSocket() {
1565 // todo: need to check following {if-block} MUST not affect "redial" process
1566 if (socket.userid == _config.userid)
1569 socket.userid = _config.userid;
1570 sockets[_config.socketIndex] = socket;
1572 connection.numberOfConnectedUsers++;
1573 // connection.peers['user-id'].addStream({audio:true})
1574 connection.peers[_config.userid] = {
1577 userid: _config.userid,
1578 extra: _config.extra,
1579 userinfo: _config.userinfo,
1580 addStream: function(session00) {
1581 // connection.peers['user-id'].addStream({audio: true, video: true);
1583 connection.addStream(session00, this.socket);
1585 removeStream: function(streamid) {
1586 if (!connection.streams[streamid])
1587 return warn('No such stream exists. Stream-id:', streamid);
1589 this.peer.connection.removeStream(connection.streams[streamid].stream);
1592 renegotiate: function(stream, session) {
1593 // connection.peers['user-id'].renegotiate();
1595 connection.renegotiate(stream, session);
1597 changeBandwidth: function(bandwidth) {
1598 // connection.peers['user-id'].changeBandwidth();
1600 if (!bandwidth) throw 'You MUST pass bandwidth object.';
1601 if (isString(bandwidth)) throw 'Pass object for bandwidth instead of string; e.g. {audio:10, video:20}';
1603 // set bandwidth for self
1604 this.peer.bandwidth = bandwidth;
1606 // ask remote user to synchronize bandwidth
1608 changeBandwidth: true,
1609 bandwidth: bandwidth
1612 sendCustomMessage: function(message) {
1613 // connection.peers['user-id'].sendCustomMessage();
1616 customMessage: true,
1620 onCustomMessage: function(message) {
1621 log('Received "private" message from', this.userid,
1622 isString(message) ? message : toStr(message));
1624 drop: function(dontSendMessage) {
1625 // connection.peers['user-id'].drop();
1627 for (var stream in connection.streams) {
1628 if (connection._skip.indexOf(stream) == -1) {
1629 stream = connection.streams[stream];
1631 if (stream.userid == connection.userid && stream.type == 'local') {
1632 this.peer.connection.removeStream(stream.stream);
1633 onStreamEndedHandler(stream, connection);
1636 if (stream.type == 'remote' && stream.userid == this.userid) {
1637 onStreamEndedHandler(stream, connection);
1642 !dontSendMessage && this.socket.send({
1646 hold: function(holdMLine) {
1647 // connection.peers['user-id'].hold();
1649 if (peer.prevCreateType == 'answer') {
1652 holdMLine: holdMLine || 'both',
1660 holdMLine: holdMLine || 'both'
1663 this.peer.hold = true;
1664 this.fireHoldUnHoldEvents({
1667 userid: connection.userid,
1668 remoteUser: this.userid
1671 unhold: function(holdMLine) {
1672 // connection.peers['user-id'].unhold();
1674 if (peer.prevCreateType == 'answer') {
1677 holdMLine: holdMLine || 'both',
1685 holdMLine: holdMLine || 'both'
1688 this.peer.hold = false;
1689 this.fireHoldUnHoldEvents({
1692 userid: connection.userid,
1693 remoteUser: this.userid
1696 fireHoldUnHoldEvents: function(e) {
1697 // this method is for inner usages only!
1699 var isHold = e.isHold;
1701 var userid = e.remoteUser || e.userid;
1703 // hold means inactive a specific media line!
1704 // a media line can contain multiple synced sources (ssrc)
1705 // i.e. a media line can reference multiple tracks!
1706 // that's why hold will affect all relevant tracks in a specific media line!
1707 for (var stream in connection.streams) {
1708 if (connection._skip.indexOf(stream) == -1) {
1709 stream = connection.streams[stream];
1711 if (stream.userid == userid) {
1712 // www.RTCMultiConnection.org/docs/onhold/
1714 connection.onhold(merge({
1718 // www.RTCMultiConnection.org/docs/onunhold/
1720 connection.onunhold(merge({
1727 redial: function() {
1728 // connection.peers['user-id'].redial();
1730 // 1st of all; remove all relevant remote media streams
1731 for (var stream in connection.streams) {
1732 if (connection._skip.indexOf(stream) == -1) {
1733 stream = connection.streams[stream];
1735 if (stream.userid == this.userid && stream.type == 'remote') {
1736 onStreamEndedHandler(stream, connection);
1741 log('ReDialing...');
1747 peer = new PeerConnection();
1748 peer.create('offer', peerConfig);
1750 sharePartOfScreen: function(args) {
1751 // www.RTCMultiConnection.org/docs/onpartofscreen/
1753 var lastScreenshot = '';
1755 function partOfScreenCapturer() {
1757 if (that.stopPartOfScreenSharing) {
1758 that.stopPartOfScreenSharing = false;
1760 if (connection.onpartofscreenstopped) {
1761 connection.onpartofscreenstopped();
1767 if (that.pausePartOfScreenSharing) {
1768 if (connection.onpartofscreenpaused) {
1769 connection.onpartofscreenpaused();
1772 return setTimeout(partOfScreenCapturer, args.interval || 200);
1775 capturePartOfScreen({
1776 element: args.element,
1777 connection: connection,
1778 callback: function(screenshot) {
1779 if (!connection.channels[that.userid]) {
1780 throw 'No such data channel exists.';
1783 // don't share repeated content
1784 if (screenshot != lastScreenshot) {
1785 lastScreenshot = screenshot;
1786 connection.channels[that.userid].send({
1787 screenshot: screenshot,
1788 isPartOfScreen: true
1792 // "once" can be used to share single screenshot
1793 !args.once && setTimeout(partOfScreenCapturer, args.interval || 200);
1798 partOfScreenCapturer();
1800 getConnectionStats: function(callback, interval) {
1801 if (!callback) throw 'callback is mandatory.';
1803 if (!window.getConnectionStats) {
1804 loadScript(connection.resources.getConnectionStats, invoker);
1807 function invoker() {
1808 RTCPeerConnection.prototype.getConnectionStats = window.getConnectionStats;
1809 peer.connection && peer.connection.getConnectionStats(callback, interval);
1812 takeSnapshot: function(callback) {
1814 userid: this.userid,
1815 connection: connection,
1822 function onSessionOpened() {
1823 // original conferencing infrastructure!
1824 if (connection.isInitiator && getLength(participants) && getLength(participants) <= connection.maxParticipantsAllowed) {
1825 if (!connection.session.oneway && !connection.session.broadcast) {
1826 defaultSocket.send({
1827 sessionid: connection.sessionid,
1828 newParticipant: _config.userid || socket.channel,
1830 userid: _config.userid || socket.channel,
1831 extra: _config.extra
1837 // 1st: renegotiation is supported only on chrome
1838 // 2nd: must not renegotiate same media multiple times
1839 // 3rd: todo: make sure that target-user has no such "renegotiated" media.
1840 if (_config.userinfo.browser == 'chrome' && !_config.renegotiatedOnce) {
1841 // this code snippet is added to make sure that "previously-renegotiated" streams are also
1842 // renegotiated to this new user
1843 for (var rSession in connection.renegotiatedSessions) {
1844 _config.renegotiatedOnce = true;
1846 if (connection.renegotiatedSessions[rSession] && connection.renegotiatedSessions[rSession].stream) {
1847 connection.peers[_config.userid].renegotiate(connection.renegotiatedSessions[rSession].stream, connection.renegotiatedSessions[rSession].session);
1853 function socketResponse(response) {
1854 if (isRMSDeleted) return;
1856 if (response.userid == connection.userid)
1860 _config.userid = response.userid;
1861 _config.extra = response.extra || {};
1862 _config.renegotiate = response.renegotiate;
1863 _config.streaminfo = response.streaminfo;
1864 _config.isInitiator = response.isInitiator;
1865 _config.userinfo = response.userinfo;
1867 var sdp = JSON.parse(response.sdp);
1869 if (sdp.type == 'offer') {
1870 // to synchronize SCTP or RTP
1871 peerConfig.preferSCTP = !!response.preferSCTP;
1872 connection.fakeDataChannels = !!response.fakeDataChannels;
1875 // initializing fake channel
1878 sdpInvoker(sdp, response.labels);
1881 if (response.candidate) {
1882 peer && peer.addIceCandidate(JSON.parse(response.candidate));
1885 if (response.streamid) {
1886 if (!rtcMultiSession.streamids) {
1887 rtcMultiSession.streamids = {};
1889 if (!rtcMultiSession.streamids[response.streamid]) {
1890 rtcMultiSession.streamids[response.streamid] = response.streamid;
1891 connection.onstreamid(response);
1895 if (response.mute || response.unmute) {
1896 if (response.promptMuteUnmute) {
1897 if (!connection.privileges.canMuteRemoteStream) {
1898 connection.onstatechange({
1899 userid: response.userid,
1900 extra: response.extra,
1901 name: 'mute-request-denied',
1902 reason: response.userid + ' tried to mute your stream; however "privileges.canMuteRemoteStream" is "false".'
1907 if (connection.streams[response.streamid]) {
1908 if (response.mute && !connection.streams[response.streamid].muted) {
1909 connection.streams[response.streamid].mute(response.session);
1911 if (response.unmute && connection.streams[response.streamid].muted) {
1912 connection.streams[response.streamid].unmute(response.session);
1916 var streamObject = {};
1917 if (connection.streams[response.streamid]) {
1918 streamObject = connection.streams[response.streamid];
1921 var session = response.session;
1922 var fakeObject = merge({}, streamObject);
1923 fakeObject.session = session;
1925 fakeObject.isAudio = !!fakeObject.session.audio && !fakeObject.session.video;
1926 fakeObject.isVideo = !!fakeObject.session.video;
1927 fakeObject.isScreen = !!fakeObject.session.screen;
1929 if (response.mute) connection.onmute(fakeObject || response);
1930 if (response.unmute) connection.onunmute(fakeObject || response);
1934 if (response.isVolumeChanged) {
1935 log('Volume of stream: ' + response.streamid + ' has changed to: ' + response.volume);
1936 if (connection.streams[response.streamid]) {
1937 var mediaElement = connection.streams[response.streamid].mediaElement;
1938 if (mediaElement) mediaElement.volume = response.volume;
1942 // to stop local stream
1943 if (response.stopped) {
1944 if (connection.streams[response.streamid]) {
1945 onStreamEndedHandler(connection.streams[response.streamid], connection);
1949 // to stop remote stream
1950 if (response.promptStreamStop /* && !connection.isInitiator */ ) {
1951 if (!connection.privileges.canStopRemoteStream) {
1952 connection.onstatechange({
1953 userid: response.userid,
1954 extra: response.extra,
1955 name: 'stop-request-denied',
1956 reason: response.userid + ' tried to stop your stream; however "privileges.canStopRemoteStream" is "false".'
1960 warn('Remote stream has been manually stopped!');
1961 if (connection.streams[response.streamid]) {
1962 connection.streams[response.streamid].stop();
1966 if (response.left) {
1967 // firefox is unable to stop remote streams
1968 // firefox doesn't auto stop streams when peer.close() is called.
1970 var userLeft = response.userid;
1971 for (var stream in connection.streams) {
1972 stream = connection.streams[stream];
1973 if (stream.userid == userLeft) {
1974 connection.stopMediaStream(stream);
1975 onStreamEndedHandler(stream, connection);
1980 if (peer && peer.connection) {
1981 // todo: verify if-block's 2nd condition
1982 if (peer.connection.signalingState != 'closed' && peer.connection.iceConnectionState.search(/disconnected|failed/gi) == -1) {
1983 peer.connection.close();
1985 peer.connection = null;
1988 if (participants[response.userid]) delete participants[response.userid];
1990 if (response.closeEntireSession) {
1991 connection.onSessionClosed(response);
1996 connection.remove(response.userid);
1999 userid: response.userid,
2000 extra: response.extra || {},
2001 entireSessionClosed: !!response.closeEntireSession
2005 // keeping session active even if initiator leaves
2006 if (response.playRoleOfBroadcaster) {
2007 if (response.extra) {
2008 // clone extra-data from initial moderator
2009 connection.extra = merge(connection.extra, response.extra);
2011 if (response.participants) {
2012 participants = response.participants;
2014 // make sure that if 2nd initiator leaves; control is shifted to 3rd person.
2015 if (participants[connection.userid]) {
2016 delete participants[connection.userid];
2019 if (sockets[0] && sockets[0].userid == response.userid) {
2021 sockets = swap(sockets);
2024 if (socketObjects[response.userid]) {
2025 delete socketObjects[response.userid];
2029 setTimeout(connection.playRoleOfInitiator, 2000);
2032 if (response.changeBandwidth) {
2033 if (!connection.peers[response.userid]) throw 'No such peer exists.';
2035 // synchronize bandwidth
2036 connection.peers[response.userid].peer.bandwidth = response.bandwidth;
2038 // renegotiate to apply bandwidth
2039 connection.peers[response.userid].renegotiate();
2042 if (response.customMessage) {
2043 if (!connection.peers[response.userid]) throw 'No such peer exists.';
2044 if (response.message.ejected) {
2045 if (connection.sessionDescriptions[connection.sessionid].userid != response.userid) {
2046 throw 'only initiator can eject a user.';
2048 // initiator ejected this user
2051 connection.onSessionClosed({
2052 userid: response.userid,
2053 extra: response.extra || _config.extra,
2056 } else connection.peers[response.userid].onCustomMessage(response.message);
2059 if (response.drop) {
2060 if (!connection.peers[response.userid]) throw 'No such peer exists.';
2061 connection.peers[response.userid].drop(true);
2062 connection.peers[response.userid].renegotiate();
2064 connection.ondrop(response.userid);
2067 if (response.hold || response.unhold) {
2068 if (!connection.peers[response.userid]) throw 'No such peer exists.';
2070 if (response.takeAction) {
2071 connection.peers[response.userid][!!response.hold ? 'hold' : 'unhold'](response.holdMLine);
2075 connection.peers[response.userid].peer.hold = !!response.hold;
2076 connection.peers[response.userid].peer.holdMLine = response.holdMLine;
2082 connection.peers[response.userid].fireHoldUnHoldEvents({
2083 kind: response.holdMLine,
2084 isHold: !!response.hold,
2085 userid: response.userid
2089 if (response.isRenegotiate) {
2090 connection.peers[response.userid].renegotiate(null, connection.peers[response.userid].peer.session);
2093 // fake data channels!
2094 if (response.fakeData) {
2095 peerConfig.onmessage(response.fakeData);
2098 // sometimes we don't need to renegotiate e.g. when peers are disconnected
2099 // or if it is firefox
2100 if (response.recreatePeer) {
2101 peer = new PeerConnection();
2104 // remote video failed either out of ICE gathering process or ICE connectivity check-up
2105 // or IceAgent was unable to locate valid candidates/ports.
2106 if (response.failedToReceiveRemoteVideo) {
2107 log('Remote peer hasn\'t received stream: ' + response.streamid + '. Renegotiating...');
2108 if (connection.peers[response.userid]) {
2109 connection.peers[response.userid].renegotiate();
2113 if (response.redial) {
2114 if (connection.peers[response.userid]) {
2115 if (connection.peers[response.userid].peer.connection.iceConnectionState != 'disconnected') {
2116 _config.redialing = false;
2119 if (connection.peers[response.userid].peer.connection.iceConnectionState == 'disconnected' && !_config.redialing) {
2120 _config.redialing = true;
2122 warn('Peer connection is closed.', toStr(connection.peers[response.userid].peer.connection), 'ReDialing..');
2123 connection.peers[response.userid].redial();
2129 connection.playRoleOfInitiator = function() {
2130 connection.dontCaptureUserMedia = true;
2132 sockets = swap(sockets);
2133 connection.dontCaptureUserMedia = false;
2136 connection.askToShareParticipants = function() {
2137 defaultSocket && defaultSocket.send({
2138 askToShareParticipants: true
2142 connection.shareParticipants = function(args) {
2144 joinUsers: participants,
2145 userid: connection.userid,
2146 extra: connection.extra
2150 if (args.dontShareWith) message.dontShareWith = args.dontShareWith;
2151 if (args.shareWith) message.shareWith = args.shareWith;
2154 defaultSocket.send(message);
2157 function sdpInvoker(sdp, labels) {
2158 if (sdp.type == 'answer') {
2159 peer.setRemoteDescription(sdp);
2163 if (!_config.renegotiate && sdp.type == 'offer') {
2164 peerConfig.offerDescription = sdp;
2166 peerConfig.session = connection.session;
2167 if (!peer) peer = new PeerConnection();
2168 peer.create('answer', peerConfig);
2174 var session = _config.renegotiate;
2176 detachMediaStream(labels, peer.connection);
2178 if (session.oneway || isData(session)) {
2180 delete _config.renegotiate;
2182 if (_config.capturing)
2185 _config.capturing = true;
2187 connection.captureUserMedia(function(stream) {
2188 _config.capturing = false;
2190 peer.addStream(stream);
2192 connection.renegotiatedSessions[JSON.stringify(_config.renegotiate)] = {
2193 session: _config.renegotiate,
2197 delete _config.renegotiate;
2200 }, _config.renegotiate);
2203 function createAnswer() {
2204 peer.recreateAnswer(sdp, session, function(_sdp, streaminfo) {
2208 streaminfo: streaminfo
2210 connection.detachStreams = [];
2216 function detachMediaStream(labels, peer) {
2217 if (!labels) return;
2218 for (var i = 0; i < labels.length; i++) {
2219 var label = labels[i];
2220 if (connection.streams[label]) {
2221 peer.removeStream(connection.streams[label].stream);
2226 function sendsdp(e) {
2228 sdp: JSON.stringify({
2232 renegotiate: !!e.renegotiate ? e.renegotiate : false,
2233 streaminfo: e.streaminfo || '',
2234 labels: e.labels || [],
2235 preferSCTP: !!connection.preferSCTP,
2236 fakeDataChannels: !!connection.fakeDataChannels,
2237 isInitiator: !!connection.isInitiator,
2239 browser: isFirefox ? 'firefox' : 'chrome'
2244 // sharing new user with existing participants
2246 function onNewParticipant(response) {
2247 var channel = response.newParticipant;
2249 if (!channel || !!participants[channel] || channel == connection.userid)
2252 var new_channel = connection.token();
2254 channel: new_channel,
2255 extra: response.userData ? response.userData.extra : response.extra,
2256 userid: response.userData ? response.userData.userid : response.userid
2259 defaultSocket.send({
2261 targetUser: channel,
2262 channel: new_channel
2268 function clearSession() {
2269 connection.numberOfConnectedUsers--;
2271 var alertMessage = {
2273 extra: connection.extra || {},
2274 userid: connection.userid,
2275 sessionid: connection.sessionid
2278 if (connection.isInitiator) {
2279 // if initiator wants to close entire session
2280 if (connection.autoCloseEntireSession) {
2281 alertMessage.closeEntireSession = true;
2282 } else if (sockets[0]) {
2283 // shift initiation control to another user
2285 playRoleOfBroadcaster: true,
2286 userid: connection.userid,
2287 extra: connection.extra,
2288 participants: participants
2293 sockets.forEach(function(socket, i) {
2294 socket.send(alertMessage);
2296 if (socketObjects[socket.channel]) {
2297 delete socketObjects[socket.channel];
2303 sockets = swap(sockets);
2305 connection.refresh();
2307 webAudioMediaStreamSources.forEach(function(mediaStreamSource) {
2308 // if source is connected; then chrome will crash on unload.
2309 mediaStreamSource.disconnect();
2312 webAudioMediaStreamSources = [];
2315 // www.RTCMultiConnection.org/docs/remove/
2316 connection.remove = function(userid) {
2317 if (rtcMultiSession.requestsFrom && rtcMultiSession.requestsFrom[userid]) delete rtcMultiSession.requestsFrom[userid];
2319 if (connection.peers[userid]) {
2320 if (connection.peers[userid].peer && connection.peers[userid].peer.connection) {
2321 if (connection.peers[userid].peer.connection.signalingState != 'closed') {
2322 connection.peers[userid].peer.connection.close();
2324 connection.peers[userid].peer.connection = null;
2326 delete connection.peers[userid];
2328 if (participants[userid]) {
2329 delete participants[userid];
2332 for (var stream in connection.streams) {
2333 stream = connection.streams[stream];
2334 if (stream.userid == userid) {
2335 onStreamEndedHandler(stream, connection);
2336 delete connection.streams[stream];
2340 if (socketObjects[userid]) {
2341 delete socketObjects[userid];
2345 // www.RTCMultiConnection.org/docs/refresh/
2346 connection.refresh = function() {
2347 // if firebase; remove data from firebase servers
2348 if (connection.isInitiator && !!connection.socket && !!connection.socket.remove) {
2349 connection.socket.remove();
2354 // to stop/remove self streams
2355 for (var i = 0; i < connection.attachStreams.length; i++) {
2356 connection.stopMediaStream(connection.attachStreams[i]);
2359 // to allow capturing of identical streams
2360 currentUserMediaRequest = {
2366 rtcMultiSession.isOwnerLeaving = true;
2368 connection.isInitiator = false;
2369 connection.isAcceptNewSession = true;
2370 connection.attachMediaStreams = [];
2371 connection.sessionDescription = null;
2372 connection.sessionDescriptions = {};
2373 connection.localStreamids = [];
2374 connection.preRecordedMedias = {};
2375 connection.snapshots = {};
2377 connection.numberOfConnectedUsers = 0;
2378 connection.numberOfSessions = 0;
2380 connection.attachStreams = [];
2381 connection.detachStreams = [];
2382 connection.fileQueue = {};
2383 connection.channels = {};
2384 connection.renegotiatedSessions = {};
2386 for (var peer in connection.peers) {
2387 if (peer != connection.userid) {
2388 delete connection.peers[peer];
2392 // to make sure remote streams are also removed!
2393 for (var stream in connection.streams) {
2394 if (connection._skip.indexOf(stream) == -1) {
2395 onStreamEndedHandler(connection.streams[stream], connection);
2396 delete connection.streams[stream];
2405 // www.RTCMultiConnection.org/docs/reject/
2406 connection.reject = function(userid) {
2407 if (!isString(userid)) userid = userid.userid;
2408 defaultSocket.send({
2409 rejectedRequestOf: userid
2412 // remove relevant data to allow him join again
2413 connection.remove(userid);
2416 rtcMultiSession.leaveHandler = function(e) {
2417 if (!connection.leaveOnPageUnload) return;
2419 if (isNull(e.keyCode)) {
2420 return clearSession();
2423 if (e.keyCode == 116) {
2428 listenEventHandler('beforeunload', rtcMultiSession.leaveHandler);
2429 listenEventHandler('keyup', rtcMultiSession.leaveHandler);
2431 rtcMultiSession.onLineOffLineHandler = function() {
2432 if (!navigator.onLine) {
2433 rtcMultiSession.isOffLine = true;
2434 } else if (rtcMultiSession.isOffLine) {
2435 rtcMultiSession.isOffLine = !navigator.onLine;
2437 // defaultSocket = getDefaultSocketRef();
2439 // pending tasks should be resumed?
2440 // sockets should be reconnected?
2441 // peers should be re-established?
2445 listenEventHandler('load', rtcMultiSession.onLineOffLineHandler);
2446 listenEventHandler('online', rtcMultiSession.onLineOffLineHandler);
2447 listenEventHandler('offline', rtcMultiSession.onLineOffLineHandler);
2449 function onSignalingReady() {
2450 if (rtcMultiSession.signalingReady) return;
2451 rtcMultiSession.signalingReady = true;
2453 setTimeout(callbackForSignalingReady, 1000);
2455 if (!connection.isInitiator) {
2456 // as soon as signaling gateway is connected;
2457 // user should check existing rooms!
2458 defaultSocket && defaultSocket.send({
2459 searchingForRooms: true
2464 function joinParticipants(joinUsers) {
2465 for (var user in joinUsers) {
2466 if (!participants[joinUsers[user]]) {
2468 sessionid: connection.sessionid,
2469 newParticipant: joinUsers[user],
2470 userid: connection.userid,
2471 extra: connection.extra
2477 function getDefaultSocketRef() {
2478 return connection.openSignalingChannel({
2479 onmessage: function(response) {
2480 // RMS == RTCMultiSession
2481 if (isRMSDeleted) return;
2483 // if message is sent by same user
2484 if (response.userid == connection.userid) return;
2486 if (response.sessionid && response.userid) {
2487 if (!connection.sessionDescriptions[response.sessionid]) {
2488 connection.numberOfSessions++;
2489 connection.sessionDescriptions[response.sessionid] = response;
2491 // fire "onNewSession" only if:
2492 // 1) "isAcceptNewSession" boolean is true
2493 // 2) "sessionDescriptions" object isn't having same session i.e. to prevent duplicate invocations
2494 if (connection.isAcceptNewSession) {
2496 if (!connection.dontOverrideSession) {
2497 connection.session = response.session;
2500 onNewSession(response);
2505 if (response.newParticipant && !connection.isAcceptNewSession && rtcMultiSession.broadcasterid === response.userid) {
2506 if (response.newParticipant != connection.userid) {
2507 onNewParticipant(response);
2511 if (getLength(participants) < connection.maxParticipantsAllowed && response.targetUser == connection.userid && response.participant) {
2512 if (connection.peers[response.userid] && !connection.peers[response.userid].peer) {
2513 delete participants[response.userid];
2514 delete connection.peers[response.userid];
2515 connection.isAcceptNewSession = true;
2516 return acceptRequest(response);
2519 if (!participants[response.userid]) {
2520 acceptRequest(response);
2524 if (response.acceptedRequestOf == connection.userid) {
2525 connection.onstatechange({
2526 userid: response.userid,
2527 extra: response.extra,
2528 name: 'request-accepted',
2529 reason: response.userid + ' accepted your participation request.'
2533 if (response.rejectedRequestOf == connection.userid) {
2534 connection.onstatechange({
2535 userid: response.userid,
2536 extra: response.extra,
2537 name: 'request-rejected',
2538 reason: response.userid + ' rejected your participation request.'
2542 if (response.customMessage) {
2543 if (response.message.drop) {
2544 connection.ondrop(response.userid);
2546 connection.attachStreams = [];
2547 // "drop" should detach all local streams
2548 for (var stream in connection.streams) {
2549 if (connection._skip.indexOf(stream) == -1) {
2550 stream = connection.streams[stream];
2551 if (stream.type == 'local') {
2552 connection.detachStreams.push(stream.streamid);
2553 onStreamEndedHandler(stream, connection);
2554 } else onStreamEndedHandler(stream, connection);
2558 if (response.message.renegotiate) {
2559 // renegotiate; so "peer.removeStream" happens.
2560 connection.renegotiate();
2562 } else if (connection.onCustomMessage) {
2563 connection.onCustomMessage(response.message);
2567 if (connection.isInitiator && response.searchingForRooms) {
2568 defaultSocket && defaultSocket.send({
2569 sessionDescription: connection.sessionDescription,
2570 responseFor: response.userid
2574 if (response.sessionDescription && response.responseFor == connection.userid) {
2575 var sessionDescription = response.sessionDescription;
2576 if (!connection.sessionDescriptions[sessionDescription.sessionid]) {
2577 connection.numberOfSessions++;
2578 connection.sessionDescriptions[sessionDescription.sessionid] = sessionDescription;
2582 if (connection.isInitiator && response.askToShareParticipants && defaultSocket) {
2583 connection.shareParticipants({
2584 shareWith: response.userid
2588 // participants are shared with single user
2589 if (response.shareWith == connection.userid && response.dontShareWith != connection.userid && response.joinUsers) {
2590 joinParticipants(response.joinUsers);
2593 // participants are shared with all users
2594 if (!response.shareWith && response.joinUsers) {
2595 if (response.dontShareWith) {
2596 if (connection.userid != response.dontShareWith) {
2597 joinParticipants(response.joinUsers);
2599 } else joinParticipants(response.joinUsers);
2602 if (response.messageFor == connection.userid && response.presenceState) {
2603 if (response.presenceState == 'checking') {
2604 defaultSocket.send({
2605 messageFor: response.userid,
2606 presenceState: 'available',
2607 _config: response._config
2609 log('participant asked for availability');
2612 if (response.presenceState == 'available') {
2613 rtcMultiSession.presenceState = 'available';
2615 connection.onstatechange({
2618 name: 'room-available',
2619 reason: 'Initiator is available and room is active.'
2622 joinSession(response._config);
2626 if (response.donotJoin && response.messageFor == connection.userid) {
2627 log(response.userid, 'is not joining your room.');
2630 // if initiator disconnects sockets, participants should also disconnect
2631 if (response.isDisconnectSockets) {
2632 log('Disconnecting your sockets because initiator also disconnected his sockets.');
2633 connection.disconnect();
2636 callback: function(socket) {
2637 socket && this.onopen(socket);
2639 onopen: function(socket) {
2640 if (socket) defaultSocket = socket;
2641 if (onSignalingReady) onSignalingReady();
2643 rtcMultiSession.defaultSocket = defaultSocket;
2645 if (!defaultSocket.__push) {
2646 defaultSocket.__push = defaultSocket.send;
2647 defaultSocket.send = function(message) {
2648 message.userid = message.userid || connection.userid;
2649 message.extra = message.extra || connection.extra || {};
2651 defaultSocket.__push(message);
2658 // default-socket is a common socket shared among all users in a specific channel;
2659 // to share participation requests; room descriptions; and other stuff.
2660 var defaultSocket = getDefaultSocketRef();
2662 rtcMultiSession.defaultSocket = defaultSocket;
2664 if (defaultSocket && onSignalingReady) setTimeout(onSignalingReady, 2000);
2666 if (connection.session.screen) {
2672 connection.getExternalIceServers && loadIceFrame(function(iceServers)
2674 connection.iceServers = connection.iceServers.concat(iceServers);
2680 if (connection.log == false) connection.skipLogs();
2681 if (connection.onlog) {
2682 log = warn = error = function() {
2685 Array.prototype.slice.call(arguments).forEach(function(argument) {
2686 log[index++] = toStr(argument);
2688 toStr = function(str) {
2691 connection.onlog(log);
2695 function setDirections() {
2696 var userMaxParticipantsAllowed = 0;
2698 // if user has set a custom max participant setting, remember it
2699 if (connection.maxParticipantsAllowed != 256) {
2700 userMaxParticipantsAllowed = connection.maxParticipantsAllowed;
2703 if (connection.direction == 'one-way') connection.session.oneway = true;
2704 if (connection.direction == 'one-to-one') connection.maxParticipantsAllowed = 1;
2705 if (connection.direction == 'one-to-many') connection.session.broadcast = true;
2706 if (connection.direction == 'many-to-many') {
2707 if (!connection.maxParticipantsAllowed || connection.maxParticipantsAllowed == 1) {
2708 connection.maxParticipantsAllowed = 256;
2712 // if user has set a custom max participant setting, set it back
2713 if (userMaxParticipantsAllowed && connection.maxParticipantsAllowed != 1) {
2714 connection.maxParticipantsAllowed = userMaxParticipantsAllowed;
2719 this.initSession = function(args) {
2720 rtcMultiSession.isOwnerLeaving = false;
2725 rtcMultiSession.isOwnerLeaving = false;
2727 if (!isNull(args.transmitRoomOnce)) {
2728 connection.transmitRoomOnce = args.transmitRoomOnce;
2731 function transmit() {
2732 if (defaultSocket && getLength(participants) < connection.maxParticipantsAllowed && !rtcMultiSession.isOwnerLeaving) {
2733 defaultSocket.send(connection.sessionDescription);
2736 if (!connection.transmitRoomOnce && !rtcMultiSession.isOwnerLeaving)
2737 setTimeout(transmit, connection.interval || 3000);
2740 // todo: test and fix next line.
2741 if (!args.dontTransmit /* || connection.transmitRoomOnce */ ) transmit();
2744 function joinSession(_config, skipOnStateChange) {
2745 if (rtcMultiSession.donotJoin && rtcMultiSession.donotJoin == _config.sessionid) {
2749 // dontOverrideSession allows you force RTCMultiConnection
2750 // to not override default session for participants;
2751 // by default, session is always overridden and set to the session coming from initiator!
2752 if (!connection.dontOverrideSession) {
2753 connection.session = _config.session || {};
2756 // make sure that inappropriate users shouldn't receive onNewSession event
2757 rtcMultiSession.broadcasterid = _config.userid;
2759 if (_config.sessionid) {
2760 // used later to prevent external rooms messages to be used by this user!
2761 connection.sessionid = _config.sessionid;
2764 connection.isAcceptNewSession = false;
2766 var channel = getRandomString();
2769 extra: _config.extra || {},
2770 userid: _config.userid
2774 if (connection.attachStreams.length) {
2775 var stream = connection.attachStreams[connection.attachStreams.length - 1];
2776 if (!!stream.getAudioTracks && stream.getAudioTracks().length) {
2777 offers.audio = true;
2779 if (stream.getVideoTracks().length) {
2780 offers.video = true;
2784 if (!isEmpty(offers)) {
2786 } else log('Seems data-only connection.');
2788 connection.onstatechange({
2789 userid: _config.userid,
2791 name: 'connecting-with-initiator',
2792 reason: 'Checking presence of the initiator; and the room.'
2795 defaultSocket.send({
2798 targetUser: _config.userid,
2799 session: connection.session,
2801 audio: !!offers.audio,
2802 video: !!offers.video
2806 connection.skipOnNewSession = false;
2807 invokeMediaCaptured(connection);
2810 // join existing session
2811 this.joinSession = function(_config) {
2813 return setTimeout(function() {
2814 warn('Default-Socket is not yet initialized.');
2815 rtcMultiSession.joinSession(_config);
2818 _config = _config || {};
2821 rtcMultiSession.presenceState = 'checking';
2823 connection.onstatechange({
2824 userid: _config.userid,
2825 extra: _config.extra || {},
2826 name: 'detecting-room-presence',
2827 reason: 'Checking presence of the room.'
2830 function contactInitiator() {
2831 defaultSocket.send({
2832 messageFor: _config.userid,
2833 presenceState: rtcMultiSession.presenceState,
2835 userid: _config.userid,
2836 extra: _config.extra || {},
2837 sessionid: _config.sessionid,
2838 session: _config.session || false
2844 function checker() {
2845 if (rtcMultiSession.presenceState == 'checking') {
2846 warn('Unable to reach initiator. Trying again...');
2848 setTimeout(function() {
2849 if (rtcMultiSession.presenceState == 'checking') {
2850 connection.onstatechange({
2851 userid: _config.userid,
2852 extra: _config.extra || {},
2853 name: 'room-not-available',
2854 reason: 'Initiator seems absent. Waiting for someone to open the room.'
2857 connection.isAcceptNewSession = true;
2858 setTimeout(checker, 2000);
2864 setTimeout(checker, 3000);
2867 connection.donotJoin = function(sessionid) {
2868 rtcMultiSession.donotJoin = sessionid;
2870 var session = connection.sessionDescriptions[sessionid];
2871 if (!session) return;
2873 defaultSocket.send({
2875 messageFor: session.userid,
2876 sessionid: sessionid
2880 connection.isAcceptNewSession = true;
2881 connection.sessionid = null;
2884 // send file/data or text message
2885 this.send = function(message, _channel) {
2886 if (!(message instanceof ArrayBuffer || message instanceof DataView)) {
2888 extra: connection.extra,
2889 userid: connection.userid,
2895 if (_channel.readyState == 'open') {
2896 _channel.send(message);
2901 for (var dataChannel in connection.channels) {
2902 var channel = connection.channels[dataChannel].channel;
2903 if (channel.readyState == 'open') {
2904 channel.send(message);
2910 this.leave = function() {
2914 // renegotiate new stream
2915 this.addStream = function(e) {
2916 var session = e.renegotiate;
2918 if (!connection.renegotiatedSessions[JSON.stringify(e.renegotiate)]) {
2919 connection.renegotiatedSessions[JSON.stringify(e.renegotiate)] = {
2920 session: e.renegotiate,
2926 if (e.socket.userid != connection.userid) {
2927 addStream(connection.peers[e.socket.userid]);
2930 for (var peer in connection.peers) {
2931 if (peer != connection.userid) {
2932 addStream(connection.peers[peer]);
2937 function addStream(_peer) {
2938 var socket = _peer.socket;
2941 warn(_peer, 'doesn\'t has socket.');
2945 updateSocketForLocalStreams(socket);
2947 if (!_peer || !_peer.peer) {
2948 throw 'No peer to renegotiate.';
2951 var peer = _peer.peer;
2954 if (!peer.attachStreams) {
2955 peer.attachStreams = [];
2958 peer.attachStreams.push(e.stream);
2961 // detaching old streams
2962 detachMediaStream(connection.detachStreams, peer.connection);
2964 if (e.stream && (session.audio || session.video || session.screen)) {
2965 peer.addStream(e.stream);
2968 peer.recreateOffer(session, function(sdp, streaminfo) {
2972 renegotiate: session,
2973 labels: connection.detachStreams,
2974 streaminfo: streaminfo
2976 connection.detachStreams = [];
2981 // www.RTCMultiConnection.org/docs/request/
2982 connection.request = function(userid, extra) {
2983 connection.captureUserMedia(function() {
2984 // open private socket that will be used to receive offer-sdp
2986 channel: connection.userid,
2991 // ask other user to create offer-sdp
2992 defaultSocket.send({
2999 function acceptRequest(response) {
3000 if (!rtcMultiSession.requestsFrom) rtcMultiSession.requestsFrom = {};
3001 if (rtcMultiSession.requestsFrom[response.userid]) return;
3004 userid: response.userid,
3005 extra: response.extra,
3006 channel: response.channel || response.userid,
3007 session: response.session || connection.session
3010 // check how participant is willing to join
3011 if (response.offers) {
3012 if (response.offers.audio && response.offers.video) {
3013 log('target user has both audio/video streams.');
3014 } else if (response.offers.audio && !response.offers.video) {
3015 log('target user has only audio stream.');
3016 } else if (!response.offers.audio && response.offers.video) {
3017 log('target user has only video stream.');
3019 log('target user has no stream; it seems one-way streaming or data-only connection.');
3022 var mandatory = connection.sdpConstraints.mandatory;
3023 if (isNull(mandatory.OfferToReceiveAudio)) {
3024 connection.sdpConstraints.mandatory.OfferToReceiveAudio = !!response.offers.audio;
3026 if (isNull(mandatory.OfferToReceiveVideo)) {
3027 connection.sdpConstraints.mandatory.OfferToReceiveVideo = !!response.offers.video;
3030 log('target user\'s SDP has?', toStr(connection.sdpConstraints.mandatory));
3033 rtcMultiSession.requestsFrom[response.userid] = obj;
3035 // www.RTCMultiConnection.org/docs/onRequest/
3036 if (connection.onRequest && connection.isInitiator) {
3037 connection.onRequest(obj);
3038 } else _accept(obj);
3041 function _accept(e) {
3042 if (rtcMultiSession.captureUserMediaOnDemand) {
3043 rtcMultiSession.captureUserMediaOnDemand = false;
3044 connection.captureUserMedia(function() {
3047 invokeMediaCaptured(connection);
3052 log('accepting request from', e.userid);
3053 participants[e.userid] = e.userid;
3058 extra: e.extra || {},
3059 session: e.session || connection.session
3063 // www.RTCMultiConnection.org/docs/accept/
3064 connection.accept = function(e) {
3065 // for backward compatibility
3066 if (arguments.length > 1 && isString(arguments[0])) {
3068 if (arguments[0]) e.userid = arguments[0];
3069 if (arguments[1]) e.extra = arguments[1];
3070 if (arguments[2]) e.channel = arguments[2];
3073 connection.captureUserMedia(function() {
3078 var isRMSDeleted = false;
3079 this.disconnect = function() {
3080 this.isOwnerLeaving = true;
3082 if (!connection.keepStreamsOpened) {
3083 for (var streamid in connection.localStreams) {
3084 connection.localStreams[streamid].stop();
3086 connection.localStreams = {};
3088 currentUserMediaRequest = {
3095 if (connection.isInitiator) {
3096 defaultSocket.send({
3097 isDisconnectSockets: true
3101 connection.refresh();
3103 rtcMultiSession.defaultSocket = defaultSocket = null;
3104 isRMSDeleted = true;
3106 connection.ondisconnected({
3107 userid: connection.userid,
3108 extra: connection.extra,
3109 peer: connection.peers[connection.userid],
3110 isSocketsDisconnected: true
3113 // if there is any peer still opened; close it.
3116 window.removeEventListener('beforeunload', rtcMultiSession.leaveHandler);
3117 window.removeEventListener('keyup', rtcMultiSession.leaveHandler);
3119 // it will not work, though :)
3122 log('Disconnected your sockets, peers, streams and everything except RTCMultiConnection object.');
3126 var webAudioMediaStreamSources = [];
3128 function convertToAudioStream(mediaStream) {
3129 if (!mediaStream) throw 'MediaStream is mandatory.';
3131 if (mediaStream.getVideoTracks && !mediaStream.getVideoTracks().length) {
3135 var context = new AudioContext();
3136 var mediaStreamSource = context.createMediaStreamSource(mediaStream);
3138 var destination = context.createMediaStreamDestination();
3139 mediaStreamSource.connect(destination);
3141 webAudioMediaStreamSources.push(mediaStreamSource);
3143 return destination.stream;
3146 var isOpera = !!window.opera || navigator.userAgent.indexOf(' OPR/') >= 0;
3147 var isFirefox = typeof window.InstallTrigger !== 'undefined';
3148 var isSafari = Object.prototype.toString.call(window.HTMLElement).indexOf('Constructor') > 0;
3149 var isChrome = !!window.chrome && !isOpera;
3150 var isIE = !!document.documentMode;
3152 var isPluginRTC = isSafari || isIE;
3154 var isMobileDevice = !!navigator.userAgent.match(/Android|iPhone|iPad|iPod|BlackBerry|IEMobile/i);
3156 // detect node-webkit
3157 var isNodeWebkit = !!(window.process && (typeof window.process == 'object') && window.process.versions && window.process.versions['node-webkit']);
3159 window.MediaStream = window.MediaStream || window.webkitMediaStream;
3160 window.AudioContext = window.AudioContext || window.webkitAudioContext;
3162 function getRandomString() {
3163 // suggested by @rvulpescu from #154
3164 if (window.crypto && crypto.getRandomValues && navigator.userAgent.indexOf('Safari') == -1) {
3165 var a = window.crypto.getRandomValues(new Uint32Array(3)),
3167 for (var i = 0, l = a.length; i < l; i++) {
3168 token += a[i].toString(36);
3172 return (Math.random() * new Date().getTime()).toString(36).replace(/\./g, '');
3176 var chromeVersion = 50;
3177 var matchArray = navigator.userAgent.match(/Chrom(e|ium)\/([0-9]+)\./);
3178 if (isChrome && matchArray && matchArray[2]) {
3179 chromeVersion = parseInt(matchArray[2], 10);
3182 var firefoxVersion = 50;
3183 matchArray = navigator.userAgent.match(/Firefox\/(.*)/);
3184 if (isFirefox && matchArray && matchArray[1]) {
3185 firefoxVersion = parseInt(matchArray[1], 10);
3188 function isData(session) {
3189 return !session.audio && !session.video && !session.screen && session.data;
3192 function isNull(obj) {
3193 return typeof obj == 'undefined';
3196 function isString(obj) {
3197 return typeof obj == 'string';
3200 function isEmpty(session) {
3202 for (var s in session) {
3208 // this method converts array-buffer into string
3209 function ab2str(buf) {
3212 result = String.fromCharCode.apply(null, new Uint16Array(buf));
3217 // this method converts string into array-buffer
3218 function str2ab(str) {
3219 if (!isString(str)) str = JSON.stringify(str);
3221 var buf = new ArrayBuffer(str.length * 2); // 2 bytes for each char
3222 var bufView = new Uint16Array(buf);
3223 for (var i = 0, strLen = str.length; i < strLen; i++) {
3224 bufView[i] = str.charCodeAt(i);
3229 function swap(arr) {
3231 length = arr.length;
3232 for (var i = 0; i < length; i++)
3233 if (arr[i] && arr[i] !== true)
3234 swapped.push(arr[i]);
3238 function forEach(obj, callback) {
3239 for (var item in obj) {
3240 callback(obj[item], item);
3244 var console = window.console || {
3246 error: function() {},
3251 console.log(arguments);
3255 console.error(arguments);
3259 console.warn(arguments);
3262 if (isChrome || isFirefox || isSafari) {
3263 var log = console.log.bind(console);
3264 var error = console.error.bind(console);
3265 var warn = console.warn.bind(console);
3268 function toStr(obj) {
3269 return JSON.stringify(obj, function(key, value) {
3270 if (value && value.sdp) {
3271 log(value.sdp.type, '\t', value.sdp.sdp);
3273 } else return value;
3277 function getLength(obj) {
3284 // Get HTMLAudioElement/HTMLVideoElement accordingly
3286 function createMediaElement(stream, session) {
3287 var mediaElement = document.createElement(stream.isAudio ? 'audio' : 'video');
3288 mediaElement.id = stream.streamid;
3291 var body = (document.body || document.documentElement);
3292 body.insertBefore(mediaElement, body.firstChild);
3294 setTimeout(function() {
3295 Plugin.attachMediaStream(mediaElement, stream)
3298 return Plugin.attachMediaStream(mediaElement, stream);
3301 // "mozSrcObject" is always preferred over "src"!!
3302 mediaElement[isFirefox ? 'mozSrcObject' : 'src'] = isFirefox ? stream : (window.URL || window.webkitURL).createObjectURL(stream);
3304 mediaElement.controls = true;
3305 mediaElement.autoplay = !!session.remote;
3306 mediaElement.muted = session.remote ? false : true;
3308 // http://goo.gl/WZ5nFl
3309 // Firefox don't yet support onended for any stream (remote/local)
3310 isFirefox && mediaElement.addEventListener('ended', function() {
3314 mediaElement.play();
3316 return mediaElement;
3319 var onStreamEndedHandlerFiredFor = {};
3321 function onStreamEndedHandler(streamedObject, connection) {
3322 if (streamedObject.mediaElement && !streamedObject.mediaElement.parentNode) return;
3324 if (onStreamEndedHandlerFiredFor[streamedObject.streamid]) return;
3325 onStreamEndedHandlerFiredFor[streamedObject.streamid] = streamedObject;
3326 connection.onstreamended(streamedObject);
3329 var onLeaveHandlerFiredFor = {};
3331 function onLeaveHandler(event, connection) {
3332 if (onLeaveHandlerFiredFor[event.userid]) return;
3333 onLeaveHandlerFiredFor[event.userid] = event;
3334 connection.onleave(event);
3337 function takeSnapshot(args) {
3338 var userid = args.userid;
3339 var connection = args.connection;
3341 function _takeSnapshot(video) {
3342 var canvas = document.createElement('canvas');
3343 canvas.width = video.videoWidth || video.clientWidth;
3344 canvas.height = video.videoHeight || video.clientHeight;
3346 var context = canvas.getContext('2d');
3347 context.drawImage(video, 0, 0, canvas.width, canvas.height);
3349 connection.snapshots[userid] = canvas.toDataURL('image/png');
3350 args.callback && args.callback(connection.snapshots[userid]);
3353 if (args.mediaElement) return _takeSnapshot(args.mediaElement);
3355 for (var stream in connection.streams) {
3356 stream = connection.streams[stream];
3357 if (stream.userid == userid && stream.stream && stream.stream.getVideoTracks && stream.stream.getVideoTracks().length) {
3358 _takeSnapshot(stream.mediaElement);
3364 function invokeMediaCaptured(connection) {
3365 // to let user know that media resource has been captured
3366 // now, he can share "sessionDescription" using sockets
3367 if (connection.onMediaCaptured) {
3368 connection.onMediaCaptured();
3369 delete connection.onMediaCaptured;
3373 function merge(mergein, mergeto) {
3374 if (!mergein) mergein = {};
3375 if (!mergeto) return mergein;
3377 for (var item in mergeto) {
3378 mergein[item] = mergeto[item];
3383 function loadScript(src, onload) {
3384 var script = document.createElement('script');
3386 script.onload = function() {
3387 log('loaded resource:', src);
3388 if (onload) onload();
3390 document.documentElement.appendChild(script);
3393 function capturePartOfScreen(args) {
3394 var connection = args.connection;
3395 var element = args.element;
3397 if (!window.html2canvas) {
3398 return loadScript(connection.resources.html2canvas, function() {
3399 capturePartOfScreen(args);
3403 if (isString(element)) {
3404 element = document.querySelector(element);
3405 if (!element) element = document.getElementById(element);
3407 if (!element) throw 'HTML DOM Element is not accessible!';
3409 // todo: store DOM element somewhere to minimize DOM querying issues
3411 // html2canvas.js is used to take screenshots
3412 html2canvas(element, {
3413 onrendered: function(canvas) {
3414 args.callback(canvas.toDataURL());
3419 function initFileBufferReader(connection, callback) {
3420 if (!window.FileBufferReader) {
3421 loadScript(connection.resources.FileBufferReader, function() {
3422 initFileBufferReader(connection, callback);
3427 function _private(chunk) {
3428 chunk.userid = chunk.extra.userid;
3432 var fileBufferReader = new FileBufferReader();
3433 fileBufferReader.onProgress = function(chunk) {
3434 connection.onFileProgress(_private(chunk), chunk.uuid);
3437 fileBufferReader.onBegin = function(file) {
3438 connection.onFileStart(_private(file));
3441 fileBufferReader.onEnd = function(file) {
3442 connection.onFileEnd(_private(file));
3445 callback(fileBufferReader);
3448 var screenFrame, loadedScreenFrame;
3450 function loadScreenFrame(skip) {
3451 if (DetectRTC.screen.extensionid != ReservedExtensionID) {
3455 if (loadedScreenFrame) return;
3456 if (!skip) return loadScreenFrame(true);
3458 loadedScreenFrame = true;
3460 var iframe = document.createElement('iframe');
3461 iframe.onload = function() {
3462 iframe.isLoaded = true;
3463 log('Screen Capturing frame is loaded.');
3465 //iframe.src = 'https://www.webrtc-experiment.com/getSourceId/';
3467 iframe.src = 'app/fusion/scripts/webrtc/getSourceId.html';
3469 iframe.style.display = 'none';
3470 (document.body || document.documentElement).appendChild(iframe);
3473 postMessage: function() {
3474 if (!iframe.isLoaded) {
3475 setTimeout(screenFrame.postMessage, 100);
3478 iframe.contentWindow.postMessage({
3479 captureSourceId: true
3485 var iceFrame, loadedIceFrame;
3487 function loadIceFrame(callback, skip) {
3488 if (loadedIceFrame) return;
3489 if (!skip) return loadIceFrame(callback, true);
3491 loadedIceFrame = true;
3493 var iframe = document.createElement('iframe');
3494 iframe.onload = function() {
3495 iframe.isLoaded = true;
3497 listenEventHandler('message', iFrameLoaderCallback);
3499 function iFrameLoaderCallback(event) {
3500 if (!event.data || !event.data.iceServers) return;
3501 callback(event.data.iceServers);
3503 // this event listener is no more needed
3504 window.removeEventListener('message', iFrameLoaderCallback);
3507 iframe.contentWindow.postMessage('get-ice-servers', '*');
3509 iframe.src = 'https://cdn.webrtc-experiment.com/getIceServers/';
3510 iframe.style.display = 'none';
3511 (document.body || document.documentElement).appendChild(iframe);
3514 function muteOrUnmute(e) {
3515 var stream = e.stream,
3517 session = e.session || {},
3518 enabled = e.enabled;
3520 if (!session.audio && !session.video) {
3521 if (!isString(session)) {
3522 session = merge(session, {
3534 // implementation from #68
3536 if (session.type == 'remote' && root.type != 'remote') return;
3537 if (session.type == 'local' && root.type != 'local') return;
3540 log(enabled ? 'Muting' : 'UnMuting', 'session', toStr(session));
3542 // enable/disable audio/video tracks
3544 if (root.type == 'local' && session.audio && !!stream.getAudioTracks) {
3545 var audioTracks = stream.getAudioTracks()[0];
3547 audioTracks.enabled = !enabled;
3550 if (root.type == 'local' && (session.video || session.screen) && !!stream.getVideoTracks) {
3551 var videoTracks = stream.getVideoTracks()[0];
3553 videoTracks.enabled = !enabled;
3556 root.sockets.forEach(function(socket) {
3557 if (root.type == 'local') {
3559 streamid: root.streamid,
3566 if (root.type == 'remote') {
3568 promptMuteUnmute: true,
3569 streamid: root.streamid,
3577 if (root.type == 'remote') return;
3579 // According to issue #135, onmute/onumute must be fired for self
3580 // "fakeObject" is used because we need to keep session for renegotiated streams;
3581 // and MUST pass exact session over onStreamEndedHandler/onmute/onhold/etc. events.
3582 var fakeObject = merge({}, root);
3583 fakeObject.session = session;
3585 fakeObject.isAudio = !!fakeObject.session.audio && !fakeObject.session.video;
3586 fakeObject.isVideo = !!fakeObject.session.video;
3587 fakeObject.isScreen = !!fakeObject.session.screen;
3590 // if muted stream is negotiated
3592 audio: stream.getAudioTracks().length && !stream.getAudioTracks()[0].enabled,
3593 video: stream.getVideoTracks().length && !stream.getVideoTracks()[0].enabled
3595 root.rtcMultiConnection.onmute(fakeObject);
3599 stream.preMuted = {};
3600 root.rtcMultiConnection.onunmute(fakeObject);
3604 var Firefox_Screen_Capturing_Warning = 'Make sure that you are using Firefox Nightly and you enabled: media.getusermedia.screensharing.enabled flag from about:config page. You also need to add your domain in "media.getusermedia.screensharing.allowed_domains" flag. If you are using WinXP then also enable "media.getusermedia.screensharing.allow_on_old_platforms" flag. NEVER forget to use "only" HTTPs for screen capturing!';
3605 var SCREEN_COMMON_FAILURE = 'HTTPs i.e. SSL-based URI is mandatory to use screen capturing.';
3607 var ReservedExtensionID = 'icgmlogfeajbfdffajhoebcfbibfhaen';
3608 //var ReservedExtensionID = 'ajhifddimkapgcifgcodmmfdlknahffk';
3610 // if application-developer deployed his own extension on Google App Store
3611 var useCustomChromeExtensionForScreenCapturing = document.domain.indexOf('webrtc-experiment.com') != -1;
3613 function initHark(args) {
3615 loadScript(args.connection.resources.hark, function() {
3621 var connection = args.connection;
3622 var streamedObject = args.streamedObject;
3623 var stream = args.stream;
3626 var speechEvents = hark(stream, options);
3628 speechEvents.on('speaking', function() {
3629 if (connection.onspeaking) {
3630 connection.onspeaking(streamedObject);
3634 speechEvents.on('stopped_speaking', function() {
3635 if (connection.onsilence) {
3636 connection.onsilence(streamedObject);
3640 speechEvents.on('volume_change', function(volume, threshold) {
3641 if (connection.onvolumechange) {
3642 connection.onvolumechange(merge({
3644 threshold: threshold
3645 }, streamedObject));
3650 attachEventListener = function(video, type, listener, useCapture) {
3651 video.addEventListener(type, listener, useCapture);
3654 var Plugin = window.PluginRTC || {};
3655 window.onPluginRTCInitialized = function(pluginRTCObject) {
3656 Plugin = pluginRTCObject;
3657 MediaStreamTrack = Plugin.MediaStreamTrack;
3658 RTCPeerConnection = Plugin.RTCPeerConnection;
3659 RTCIceCandidate = Plugin.RTCIceCandidate;
3660 RTCSessionDescription = Plugin.RTCSessionDescription;
3662 log(isPluginRTC ? 'Java-Applet' : 'ActiveX', 'plugin has been loaded.');
3664 if (!isEmpty(Plugin)) window.onPluginRTCInitialized(Plugin);
3668 loadScript('https://cdn.webrtc-experiment.com/Plugin.EveryWhere.js');
3669 // loadScript('https://cdn.webrtc-experiment.com/Plugin.Temasys.js');
3672 var MediaStream = window.MediaStream;
3674 if (typeof MediaStream === 'undefined' && typeof webkitMediaStream !== 'undefined') {
3675 MediaStream = webkitMediaStream;
3678 /*global MediaStream:true */
3679 if (typeof MediaStream !== 'undefined' && !('stop' in MediaStream.prototype)) {
3680 MediaStream.prototype.stop = function() {
3681 this.getAudioTracks().forEach(function(track) {
3685 this.getVideoTracks().forEach(function(track) {
3691 var defaultConstraints = {
3696 /* by @FreCap pull request #41 */
3697 var currentUserMediaRequest = {
3703 function getUserMedia(options) {
3705 if (!Plugin.getUserMedia) {
3706 setTimeout(function() {
3707 getUserMedia(options);
3712 return Plugin.getUserMedia(options.constraints || {
3715 }, options.onsuccess, options.onerror);
3718 if (currentUserMediaRequest.mutex === true) {
3719 currentUserMediaRequest.queueRequests.push(options);
3722 currentUserMediaRequest.mutex = true;
3724 var connection = options.connection;
3726 // tools.ietf.org/html/draft-alvestrand-constraints-resolution-00
3727 var mediaConstraints = options.mediaConstraints || {};
3728 var videoConstraints = typeof mediaConstraints.video == 'boolean' ? mediaConstraints.video : mediaConstraints.video || mediaConstraints;
3729 var audioConstraints = typeof mediaConstraints.audio == 'boolean' ? mediaConstraints.audio : mediaConstraints.audio || defaultConstraints;
3732 var hints = options.constraints || {
3733 audio: defaultConstraints,
3734 video: defaultConstraints
3737 if (hints.video && hints.video.mozMediaSource) {
3738 // "mozMediaSource" is redundant
3739 // need to check "mediaSource" instead.
3740 videoConstraints = {};
3743 if (hints.video == true) hints.video = defaultConstraints;
3744 if (hints.audio == true) hints.audio = defaultConstraints;
3746 // connection.mediaConstraints.audio = false;
3747 if (typeof audioConstraints == 'boolean' && hints.audio) {
3748 hints.audio = audioConstraints;
3751 // connection.mediaConstraints.video = false;
3752 if (typeof videoConstraints == 'boolean' && hints.video) {
3753 hints.video = videoConstraints;
3756 // connection.mediaConstraints.audio.mandatory = {prop:true};
3757 var audioMandatoryConstraints = audioConstraints.mandatory;
3758 if (!isEmpty(audioMandatoryConstraints)) {
3759 hints.audio.mandatory = merge(hints.audio.mandatory, audioMandatoryConstraints);
3762 // connection.media.min(320,180);
3763 // connection.media.max(1920,1080);
3764 var videoMandatoryConstraints = videoConstraints.mandatory;
3765 if (videoMandatoryConstraints) {
3768 if (videoMandatoryConstraints.minWidth) {
3769 mandatory.minWidth = videoMandatoryConstraints.minWidth;
3772 if (videoMandatoryConstraints.minHeight) {
3773 mandatory.minHeight = videoMandatoryConstraints.minHeight;
3776 if (videoMandatoryConstraints.maxWidth) {
3777 mandatory.maxWidth = videoMandatoryConstraints.maxWidth;
3780 if (videoMandatoryConstraints.maxHeight) {
3781 mandatory.maxHeight = videoMandatoryConstraints.maxHeight;
3784 if (videoMandatoryConstraints.minAspectRatio) {
3785 mandatory.minAspectRatio = videoMandatoryConstraints.minAspectRatio;
3788 if (videoMandatoryConstraints.maxFrameRate) {
3789 mandatory.maxFrameRate = videoMandatoryConstraints.maxFrameRate;
3792 if (videoMandatoryConstraints.minFrameRate) {
3793 mandatory.minFrameRate = videoMandatoryConstraints.minFrameRate;
3796 if (mandatory.minWidth && mandatory.minHeight) {
3797 // http://goo.gl/IZVYsj
3798 var allowed = ['1920:1080', '1280:720', '960:720', '640:360', '640:480', '320:240', '320:180'];
3800 if (allowed.indexOf(mandatory.minWidth + ':' + mandatory.minHeight) == -1 ||
3801 allowed.indexOf(mandatory.maxWidth + ':' + mandatory.maxHeight) == -1) {
3802 error('The min/max width/height constraints you passed "seems" NOT supported.', toStr(mandatory));
3805 if (mandatory.minWidth > mandatory.maxWidth || mandatory.minHeight > mandatory.maxHeight) {
3806 error('Minimum value must not exceed maximum value.', toStr(mandatory));
3809 if (mandatory.minWidth >= 1280 && mandatory.minHeight >= 720) {
3810 warn('Enjoy HD video! min/' + mandatory.minWidth + ':' + mandatory.minHeight + ', max/' + mandatory.maxWidth + ':' + mandatory.maxHeight);
3814 hints.video.mandatory = merge(hints.video.mandatory, mandatory);
3817 if (videoMandatoryConstraints) {
3818 hints.video.mandatory = merge(hints.video.mandatory, videoMandatoryConstraints);
3821 // videoConstraints.optional = [{prop:true}];
3822 if (videoConstraints.optional && videoConstraints.optional instanceof Array && videoConstraints.optional.length) {
3823 hints.video.optional = hints.video.optional ? hints.video.optional.concat(videoConstraints.optional) : videoConstraints.optional;
3826 // audioConstraints.optional = [{prop:true}];
3827 if (audioConstraints.optional && audioConstraints.optional instanceof Array && audioConstraints.optional.length) {
3828 hints.audio.optional = hints.audio.optional ? hints.audio.optional.concat(audioConstraints.optional) : audioConstraints.optional;
3831 if (hints.video.mandatory && !isEmpty(hints.video.mandatory) && connection._mediaSources.video) {
3832 hints.video.optional.forEach(function(video, index) {
3833 if (video.sourceId == connection._mediaSources.video) {
3834 delete hints.video.optional[index];
3838 hints.video.optional = swap(hints.video.optional);
3840 hints.video.optional.push({
3841 sourceId: connection._mediaSources.video
3845 if (hints.audio.mandatory && !isEmpty(hints.audio.mandatory) && connection._mediaSources.audio) {
3846 hints.audio.optional.forEach(function(audio, index) {
3847 if (audio.sourceId == connection._mediaSources.audio) {
3848 delete hints.audio.optional[index];
3852 hints.audio.optional = swap(hints.audio.optional);
3854 hints.audio.optional.push({
3855 sourceId: connection._mediaSources.audio
3859 if (hints.video && !hints.video.mozMediaSource && hints.video.optional && hints.video.mandatory) {
3860 if (!hints.video.optional.length && isEmpty(hints.video.mandatory)) {
3865 if (isMobileDevice) {
3866 // Android fails for some constraints
3867 // so need to force {audio:true,video:true}
3869 audio: !!hints.audio,
3870 video: !!hints.video
3874 // connection.mediaConstraints always overrides constraints
3875 // passed from "captureUserMedia" function.
3876 // todo: need to verify all possible situations
3877 log('invoked getUserMedia with constraints:', toStr(hints));
3879 // easy way to match
3880 var idInstance = JSON.stringify(hints);
3882 function streaming(stream, returnBack, streamid) {
3883 if (!streamid) streamid = getRandomString();
3885 // localStreams object will store stream
3886 // until it is removed using native-stop method.
3887 connection.localStreams[streamid] = stream;
3889 var video = options.video;
3891 video[isFirefox ? 'mozSrcObject' : 'src'] = isFirefox ? stream : (window.URL || window.webkitURL).createObjectURL(stream);
3895 options.onsuccess(stream, returnBack, idInstance, streamid);
3896 currentUserMediaRequest.streams[idInstance] = {
3900 currentUserMediaRequest.mutex = false;
3901 if (currentUserMediaRequest.queueRequests.length)
3902 getUserMedia(currentUserMediaRequest.queueRequests.shift());
3905 if (currentUserMediaRequest.streams[idInstance]) {
3906 streaming(currentUserMediaRequest.streams[idInstance].stream, true, currentUserMediaRequest.streams[idInstance].streamid);
3908 n.getMedia = n.webkitGetUserMedia || n.mozGetUserMedia;
3910 // http://goo.gl/eETIK4
3911 n.getMedia(hints, streaming, function(error) {
3912 options.onerror(error, hints);
3917 var RTCSessionDescription = window.RTCSessionDescription || window.mozRTCSessionDescription;
3918 var RTCIceCandidate = window.RTCIceCandidate || window.mozRTCIceCandidate;
3920 var RTCPeerConnection;
3921 if (typeof mozRTCPeerConnection !== 'undefined') {
3922 RTCPeerConnection = mozRTCPeerConnection;
3923 } else if (typeof webkitRTCPeerConnection !== 'undefined') {
3924 RTCPeerConnection = webkitRTCPeerConnection;
3925 } else if (typeof window.RTCPeerConnection !== 'undefined') {
3926 RTCPeerConnection = window.RTCPeerConnection;
3928 console.error('WebRTC 1.0 (RTCPeerConnection) API seems NOT available in this browser.');
3931 function setSdpConstraints(config) {
3934 var sdpConstraints_mandatory = {
3935 OfferToReceiveAudio: !!config.OfferToReceiveAudio,
3936 OfferToReceiveVideo: !!config.OfferToReceiveVideo
3940 mandatory: sdpConstraints_mandatory,
3942 VoiceActivityDetection: false
3946 if (!!navigator.mozGetUserMedia && firefoxVersion > 34) {
3948 OfferToReceiveAudio: !!config.OfferToReceiveAudio,
3949 OfferToReceiveVideo: !!config.OfferToReceiveVideo
3953 return sdpConstraints;
3956 function PeerConnection() {
3958 create: function(type, options) {
3959 merge(this, options);
3965 this.attachMediaStreams();
3967 if (isFirefox && this.session.data) {
3968 if (this.session.data && type == 'offer') {
3969 this.createDataChannel();
3972 this.getLocalDescription(type);
3974 if (this.session.data && type == 'answer') {
3975 this.createDataChannel();
3977 } else self.getLocalDescription(type);
3981 getLocalDescription: function(createType) {
3982 log('(getLocalDescription) peer createType is', createType);
3984 if (this.session.inactive && isNull(this.rtcMultiConnection.waitUntilRemoteStreamStartsFlowing)) {
3985 // inactive session returns blank-stream
3986 this.rtcMultiConnection.waitUntilRemoteStreamStartsFlowing = false;
3991 if (createType == 'answer') {
3992 this.setRemoteDescription(this.offerDescription, createDescription);
3993 } else createDescription();
3995 function createDescription() {
3996 self.connection[createType == 'offer' ? 'createOffer' : 'createAnswer'](function(sessionDescription) {
3997 sessionDescription.sdp = self.serializeSdp(sessionDescription.sdp, createType);
3998 self.connection.setLocalDescription(sessionDescription);
4000 if (self.trickleIce) {
4001 self.onSessionDescription(sessionDescription, self.streaminfo);
4004 if (sessionDescription.type == 'offer') {
4005 log('offer sdp', sessionDescription.sdp);
4008 self.prevCreateType = createType;
4009 }, self.onSdpError, self.constraints);
4012 serializeSdp: function(sdp, createType) {
4013 // it is "connection.processSdp=function(sdp){return sdp;}"
4014 sdp = this.processSdp(sdp);
4016 if (isFirefox) return sdp;
4018 if (this.session.inactive && !this.holdMLine) {
4020 if ((this.session.screen || this.session.video) && this.session.audio) {
4021 this.holdMLine = 'both';
4022 } else if (this.session.screen || this.session.video) {
4023 this.holdMLine = 'video';
4024 } else if (this.session.audio) {
4025 this.holdMLine = 'audio';
4029 sdp = this.setBandwidth(sdp);
4030 if (this.holdMLine == 'both') {
4033 sdp = sdp.replace(/a=sendonly|a=recvonly|a=sendrecv/g, 'a=inactive');
4034 } else if (this.prevSDP) {
4035 if (!this.session.inactive) {
4036 // it means that DTSL key exchange already happened for single or multiple media lines.
4037 // this block checks, key-exchange must be happened for all media lines.
4040 // todo: test it: makes sense?
4041 if (chromeVersion <= 35) {
4046 } else if (this.holdMLine == 'audio' || this.holdMLine == 'video') {
4047 sdp = sdp.split('m=');
4052 if (sdp[1] && sdp[1].indexOf('audio') == 0) {
4053 audio = 'm=' + sdp[1];
4055 if (sdp[2] && sdp[2].indexOf('audio') == 0) {
4056 audio = 'm=' + sdp[2];
4059 if (sdp[1] && sdp[1].indexOf('video') == 0) {
4060 video = 'm=' + sdp[1];
4062 if (sdp[2] && sdp[2].indexOf('video') == 0) {
4063 video = 'm=' + sdp[2];
4066 if (this.holdMLine == 'audio') {
4068 this.prevSDP = sdp[0] + audio + video;
4069 sdp = sdp[0] + audio.replace(/a=sendonly|a=recvonly|a=sendrecv/g, 'a=inactive') + video;
4070 } else if (this.prevSDP) {
4075 if (this.holdMLine == 'video') {
4077 this.prevSDP = sdp[0] + audio + video;
4078 sdp = sdp[0] + audio + video.replace(/a=sendonly|a=recvonly|a=sendrecv/g, 'a=inactive');
4079 } else if (this.prevSDP) {
4085 if (!this.hold && this.session.inactive) {
4086 // transport.cc&l=852 - http://goo.gl/0FxxqG
4087 // dtlstransport.h&l=234 - http://goo.gl/7E4sYF
4088 // http://tools.ietf.org/html/rfc4340
4090 // From RFC 4145, SDP setup attribute values.
4091 // http://goo.gl/xETJEp && http://goo.gl/3Wgcau
4092 if (createType == 'offer') {
4093 sdp = sdp.replace(/a=setup:passive|a=setup:active|a=setup:holdconn/g, 'a=setup:actpass');
4095 sdp = sdp.replace(/a=setup:actpass|a=setup:passive|a=setup:holdconn/g, 'a=setup:active');
4098 // whilst doing handshake, either media lines were "inactive"
4099 // or no media lines were present
4100 sdp = sdp.replace(/a=inactive/g, 'a=sendrecv');
4102 // this.session.inactive = false;
4106 this.setConstraints();
4107 this.connection = new RTCPeerConnection(this.iceServers, this.optionalArgument);
4109 if (this.session.data) {
4110 log('invoked: createDataChannel');
4111 this.createDataChannel();
4114 this.connection.onicecandidate = function(event) {
4115 if (!event.candidate) {
4116 if (!self.trickleIce) {
4123 if (!self.trickleIce) return;
4125 self.onicecandidate(event.candidate);
4128 function returnSDP() {
4129 if (self.returnedSDP) {
4130 self.returnedSDP = false;
4133 self.returnedSDP = true;
4135 self.onSessionDescription(self.connection.localDescription, self.streaminfo);
4138 this.connection.onaddstream = function(e) {
4139 log('onaddstream', isPluginRTC ? e.stream : toStr(e.stream));
4141 self.onaddstream(e.stream, self.session);
4144 this.connection.onremovestream = function(e) {
4145 self.onremovestream(e.stream);
4148 this.connection.onsignalingstatechange = function() {
4149 self.connection && self.oniceconnectionstatechange({
4150 iceConnectionState: self.connection.iceConnectionState,
4151 iceGatheringState: self.connection.iceGatheringState,
4152 signalingState: self.connection.signalingState
4156 this.connection.oniceconnectionstatechange = function() {
4157 if (!self.connection) return;
4159 self.oniceconnectionstatechange({
4160 iceConnectionState: self.connection.iceConnectionState,
4161 iceGatheringState: self.connection.iceGatheringState,
4162 signalingState: self.connection.signalingState
4165 if (self.trickleIce) return;
4167 if (self.connection.iceGatheringState == 'complete') {
4168 log('iceGatheringState', self.connection.iceGatheringState);
4175 setBandwidth: function(sdp) {
4176 if (isMobileDevice || isFirefox || !this.bandwidth) return sdp;
4178 var bandwidth = this.bandwidth;
4180 if (this.session.screen) {
4181 if (!bandwidth.screen) {
4182 warn('It seems that you are not using bandwidth for screen. Screen sharing is expected to fail.');
4183 } else if (bandwidth.screen < 300) {
4184 warn('It seems that you are using wrong bandwidth value for screen. Screen sharing is expected to fail.');
4188 // if screen; must use at least 300kbs
4189 if (bandwidth.screen && this.session.screen) {
4190 sdp = sdp.replace(/b=AS([^\r\n]+\r\n)/g, '');
4191 sdp = sdp.replace(/a=mid:video\r\n/g, 'a=mid:video\r\nb=AS:' + bandwidth.screen + '\r\n');
4194 // remove existing bandwidth lines
4195 if (bandwidth.audio || bandwidth.video || bandwidth.data) {
4196 sdp = sdp.replace(/b=AS([^\r\n]+\r\n)/g, '');
4199 if (bandwidth.audio) {
4200 sdp = sdp.replace(/a=mid:audio\r\n/g, 'a=mid:audio\r\nb=AS:' + bandwidth.audio + '\r\n');
4203 if (bandwidth.video) {
4204 sdp = sdp.replace(/a=mid:video\r\n/g, 'a=mid:video\r\nb=AS:' + (this.session.screen ? bandwidth.screen : bandwidth.video) + '\r\n');
4207 if (bandwidth.data && !this.preferSCTP) {
4208 sdp = sdp.replace(/a=mid:data\r\n/g, 'a=mid:data\r\nb=AS:' + bandwidth.data + '\r\n');
4213 setConstraints: function() {
4214 var sdpConstraints = setSdpConstraints({
4215 OfferToReceiveAudio: !!this.session.audio,
4216 OfferToReceiveVideo: !!this.session.video || !!this.session.screen
4219 if (this.sdpConstraints.mandatory) {
4220 sdpConstraints = setSdpConstraints(this.sdpConstraints.mandatory);
4223 this.constraints = sdpConstraints;
4225 if (this.constraints) {
4226 log('sdp-constraints', toStr(this.constraints));
4229 this.optionalArgument = {
4230 optional: this.optionalArgument.optional || [],
4231 mandatory: this.optionalArgument.mandatory || {}
4234 if (!this.preferSCTP) {
4235 this.optionalArgument.optional.push({
4236 RtpDataChannels: true
4240 log('optional-argument', toStr(this.optionalArgument));
4242 if (!isNull(this.iceServers)) {
4243 var iceCandidates = this.rtcMultiConnection.candidates;
4245 var stun = iceCandidates.stun;
4246 var turn = iceCandidates.turn;
4247 var host = iceCandidates.host;
4249 if (!isNull(iceCandidates.reflexive)) stun = iceCandidates.reflexive;
4250 if (!isNull(iceCandidates.relay)) turn = iceCandidates.relay;
4252 if (!host && !stun && turn) {
4253 this.rtcConfiguration.iceTransports = 'relay';
4254 } else if (!host && !stun && !turn) {
4255 this.rtcConfiguration.iceTransports = 'none';
4259 iceServers: this.iceServers,
4260 iceTransports: this.rtcConfiguration.iceTransports
4262 } else this.iceServers = null;
4264 log('rtc-configuration', toStr(this.iceServers));
4266 onSdpError: function(e) {
4267 var message = toStr(e);
4269 if (message && message.indexOf('RTP/SAVPF Expects at least 4 fields') != -1) {
4270 message = 'It seems that you are trying to interop RTP-datachannels with SCTP. It is not supported!';
4272 error('onSdpError:', message);
4274 onSdpSuccess: function() {
4277 onMediaError: function(err) {
4280 setRemoteDescription: function(sessionDescription, onSdpSuccess) {
4281 if (!sessionDescription) throw 'Remote session description should NOT be NULL.';
4283 if (!this.connection) return;
4285 log('setting remote description', sessionDescription.type, sessionDescription.sdp);
4288 this.connection.setRemoteDescription(
4289 new RTCSessionDescription(sessionDescription),
4290 onSdpSuccess || this.onSdpSuccess,
4292 if (error.search(/STATE_SENTINITIATE|STATE_INPROGRESS/gi) == -1) {
4293 self.onSdpError(error);
4298 addIceCandidate: function(candidate) {
4301 RTCIceCandidate(candidate, function(iceCandidate) {
4302 onAddIceCandidate(iceCandidate);
4304 } else onAddIceCandidate(new RTCIceCandidate(candidate));
4306 function onAddIceCandidate(iceCandidate) {
4307 self.connection.addIceCandidate(iceCandidate, function() {
4308 log('added:', candidate.sdpMid, candidate.candidate);
4310 error('onIceFailure', arguments, candidate.candidate);
4314 createDataChannel: function(channelIdentifier) {
4315 // skip 2nd invocation of createDataChannel
4316 if (this.channels && this.channels.length) return;
4320 if (!this.channels) this.channels = [];
4322 // protocol: 'text/chat', preset: true, stream: 16
4323 // maxRetransmits:0 && ordered:false && outOfOrderAllowed: false
4324 var dataChannelDict = {};
4326 if (this.dataChannelDict) dataChannelDict = this.dataChannelDict;
4328 if (isChrome && !this.preferSCTP) {
4329 dataChannelDict.reliable = false; // Deprecated!
4332 log('dataChannelDict', toStr(dataChannelDict));
4334 if (this.type == 'answer' || isFirefox) {
4335 this.connection.ondatachannel = function(event) {
4336 self.setChannelEvents(event.channel);
4340 if ((isChrome && this.type == 'offer') || isFirefox) {
4341 this.setChannelEvents(
4342 this.connection.createDataChannel(channelIdentifier || 'channel', dataChannelDict)
4346 setChannelEvents: function(channel) {
4349 channel.binaryType = 'arraybuffer';
4351 if (this.dataChannelDict.binaryType) {
4352 channel.binaryType = this.dataChannelDict.binaryType;
4355 channel.onmessage = function(event) {
4356 self.onmessage(event.data);
4359 var numberOfTimes = 0;
4360 channel.onopen = function() {
4361 channel.push = channel.send;
4362 channel.send = function(data) {
4363 if (self.connection.iceConnectionState == 'disconnected') {
4367 if (channel.readyState.search(/closing|closed/g) != -1) {
4371 if (channel.readyState.search(/connecting|open/g) == -1) {
4375 if (channel.readyState == 'connecting') {
4377 return setTimeout(function() {
4378 if (numberOfTimes < 20) {
4380 } else throw 'Number of times exceeded to wait for WebRTC data connection to be opened.';
4387 warn('Data transmission failed. Re-transmitting..', numberOfTimes, toStr(e));
4388 if (numberOfTimes >= 20) throw 'Number of times exceeded to resend data packets over WebRTC data channels.';
4389 setTimeout(function() {
4394 self.onopen(channel);
4397 channel.onerror = function(event) {
4398 self.onerror(event);
4401 channel.onclose = function(event) {
4402 self.onclose(event);
4405 this.channels.push(channel);
4407 addStream: function(stream) {
4408 if (!stream.streamid && !isIE) {
4409 stream.streamid = getRandomString();
4412 // todo: maybe need to add isAudio/isVideo/isScreen if missing?
4414 log('attaching stream:', stream.streamid, isPluginRTC ? stream : toStr(stream));
4416 this.connection.addStream(stream);
4418 this.sendStreamId(stream);
4419 this.getStreamInfo();
4421 attachMediaStreams: function() {
4422 var streams = this.attachStreams;
4423 for (var i = 0; i < streams.length; i++) {
4424 this.addStream(streams[i]);
4427 getStreamInfo: function() {
4428 this.streaminfo = '';
4429 var streams = this.connection.getLocalStreams();
4430 for (var i = 0; i < streams.length; i++) {
4432 this.streaminfo = JSON.stringify({
4433 streamid: streams[i].streamid || '',
4434 isScreen: !!streams[i].isScreen,
4435 isAudio: !!streams[i].isAudio,
4436 isVideo: !!streams[i].isVideo,
4437 preMuted: streams[i].preMuted || {}
4440 this.streaminfo += '----' + JSON.stringify({
4441 streamid: streams[i].streamid || '',
4442 isScreen: !!streams[i].isScreen,
4443 isAudio: !!streams[i].isAudio,
4444 isVideo: !!streams[i].isVideo,
4445 preMuted: streams[i].preMuted || {}
4450 recreateOffer: function(renegotiate, callback) {
4451 log('recreating offer');
4453 this.type = 'offer';
4454 this.session = renegotiate;
4456 // todo: make sure this doesn't affect renegotiation scenarios
4457 // this.setConstraints();
4459 this.onSessionDescription = callback;
4460 this.getStreamInfo();
4462 // one can renegotiate data connection in existing audio/video/screen connection!
4463 if (this.session.data) {
4464 this.createDataChannel();
4467 this.getLocalDescription('offer');
4469 recreateAnswer: function(sdp, session, callback) {
4470 // if(isFirefox) this.create(this.type, this);
4472 log('recreating answer');
4474 this.type = 'answer';
4475 this.session = session;
4477 // todo: make sure this doesn't affect renegotiation scenarios
4478 // this.setConstraints();
4480 this.onSessionDescription = callback;
4481 this.offerDescription = sdp;
4482 this.getStreamInfo();
4484 // one can renegotiate data connection in existing audio/video/screen connection!
4485 if (this.session.data) {
4486 this.createDataChannel();
4489 this.getLocalDescription('answer');
4495 SaveToDisk: invokeSaveAsDialog
4499 function invokeSaveAsDialog(fileUrl, fileName) {
4501 if (typeof navigator.msSaveOrOpenBlob !== 'undefined') {
4502 return navigator.msSaveOrOpenBlob(file, fileFullName);
4503 } else if (typeof navigator.msSaveBlob !== 'undefined') {
4504 return navigator.msSaveBlob(file, fileFullName);
4508 var hyperlink = document.createElement('a');
4509 hyperlink.href = fileUrl;
4510 hyperlink.target = '_blank';
4511 hyperlink.download = fileName || fileUrl;
4513 if (!!navigator.mozGetUserMedia) {
4514 hyperlink.onclick = function() {
4515 (document.body || document.documentElement).removeChild(hyperlink);
4517 (document.body || document.documentElement).appendChild(hyperlink);
4520 var evt = new MouseEvent('click', {
4526 hyperlink.dispatchEvent(evt);
4528 if (!navigator.mozGetUserMedia) {
4529 URL.revokeObjectURL(hyperlink.href);
4534 send: function(config) {
4535 var connection = config.connection;
4537 if (config.text instanceof ArrayBuffer || config.text instanceof DataView) {
4538 return config.channel.send(config.text, config._channel);
4541 var channel = config.channel,
4542 _channel = config._channel,
4543 initialText = config.text,
4544 packetSize = connection.chunkSize || 1000,
4545 textToTransfer = '',
4548 if (!isString(initialText)) {
4550 initialText = JSON.stringify(initialText);
4553 // uuid is used to uniquely identify sending instance
4554 var uuid = getRandomString();
4555 var sendingTime = new Date().getTime();
4557 sendText(initialText);
4559 function sendText(textMessage, text) {
4563 sendingTime: sendingTime
4568 data.packets = parseInt(text.length / packetSize);
4571 if (text.length > packetSize)
4572 data.message = text.slice(0, packetSize);
4574 data.message = text;
4576 data.isobject = isobject;
4579 channel.send(data, _channel);
4581 textToTransfer = text.slice(data.message.length);
4583 if (textToTransfer.length) {
4584 setTimeout(function() {
4585 sendText(null, textToTransfer);
4586 }, connection.chunkInterval || 100);
4592 function TextReceiver(connection) {
4595 function receive(data, userid, extra) {
4596 // uuid is used to uniquely identify sending instance
4597 var uuid = data.uuid;
4598 if (!content[uuid]) content[uuid] = [];
4600 content[uuid].push(data.message);
4602 var message = content[uuid].join('');
4603 if (data.isobject) message = JSON.parse(message);
4605 // latency detection
4606 var receivingTime = new Date().getTime();
4607 var latency = receivingTime - data.sendingTime;
4616 if (message.preRecordedMediaChunk) {
4617 if (!connection.preRecordedMedias[message.streamerid]) {
4618 connection.shareMediaFile(null, null, message.streamerid);
4620 connection.preRecordedMedias[message.streamerid].onData(message.chunk);
4621 } else if (connection.autoTranslateText) {
4622 e.original = e.data;
4623 connection.Translator.TranslateText(e.data, function(translatedText) {
4624 e.data = translatedText;
4625 connection.onmessage(e);
4627 } else if (message.isPartOfScreen) {
4628 connection.onpartofscreen(message);
4629 } else connection.onmessage(e);
4631 delete content[uuid];
4640 // Last time updated at Sep 25, 2015, 08:32:23
4642 // Latest file can be found here: https://cdn.webrtc-experiment.com/DetectRTC.js
4644 // Muaz Khan - www.MuazKhan.com
4645 // MIT License - www.WebRTC-Experiment.com/licence
4646 // Documentation - github.com/muaz-khan/DetectRTC
4650 // DetectRTC.hasWebcam (has webcam device!)
4651 // DetectRTC.hasMicrophone (has microphone device!)
4652 // DetectRTC.hasSpeakers (has speakers!)
4658 var navigator = window.navigator;
4660 if (navigator.mediaDevices && navigator.mediaDevices.enumerateDevices) {
4661 // Firefox 38+ seems having support of enumerateDevices
4662 // Thanks @xdumaine/enumerateDevices
4663 navigator.enumerateDevices = function(callback) {
4664 navigator.mediaDevices.enumerateDevices().then(callback);
4668 if (typeof navigator !== 'undefined') {
4669 if (typeof navigator.webkitGetUserMedia !== 'undefined') {
4670 navigator.getUserMedia = navigator.webkitGetUserMedia;
4673 if (typeof navigator.mozGetUserMedia !== 'undefined') {
4674 navigator.getUserMedia = navigator.mozGetUserMedia;
4678 getUserMedia: function() {}
4682 var isMobileDevice = !!navigator.userAgent.match(/Android|iPhone|iPad|iPod|BlackBerry|IEMobile/i);
4683 var isEdge = navigator.userAgent.indexOf('Edge') !== -1 && (!!navigator.msSaveOrOpenBlob || !!navigator.msSaveBlob);
4685 // this one can also be used:
4686 // https://www.websocket.org/js/stuff.js (DetectBrowser.js)
4688 function getBrowserInfo() {
4689 var nVer = navigator.appVersion;
4690 var nAgt = navigator.userAgent;
4691 var browserName = navigator.appName;
4692 var fullVersion = '' + parseFloat(navigator.appVersion);
4693 var majorVersion = parseInt(navigator.appVersion, 10);
4694 var nameOffset, verOffset, ix;
4696 // In Opera, the true version is after 'Opera' or after 'Version'
4697 if ((verOffset = nAgt.indexOf('Opera')) !== -1) {
4698 browserName = 'Opera';
4699 fullVersion = nAgt.substring(verOffset + 6);
4701 if ((verOffset = nAgt.indexOf('Version')) !== -1) {
4702 fullVersion = nAgt.substring(verOffset + 8);
4705 // In MSIE, the true version is after 'MSIE' in userAgent
4706 else if ((verOffset = nAgt.indexOf('MSIE')) !== -1) {
4708 fullVersion = nAgt.substring(verOffset + 5);
4710 // In Chrome, the true version is after 'Chrome'
4711 else if ((verOffset = nAgt.indexOf('Chrome')) !== -1) {
4712 browserName = 'Chrome';
4713 fullVersion = nAgt.substring(verOffset + 7);
4715 // In Safari, the true version is after 'Safari' or after 'Version'
4716 else if ((verOffset = nAgt.indexOf('Safari')) !== -1) {
4717 browserName = 'Safari';
4718 fullVersion = nAgt.substring(verOffset + 7);
4720 if ((verOffset = nAgt.indexOf('Version')) !== -1) {
4721 fullVersion = nAgt.substring(verOffset + 8);
4724 // In Firefox, the true version is after 'Firefox'
4725 else if ((verOffset = nAgt.indexOf('Firefox')) !== -1) {
4726 browserName = 'Firefox';
4727 fullVersion = nAgt.substring(verOffset + 8);
4730 // In most other browsers, 'name/version' is at the end of userAgent
4731 else if ((nameOffset = nAgt.lastIndexOf(' ') + 1) < (verOffset = nAgt.lastIndexOf('/'))) {
4732 browserName = nAgt.substring(nameOffset, verOffset);
4733 fullVersion = nAgt.substring(verOffset + 1);
4735 if (browserName.toLowerCase() === browserName.toUpperCase()) {
4736 browserName = navigator.appName;
4741 browserName = 'Edge';
4742 // fullVersion = navigator.userAgent.split('Edge/')[1];
4743 fullVersion = parseInt(navigator.userAgent.match(/Edge\/(\d+).(\d+)$/)[2], 10);
4746 // trim the fullVersion string at semicolon/space if present
4747 if ((ix = fullVersion.indexOf(';')) !== -1) {
4748 fullVersion = fullVersion.substring(0, ix);
4751 if ((ix = fullVersion.indexOf(' ')) !== -1) {
4752 fullVersion = fullVersion.substring(0, ix);
4755 majorVersion = parseInt('' + fullVersion, 10);
4757 if (isNaN(majorVersion)) {
4758 fullVersion = '' + parseFloat(navigator.appVersion);
4759 majorVersion = parseInt(navigator.appVersion, 10);
4763 fullVersion: fullVersion,
4764 version: majorVersion,
4770 Android: function() {
4771 return navigator.userAgent.match(/Android/i);
4773 BlackBerry: function() {
4774 return navigator.userAgent.match(/BlackBerry/i);
4777 return navigator.userAgent.match(/iPhone|iPad|iPod/i);
4780 return navigator.userAgent.match(/Opera Mini/i);
4782 Windows: function() {
4783 return navigator.userAgent.match(/IEMobile/i);
4786 return (isMobile.Android() || isMobile.BlackBerry() || isMobile.iOS() || isMobile.Opera() || isMobile.Windows());
4788 getOsName: function() {
4789 var osName = 'Unknown OS';
4790 if (isMobile.Android()) {
4794 if (isMobile.BlackBerry()) {
4795 osName = 'BlackBerry';
4798 if (isMobile.iOS()) {
4802 if (isMobile.Opera()) {
4803 osName = 'Opera Mini';
4806 if (isMobile.Windows()) {
4814 var osName = 'Unknown OS';
4816 if (isMobile.any()) {
4817 osName = isMobile.getOsName();
4819 if (navigator.appVersion.indexOf('Win') !== -1) {
4823 if (navigator.appVersion.indexOf('Mac') !== -1) {
4827 if (navigator.appVersion.indexOf('X11') !== -1) {
4831 if (navigator.appVersion.indexOf('Linux') !== -1) {
4837 var isCanvasSupportsStreamCapturing = false;
4838 var isVideoSupportsStreamCapturing = false;
4839 ['captureStream', 'mozCaptureStream', 'webkitCaptureStream'].forEach(function(item) {
4841 if (item in document.createElement('canvas')) {
4842 isCanvasSupportsStreamCapturing = true;
4845 if (item in document.createElement('video')) {
4846 isVideoSupportsStreamCapturing = true;
4850 // via: https://github.com/diafygi/webrtc-ips
4851 function DetectLocalIPAddress(callback) {
4852 getIPs(function(ip) {
4854 if (ip.match(/^(192\.168\.|169\.254\.|10\.|172\.(1[6-9]|2\d|3[01]))/)) {
4855 callback('Local: ' + ip);
4858 //assume the rest are public IPs
4860 callback('Public: ' + ip);
4865 //get the IP addresses associated with an account
4866 function getIPs(callback) {
4867 var ipDuplicates = {};
4869 //compatibility for firefox and chrome
4870 var RTCPeerConnection = window.RTCPeerConnection || window.mozRTCPeerConnection || window.webkitRTCPeerConnection;
4871 var useWebKit = !!window.webkitRTCPeerConnection;
4873 // bypass naive webrtc blocking using an iframe
4874 if (!RTCPeerConnection) {
4875 var iframe = document.getElementById('iframe');
4877 //<iframe id="iframe" sandbox="allow-same-origin" style="display: none"></iframe>
4878 throw 'NOTE: you need to have an iframe in the page right above the script tag.';
4880 var win = iframe.contentWindow;
4881 RTCPeerConnection = win.RTCPeerConnection || win.mozRTCPeerConnection || win.webkitRTCPeerConnection;
4882 useWebKit = !!win.webkitRTCPeerConnection;
4885 //minimal requirements for data connection
4886 var mediaConstraints = {
4888 RtpDataChannels: true
4892 //firefox already has a default stun server in about:config
4893 // media.peerconnection.default_iceservers =
4894 // [{"url": "stun:stun.services.mozilla.com"}]
4897 //add same stun server for chrome
4901 urls: 'stun:stun.services.mozilla.com'
4905 if (typeof DetectRTC !== 'undefined' && DetectRTC.browser.isFirefox && DetectRTC.browser.version <= 38) {
4907 url: servers[0].urls
4912 //construct a new RTCPeerConnection
4913 var pc = new RTCPeerConnection(servers, mediaConstraints);
4915 function handleCandidate(candidate) {
4916 //match just the IP address
4917 var ipRegex = /([0-9]{1,3}(\.[0-9]{1,3}){3})/;
4918 var ipAddress = ipRegex.exec(candidate)[1];
4921 if (ipDuplicates[ipAddress] === undefined) {
4922 callback(ipAddress);
4925 ipDuplicates[ipAddress] = true;
4928 //listen for candidate events
4929 pc.onicecandidate = function(ice) {
4930 //skip non-candidate events
4931 if (ice.candidate) {
4932 handleCandidate(ice.candidate.candidate);
4936 //create a bogus data channel
4937 pc.createDataChannel('');
4939 //create an offer sdp
4940 pc.createOffer(function(result) {
4942 //trigger the stun server request
4943 pc.setLocalDescription(result, function() {}, function() {});
4947 //wait for a while to let everything done
4948 setTimeout(function() {
4949 //read candidate info from local description
4950 var lines = pc.localDescription.sdp.split('\n');
4952 lines.forEach(function(line) {
4953 if (line.indexOf('a=candidate:') === 0) {
4954 handleCandidate(line);
4960 var MediaDevices = [];
4962 // ---------- Media Devices detection
4963 var canEnumerate = false;
4965 /*global MediaStreamTrack:true */
4966 if (typeof MediaStreamTrack !== 'undefined' && 'getSources' in MediaStreamTrack) {
4967 canEnumerate = true;
4968 } else if (navigator.mediaDevices && !!navigator.mediaDevices.enumerateDevices) {
4969 canEnumerate = true;
4972 var hasMicrophone = canEnumerate;
4973 var hasSpeakers = canEnumerate;
4974 var hasWebcam = canEnumerate;
4976 // http://dev.w3.org/2011/webrtc/editor/getusermedia.html#mediadevices
4977 // todo: switch to enumerateDevices when landed in canary.
4978 function checkDeviceSupport(callback) {
4979 // This method is useful only for Chrome!
4981 if (!navigator.enumerateDevices && window.MediaStreamTrack && window.MediaStreamTrack.getSources) {
4982 navigator.enumerateDevices = window.MediaStreamTrack.getSources.bind(window.MediaStreamTrack);
4985 if (!navigator.enumerateDevices && navigator.enumerateDevices) {
4986 navigator.enumerateDevices = navigator.enumerateDevices.bind(navigator);
4989 if (!navigator.enumerateDevices) {
4997 navigator.enumerateDevices(function(devices) {
4998 devices.forEach(function(_device) {
5000 for (var d in _device) {
5001 device[d] = _device[d];
5005 MediaDevices.forEach(function(d) {
5006 if (d.id === device.id) {
5015 // if it is MediaStreamTrack.getSources
5016 if (device.kind === 'audio') {
5017 device.kind = 'audioinput';
5020 if (device.kind === 'video') {
5021 device.kind = 'videoinput';
5024 if (!device.deviceId) {
5025 device.deviceId = device.id;
5029 device.id = device.deviceId;
5032 if (!device.label) {
5033 device.label = 'Please invoke getUserMedia once.';
5035 device.label = 'HTTPs is required to get label of this ' + device.kind + ' device.';
5039 if (device.kind === 'audioinput' || device.kind === 'audio') {
5040 hasMicrophone = true;
5043 if (device.kind === 'audiooutput') {
5047 if (device.kind === 'videoinput' || device.kind === 'video') {
5051 // there is no 'videoouput' in the spec.
5053 MediaDevices.push(device);
5056 if (typeof DetectRTC !== 'undefined') {
5057 DetectRTC.MediaDevices = MediaDevices;
5058 DetectRTC.hasMicrophone = hasMicrophone;
5059 DetectRTC.hasSpeakers = hasSpeakers;
5060 DetectRTC.hasWebcam = hasWebcam;
5069 // check for microphone/camera support!
5070 checkDeviceSupport();
5075 // DetectRTC.browser.name || DetectRTC.browser.version || DetectRTC.browser.fullVersion
5076 DetectRTC.browser = getBrowserInfo();
5078 // DetectRTC.isChrome || DetectRTC.isFirefox || DetectRTC.isEdge
5079 DetectRTC.browser['is' + DetectRTC.browser.name] = true;
5081 var isHTTPs = location.protocol === 'https:';
5082 var isNodeWebkit = !!(window.process && (typeof window.process === 'object') && window.process.versions && window.process.versions['node-webkit']);
5084 // --------- Detect if system supports WebRTC 1.0 or WebRTC 1.1.
5085 var isWebRTCSupported = false;
5086 ['webkitRTCPeerConnection', 'mozRTCPeerConnection', 'RTCIceGatherer'].forEach(function(item) {
5087 if (item in window) {
5088 isWebRTCSupported = true;
5091 DetectRTC.isWebRTCSupported = isWebRTCSupported;
5094 DetectRTC.isORTCSupported = typeof RTCIceGatherer !== 'undefined';
5096 // --------- Detect if system supports screen capturing API
5097 var isScreenCapturingSupported = false;
5098 if (DetectRTC.browser.isChrome && DetectRTC.browser.version >= 35) {
5099 isScreenCapturingSupported = true;
5100 } else if (DetectRTC.browser.isFirefox && DetectRTC.browser.version >= 34) {
5101 isScreenCapturingSupported = true;
5105 isScreenCapturingSupported = false;
5107 DetectRTC.isScreenCapturingSupported = isScreenCapturingSupported;
5109 // --------- Detect if WebAudio API are supported
5111 ['AudioContext', 'webkitAudioContext', 'mozAudioContext', 'msAudioContext'].forEach(function(item) {
5112 if (webAudio.isSupported && webAudio.isCreateMediaStreamSourceSupported) {
5115 if (item in window) {
5116 webAudio.isSupported = true;
5118 if ('createMediaStreamSource' in window[item].prototype) {
5119 webAudio.isCreateMediaStreamSourceSupported = true;
5123 DetectRTC.isAudioContextSupported = webAudio.isSupported;
5124 DetectRTC.isCreateMediaStreamSourceSupported = webAudio.isCreateMediaStreamSourceSupported;
5126 // ---------- Detect if SCTP/RTP channels are supported.
5128 var isRtpDataChannelsSupported = false;
5129 if (DetectRTC.browser.isChrome && DetectRTC.browser.version > 31) {
5130 isRtpDataChannelsSupported = true;
5132 DetectRTC.isRtpDataChannelsSupported = isRtpDataChannelsSupported;
5134 var isSCTPSupportd = false;
5135 if (DetectRTC.browser.isFirefox && DetectRTC.browser.version > 28) {
5136 isSCTPSupportd = true;
5137 } else if (DetectRTC.browser.isChrome && DetectRTC.browser.version > 25) {
5138 isSCTPSupportd = true;
5139 } else if (DetectRTC.browser.isOpera && DetectRTC.browser.version >= 11) {
5140 isSCTPSupportd = true;
5142 DetectRTC.isSctpDataChannelsSupported = isSCTPSupportd;
5146 DetectRTC.isMobileDevice = isMobileDevice; // "isMobileDevice" boolean is defined in "getBrowserInfo.js"
5150 DetectRTC.isWebSocketsSupported = 'WebSocket' in window && 2 === window.WebSocket.CLOSING;
5151 DetectRTC.isWebSocketsBlocked = 'Checking';
5153 if (DetectRTC.isWebSocketsSupported) {
5155 var href = window.location.href;
5156 var hostPatt = new RegExp(window.location.host +"/[^/]*");
5157 var res = hostPatt.exec(href);
5158 var protocol = window.location.protocol.replace("http","ws");
5160 var signalingServerPath = protocol + "//" + res + "/contact";
5161 var websocket = new WebSocket(signalingServerPath)
5162 // var websocket = new WebSocket('wss://echo.websocket.org:443/');
5164 websocket.onopen = function() {
5165 DetectRTC.isWebSocketsBlocked = false;
5167 if (DetectRTC.loadCallback) {
5168 DetectRTC.loadCallback();
5171 websocket.onerror = function() {
5172 DetectRTC.isWebSocketsBlocked = true;
5174 if (DetectRTC.loadCallback) {
5175 DetectRTC.loadCallback();
5181 var isGetUserMediaSupported = false;
5182 if (navigator.getUserMedia) {
5183 isGetUserMediaSupported = true;
5184 } else if (navigator.mediaDevices && navigator.mediaDevices.getUserMedia) {
5185 isGetUserMediaSupported = true;
5187 if (DetectRTC.browser.isChrome && DetectRTC.browser.version >= 47 && !isHTTPs) {
5188 DetectRTC.isGetUserMediaSupported = 'Requires HTTPs';
5190 DetectRTC.isGetUserMediaSupported = isGetUserMediaSupported;
5193 DetectRTC.osName = osName; // "osName" is defined in "detectOSName.js"
5196 DetectRTC.isCanvasSupportsStreamCapturing = isCanvasSupportsStreamCapturing;
5197 DetectRTC.isVideoSupportsStreamCapturing = isVideoSupportsStreamCapturing;
5200 DetectRTC.DetectLocalIPAddress = DetectLocalIPAddress;
5203 DetectRTC.load = function(callback) {
5204 this.loadCallback = callback;
5206 checkDeviceSupport(callback);
5209 DetectRTC.MediaDevices = MediaDevices;
5210 DetectRTC.hasMicrophone = hasMicrophone;
5211 DetectRTC.hasSpeakers = hasSpeakers;
5212 DetectRTC.hasWebcam = hasWebcam;
5215 var isSetSinkIdSupported = false;
5216 if ('setSinkId' in document.createElement('video')) {
5217 isSetSinkIdSupported = true;
5219 DetectRTC.isSetSinkIdSupported = isSetSinkIdSupported;
5222 var isRTPSenderReplaceTracksSupported = false;
5223 if (DetectRTC.browser.isFirefox /*&& DetectRTC.browser.version > 39*/ ) {
5224 /*global mozRTCPeerConnection:true */
5225 if ('getSenders' in mozRTCPeerConnection.prototype) {
5226 isRTPSenderReplaceTracksSupported = true;
5228 } else if (DetectRTC.browser.isChrome) {
5229 /*global webkitRTCPeerConnection:true */
5230 if ('getSenders' in webkitRTCPeerConnection.prototype) {
5231 isRTPSenderReplaceTracksSupported = true;
5234 DetectRTC.isRTPSenderReplaceTracksSupported = isRTPSenderReplaceTracksSupported;
5237 var isRemoteStreamProcessingSupported = false;
5238 if (DetectRTC.browser.isFirefox && DetectRTC.browser.version > 38) {
5239 isRemoteStreamProcessingSupported = true;
5241 DetectRTC.isRemoteStreamProcessingSupported = isRemoteStreamProcessingSupported;
5244 var isApplyConstraintsSupported = false;
5246 /*global MediaStreamTrack:true */
5247 if (typeof MediaStreamTrack !== 'undefined' && 'applyConstraints' in MediaStreamTrack.prototype) {
5248 isApplyConstraintsSupported = true;
5250 DetectRTC.isApplyConstraintsSupported = isApplyConstraintsSupported;
5253 var isMultiMonitorScreenCapturingSupported = false;
5254 if (DetectRTC.browser.isFirefox && DetectRTC.browser.version >= 43) {
5255 // version 43 merely supports platforms for multi-monitors
5256 // version 44 will support exact multi-monitor selection i.e. you can select any monitor for screen capturing.
5257 isMultiMonitorScreenCapturingSupported = true;
5259 DetectRTC.isMultiMonitorScreenCapturingSupported = isMultiMonitorScreenCapturingSupported;
5261 window.DetectRTC = DetectRTC;
5265 // DetectRTC extender
5268 DetectRTC.screen = {
5269 chromeMediaSource: 'screen',
5270 extensionid: ReservedExtensionID,
5271 getSourceId: function(callback) {
5272 if (!callback) throw '"callback" parameter is mandatory.';
5274 // make sure that chrome extension is installed.
5275 if (!!DetectRTC.screen.status) {
5276 onstatus(DetectRTC.screen.status);
5277 } else DetectRTC.screen.getChromeExtensionStatus(onstatus);
5279 function onstatus(status) {
5280 if (status == 'installed-enabled') {
5281 screenCallback = callback;
5282 window.postMessage('get-sourceId', '*');
5286 DetectRTC.screen.chromeMediaSource = 'screen';
5287 callback('No-Response'); // chrome extension isn't available
5290 onMessageCallback: function(data) {
5291 if (!(isString(data) || !!data.sourceId)) return;
5293 log('chrome message', data);
5295 // "cancel" button is clicked
5296 if (data == 'PermissionDeniedError') {
5297 DetectRTC.screen.chromeMediaSource = 'PermissionDeniedError';
5298 if (screenCallback) return screenCallback('PermissionDeniedError');
5299 else throw new Error('PermissionDeniedError');
5302 // extension notified his presence
5303 if (data == 'rtcmulticonnection-extension-loaded') {
5304 DetectRTC.screen.chromeMediaSource = 'desktop';
5305 if (DetectRTC.screen.onScreenCapturingExtensionAvailable) {
5306 DetectRTC.screen.onScreenCapturingExtensionAvailable();
5308 // make sure that this event isn't fired multiple times
5309 DetectRTC.screen.onScreenCapturingExtensionAvailable = null;
5313 // extension shared temp sourceId
5314 if (data.sourceId) {
5315 DetectRTC.screen.sourceId = data.sourceId;
5316 if (screenCallback) screenCallback(DetectRTC.screen.sourceId);
5319 getChromeExtensionStatus: function(extensionid, callback) {
5320 function _callback(status) {
5321 DetectRTC.screen.status = status;
5325 if (isFirefox) return _callback('not-chrome');
5327 if (arguments.length != 2) {
5328 callback = extensionid;
5329 extensionid = this.extensionid;
5332 var image = document.createElement('img');
5333 image.src = 'chrome-extension://' + extensionid + '/icon.png';
5334 image.onload = function() {
5335 DetectRTC.screen.chromeMediaSource = 'screen';
5336 window.postMessage('are-you-there', '*');
5337 setTimeout(function() {
5338 if (DetectRTC.screen.chromeMediaSource == 'screen') {
5340 DetectRTC.screen.chromeMediaSource == 'desktop' ? 'installed-enabled' : 'installed-disabled' /* if chrome extension isn't permitted for current domain, then it will be installed-disabled all the time even if extension is enabled. */
5342 } else _callback('installed-enabled');
5345 image.onerror = function() {
5346 _callback('not-installed');
5352 if (!window.addEventListener) {
5353 window.addEventListener = function(el, eventName, eventHandler) {
5354 if (!el.attachEvent) return;
5355 el.attachEvent('on' + eventName, eventHandler);
5359 function listenEventHandler(eventName, eventHandler) {
5360 window.removeEventListener(eventName, eventHandler);
5361 window.addEventListener(eventName, eventHandler, false);
5364 window.addEventListener('message', function(event) {
5365 if (event.origin != window.location.origin) {
5369 DetectRTC.screen.onMessageCallback(event.data);
5372 function setDefaults(connection) {
5373 var DetectRTC = window.DetectRTC || {};
5375 // www.RTCMultiConnection.org/docs/userid/
5376 connection.userid = getRandomString();
5378 // www.RTCMultiConnection.org/docs/session/
5379 connection.session = {
5384 // www.RTCMultiConnection.org/docs/maxParticipantsAllowed/
5385 connection.maxParticipantsAllowed = 256;
5387 // www.RTCMultiConnection.org/docs/direction/
5388 // 'many-to-many' / 'one-to-many' / 'one-to-one' / 'one-way'
5389 connection.direction = 'many-to-many';
5391 // www.RTCMultiConnection.org/docs/mediaConstraints/
5392 connection.mediaConstraints = {
5393 mandatory: {}, // kept for backward compatibility
5394 optional: [], // kept for backward compatibility
5405 // www.RTCMultiConnection.org/docs/candidates/
5406 connection.candidates = {
5412 connection.sdpConstraints = {};
5414 // as @serhanters proposed in #225
5415 // it will auto fix "all" renegotiation scenarios
5416 connection.sdpConstraints.mandatory = {
5417 OfferToReceiveAudio: true,
5418 OfferToReceiveVideo: true
5421 connection.privileges = {
5422 canStopRemoteStream: false, // user can stop remote streams
5423 canMuteRemoteStream: false // user can mute remote streams
5426 connection.iceProtocols = {
5431 // www.RTCMultiConnection.org/docs/preferSCTP/
5432 connection.preferSCTP = isFirefox || chromeVersion >= 32 ? true : false;
5433 connection.chunkInterval = isFirefox || chromeVersion >= 32 ? 100 : 500; // 500ms for RTP and 100ms for SCTP
5434 connection.chunkSize = isFirefox || chromeVersion >= 32 ? 13 * 1000 : 1000; // 1000 chars for RTP and 13000 chars for SCTP
5436 // www.RTCMultiConnection.org/docs/fakeDataChannels/
5437 connection.fakeDataChannels = false;
5439 connection.waitUntilRemoteStreamStartsFlowing = null; // NULL == true
5441 // auto leave on page unload
5442 connection.leaveOnPageUnload = true;
5444 // get ICE-servers from XirSys
5445 connection.getExternalIceServers = isChrome;
5447 // www.RTCMultiConnection.org/docs/UA/
5449 isFirefox: isFirefox,
5451 isMobileDevice: isMobileDevice,
5452 version: isChrome ? chromeVersion : firefoxVersion,
5453 isNodeWebkit: isNodeWebkit,
5459 // file queue: to store previous file objects in memory;
5460 // and stream over newly connected peers
5461 // www.RTCMultiConnection.org/docs/fileQueue/
5462 connection.fileQueue = {};
5464 // this array is aimed to store all renegotiated streams' session-types
5465 connection.renegotiatedSessions = {};
5467 // www.RTCMultiConnection.org/docs/channels/
5468 connection.channels = {};
5470 // www.RTCMultiConnection.org/docs/extra/
5471 connection.extra = {};
5473 // www.RTCMultiConnection.org/docs/bandwidth/
5474 connection.bandwidth = {
5475 screen: 300 // 300kbps (dirty workaround)
5478 // www.RTCMultiConnection.org/docs/caniuse/
5479 connection.caniuse = {
5480 RTCPeerConnection: DetectRTC.isWebRTCSupported,
5481 getUserMedia: !!navigator.webkitGetUserMedia || !!navigator.mozGetUserMedia,
5482 AudioContext: DetectRTC.isAudioContextSupported,
5484 // there is no way to check whether "getUserMedia" flag is enabled or not!
5485 ScreenSharing: DetectRTC.isScreenCapturingSupported,
5486 RtpDataChannels: DetectRTC.isRtpDataChannelsSupported,
5487 SctpDataChannels: DetectRTC.isSctpDataChannelsSupported
5490 // www.RTCMultiConnection.org/docs/snapshots/
5491 connection.snapshots = {};
5493 // www.WebRTC-Experiment.com/demos/MediaStreamTrack.getSources.html
5494 connection._mediaSources = {};
5496 // www.RTCMultiConnection.org/docs/devices/
5497 connection.devices = {};
5499 // www.RTCMultiConnection.org/docs/language/ (to see list of all supported languages)
5500 connection.language = 'en';
5502 // www.RTCMultiConnection.org/docs/autoTranslateText/
5503 connection.autoTranslateText = false;
5505 // please use your own Google Translate API key
5506 // Google Translate is a paid service.
5507 connection.googKey = 'AIzaSyCgB5hmFY74WYB-EoWkhr9cAGr6TiTHrEE';
5509 connection.localStreamids = [];
5510 connection.localStreams = {};
5512 // this object stores pre-recorded media streaming uids
5513 // multiple pre-recorded media files can be streamed concurrently.
5514 connection.preRecordedMedias = {};
5516 // www.RTCMultiConnection.org/docs/attachStreams/
5517 connection.attachStreams = [];
5519 // www.RTCMultiConnection.org/docs/detachStreams/
5520 connection.detachStreams = [];
5522 connection.optionalArgument = {
5524 DtlsSrtpKeyAgreement: true
5526 googImprovedWifiBwe: true
5528 googScreencastMinBitrate: 300
5533 connection.dataChannelDict = {};
5535 // www.RTCMultiConnection.org/docs/dontAttachStream/
5536 connection.dontAttachStream = false;
5538 // www.RTCMultiConnection.org/docs/dontCaptureUserMedia/
5539 connection.dontCaptureUserMedia = false;
5541 // this feature added to keep users privacy and
5542 // make sure HTTPs pages NEVER auto capture users media
5543 // isChrome && location.protocol == 'https:'
5544 connection.preventSSLAutoAllowed = false;
5546 connection.autoReDialOnFailure = true;
5547 connection.isInitiator = false;
5549 // access DetectRTC.js features directly!
5550 connection.DetectRTC = DetectRTC;
5552 // you can falsify it to merge all ICE in SDP and share only SDP!
5553 // such mechanism is useful for SIP/XMPP and XMLHttpRequest signaling
5554 // bug: renegotiation fails if "trickleIce" is false
5555 connection.trickleIce = true;
5557 // this object stores list of all sessions in current channel
5558 connection.sessionDescriptions = {};
5560 // this object stores current user's session-description
5561 // it is set only for initiator
5562 // it is set as soon as "open" method is invoked.
5563 connection.sessionDescription = null;
5565 // resources used in RTCMultiConnection
5566 connection.resources = {
5568 /* Commenting this block as we do not wasnt external dependencies
5570 RecordRTC: 'https://cdn.webrtc-experiment.com/RecordRTC.js',
5571 PreRecordedMediaStreamer: 'https://cdn.webrtc-experiment.com/PreRecordedMediaStreamer.js',
5572 customGetUserMediaBar: 'https://cdn.webrtc-experiment.com/navigator.customGetUserMediaBar.js',
5573 html2canvas: 'https://cdn.webrtc-experiment.com/screenshot.js',
5574 hark: 'https://cdn.webrtc-experiment.com/hark.js',
5575 firebase: 'https://cdn.webrtc-experiment.com/firebase.js',
5576 firebaseio: 'https://webrtc-signaling.firebaseio.com/',
5580 getConnectionStats: 'https://cdn.webrtc-experiment.com/getConnectionStats.js',
5581 FileBufferReader: 'https://cdn.webrtc-experiment.com/FileBufferReader.js'
5589 // www.RTCMultiConnection.org/docs/body/
5590 connection.body = document.body || document.documentElement;
5591 connection.screenbody = null;// document.body || document.documentElement;
5592 connection.videobody = null;//document.body || document.documentElement;
5594 // www.RTCMultiConnection.org/docs/peers/
5595 connection.peers = {};
5597 // www.RTCMultiConnection.org/docs/firebase/
5598 connection.firebase = 'chat';
5600 connection.numberOfSessions = 0;
5601 connection.numberOfConnectedUsers = 0;
5603 // by default, data-connections will always be getting
5604 // FileBufferReader.js if absent.
5605 connection.enableFileSharing = true;
5607 // www.RTCMultiConnection.org/docs/autoSaveToDisk/
5608 // to make sure file-saver dialog is not invoked.
5609 connection.autoSaveToDisk = false;
5611 connection.processSdp = function(sdp) {
5616 // www.RTCMultiConnection.org/docs/onmessage/
5617 connection.onmessage = function(e) {
5618 log('onmessage', toStr(e));
5621 // www.RTCMultiConnection.org/docs/onopen/
5622 connection.onopen = function(e) {
5623 log('Data connection is opened between you and', e.userid);
5626 // www.RTCMultiConnection.org/docs/onerror/
5627 connection.onerror = function(e) {
5628 error(onerror, toStr(e));
5631 // www.RTCMultiConnection.org/docs/onclose/
5632 connection.onclose = function(e) {
5633 warn('onclose', toStr(e));
5635 // todo: should we use "stop" or "remove"?
5636 // BTW, it is remote user!
5637 connection.streams.remove({
5642 var progressHelper = {};
5644 // www.RTCMultiConnection.org/docs/onFileStart/
5645 connection.onFileStart = function(file) {
5646 var div = document.createElement('div');
5647 div.title = file.name;
5648 div.innerHTML = '<label>0%</label> <progress></progress>';
5649 connection.body.insertBefore(div, connection.body.firstChild);
5650 progressHelper[file.uuid] = {
5652 progress: div.querySelector('progress'),
5653 label: div.querySelector('label')
5655 progressHelper[file.uuid].progress.max = file.maxChunks;
5658 // www.RTCMultiConnection.org/docs/onFileProgress/
5659 connection.onFileProgress = function(chunk) {
5660 var helper = progressHelper[chunk.uuid];
5661 if (!helper) return;
5662 helper.progress.value = chunk.currentPosition || chunk.maxChunks || helper.progress.max;
5663 updateLabel(helper.progress, helper.label);
5666 // www.RTCMultiConnection.org/docs/onFileEnd/
5667 connection.onFileEnd = function(file) {
5668 if (progressHelper[file.uuid]) progressHelper[file.uuid].div.innerHTML = '<a href="' + file.url + '" target="_blank" download="' + file.name + '">' + file.name + '</a>';
5670 // for backward compatibility
5671 if (connection.onFileSent || connection.onFileReceived) {
5672 if (connection.onFileSent) connection.onFileSent(file, file.uuid);
5673 if (connection.onFileReceived) connection.onFileReceived(file.name, file);
5677 function updateLabel(progress, label) {
5678 if (progress.position == -1) return;
5679 var position = +progress.position.toFixed(2).split('.')[1] || 100;
5680 label.innerHTML = position + '%';
5683 // www.RTCMultiConnection.org/docs/onstream/
5684 connection.onstream = function(e) {
5687 if(e.isVideo || e.isAudio)
5689 var videoTag = e.mediaElement;
5690 var videoType = e.type;
5691 var parentDiv = connection.videobody;
5693 if(videoType == "local")
5695 videoTag.style.top = "auto";
5696 videoTag.style.position = "absolute";
5697 //videoTag.style.left = (parentDiv.offsetWidth - 160) + "px";
5698 videoTag.style.height = "150px";
5699 videoTag.style.width = "150px";
5700 videoTag.style.zIndex = 1;
5703 else if(videoType == "remote")
5705 videoTag.style.top = "auto";
5706 videoTag.style.position = "absolute";
5707 videoTag.style.zIndex = -1;
5710 parentDiv.appendChild(videoTag);
5714 var screenTag = e.mediaElement;
5715 var screenType = e.type;
5716 var parentDiv = connection.screenbody;
5718 if(screenType == "local")
5720 // no need to display this because the person sharing his/her screen doesn't have to see whats being shared
5721 // enabled for demo purposes
5722 parentDiv.appendChild(screenTag);
5724 else if(screenType == "remote")
5726 parentDiv.appendChild(screenTag);
5733 connection.body.insertBefore(e.mediaElement, connection.body.firstChild);
5738 // www.RTCMultiConnection.org/docs/onStreamEndedHandler/
5739 connection.onstreamended = function(e) {
5740 log('onStreamEndedHandler:', e);
5742 if (!e.mediaElement) {
5743 return warn('Event.mediaElement is undefined', e);
5745 if (!e.mediaElement.parentNode) {
5746 e.mediaElement = document.getElementById(e.streamid);
5748 if (!e.mediaElement) {
5749 return warn('Event.mediaElement is undefined', e);
5752 if (!e.mediaElement.parentNode) {
5753 return warn('Event.mediElement.parentNode is null.', e);
5757 e.mediaElement.parentNode.removeChild(e.mediaElement);
5760 // todo: need to write documentation link
5761 connection.onSessionClosed = function(session) {
5762 if (session.isEjected) {
5763 warn(session.userid, 'ejected you.');
5764 } else warn('Session has been closed.', session);
5767 // www.RTCMultiConnection.org/docs/onmute/
5768 connection.onmute = function(e) {
5769 if (e.isVideo && e.mediaElement) {
5770 e.mediaElement.pause();
5771 e.mediaElement.setAttribute('poster', e.snapshot || connection.resources.muted);
5773 if (e.isAudio && e.mediaElement) {
5774 e.mediaElement.muted = true;
5778 // www.RTCMultiConnection.org/docs/onunmute/
5779 connection.onunmute = function(e) {
5780 if (e.isVideo && e.mediaElement) {
5781 e.mediaElement.play();
5782 e.mediaElement.removeAttribute('poster');
5784 if (e.isAudio && e.mediaElement) {
5785 e.mediaElement.muted = false;
5789 // www.RTCMultiConnection.org/docs/onleave/
5790 connection.onleave = function(e) {
5791 log('onleave', toStr(e));
5794 connection.token = getRandomString;
5796 connection.peers[connection.userid] = {
5800 renegotiate: function() {},
5801 addStream: function() {},
5802 hold: function() {},
5803 unhold: function() {},
5804 changeBandwidth: function() {},
5805 sharePartOfScreen: function() {}
5808 connection._skip = ['stop', 'mute', 'unmute', '_private', '_selectStreams', 'selectFirst', 'selectAll', 'remove'];
5810 // www.RTCMultiConnection.org/docs/streams/
5811 connection.streams = {
5812 mute: function(session) {
5813 this._private(session, true);
5815 unmute: function(session) {
5816 this._private(session, false);
5818 _private: function(session, enabled) {
5819 if (session && !isString(session)) {
5820 for (var stream in this) {
5821 if (connection._skip.indexOf(stream) == -1) {
5822 _muteOrUnMute(this[stream], session, enabled);
5826 function _muteOrUnMute(stream, session, isMute) {
5827 if (session.local && stream.type != 'local') return;
5828 if (session.remote && stream.type != 'remote') return;
5830 if (session.isScreen && !stream.isScreen) return;
5831 if (session.isAudio && !stream.isAudio) return;
5832 if (session.isVideo && !stream.isVideo) return;
5834 if (isMute) stream.mute(session);
5835 else stream.unmute(session);
5840 // implementation from #68
5841 for (var stream in this) {
5842 if (connection._skip.indexOf(stream) == -1) {
5843 this[stream]._private(session, enabled);
5847 stop: function(type) {
5849 for (var stream in this) {
5850 if (connection._skip.indexOf(stream) == -1) {
5851 _stream = this[stream];
5853 if (!type) _stream.stop();
5855 else if (isString(type)) {
5856 // connection.streams.stop('screen');
5858 config[type] = true;
5859 _stopStream(_stream, config);
5860 } else _stopStream(_stream, type);
5864 function _stopStream(_stream, config) {
5865 // connection.streams.stop({ remote: true, userid: 'remote-userid' });
5866 if (config.userid && _stream.userid != config.userid) return;
5868 if (config.local && _stream.type != 'local') return;
5869 if (config.remote && _stream.type != 'remote') return;
5871 if (config.screen && !!_stream.isScreen) {
5875 if (config.audio && !!_stream.isAudio) {
5879 if (config.video && !!_stream.isVideo) {
5883 // connection.streams.stop('local');
5884 if (!config.audio && !config.video && !config.screen) {
5889 remove: function(type) {
5891 for (var stream in this) {
5892 if (connection._skip.indexOf(stream) == -1) {
5893 _stream = this[stream];
5895 if (!type) _stopAndRemoveStream(_stream, {
5900 else if (isString(type)) {
5901 // connection.streams.stop('screen');
5903 config[type] = true;
5904 _stopAndRemoveStream(_stream, config);
5905 } else _stopAndRemoveStream(_stream, type);
5909 function _stopAndRemoveStream(_stream, config) {
5910 // connection.streams.remove({ remote: true, userid: 'remote-userid' });
5911 if (config.userid && _stream.userid != config.userid) return;
5913 if (config.local && _stream.type != 'local') return;
5914 if (config.remote && _stream.type != 'remote') return;
5916 if (config.screen && !!_stream.isScreen) {
5920 if (config.audio && !!_stream.isAudio) {
5924 if (config.video && !!_stream.isVideo) {
5928 // connection.streams.remove('local');
5929 if (!config.audio && !config.video && !config.screen) {
5934 function endStream(_stream) {
5935 onStreamEndedHandler(_stream, connection);
5936 delete connection.streams[_stream.streamid];
5939 selectFirst: function(args) {
5940 return this._selectStreams(args, false);
5942 selectAll: function(args) {
5943 return this._selectStreams(args, true);
5945 _selectStreams: function(args, all) {
5946 if (!args || isString(args) || isEmpty(args)) throw 'Invalid arguments.';
5948 // if userid is used then both local/remote shouldn't be auto-set
5949 if (isNull(args.local) && isNull(args.remote) && isNull(args.userid)) {
5950 args.local = args.remote = true;
5953 if (!args.isAudio && !args.isVideo && !args.isScreen) {
5954 args.isAudio = args.isVideo = args.isScreen = true;
5957 var selectedStreams = [];
5958 for (var stream in this) {
5959 if (connection._skip.indexOf(stream) == -1 && (stream = this[stream]) && ((args.local && stream.type == 'local') || (args.remote && stream.type == 'remote') || (args.userid && stream.userid == args.userid))) {
5960 if (args.isVideo && stream.isVideo) {
5961 selectedStreams.push(stream);
5964 if (args.isAudio && stream.isAudio) {
5965 selectedStreams.push(stream);
5968 if (args.isScreen && stream.isScreen) {
5969 selectedStreams.push(stream);
5974 return !!all ? selectedStreams : selectedStreams[0];
5978 var iceServers = [];
5981 // these servers are provided by research
5983 url: '' /*TODO To test this WebRTC with some open stun and turn test servers*/
5987 url: 'turn:207.140.168.120:3478',
5993 url: 'turn:207.140.168.120:443?transport=tcp',
5994 credential: 'harmfulmustard',
5998 /* CHANGED: Fusion: These are servers for testing purposes
6001 url: 'stun:stun.l.google.com:19302'
6005 url: 'stun:stun.anyfirewall.com:3478'
6009 url: 'turn:turn.bistri.com:80',
6010 credential: 'homeo',
6015 url: 'turn:turn.anyfirewall.com:443?transport=tcp',
6016 credential: 'webrtc',
6022 connection.iceServers = iceServers;
6024 connection.rtcConfiguration = {
6026 iceTransports: 'all', // none || relay || all - ref: http://goo.gl/40I39K
6030 // www.RTCMultiConnection.org/docs/media/
6031 connection.media = {
6032 min: function(width, height) {
6033 if (!connection.mediaConstraints.video) return;
6035 if (!connection.mediaConstraints.video.mandatory) {
6036 connection.mediaConstraints.video.mandatory = {};
6038 connection.mediaConstraints.video.mandatory.minWidth = width;
6039 connection.mediaConstraints.video.mandatory.minHeight = height;
6041 max: function(width, height) {
6042 if (!connection.mediaConstraints.video) return;
6044 if (!connection.mediaConstraints.video.mandatory) {
6045 connection.mediaConstraints.video.mandatory = {};
6048 connection.mediaConstraints.video.mandatory.maxWidth = width;
6049 connection.mediaConstraints.video.mandatory.maxHeight = height;
6053 connection._getStream = function(event) {
6054 var resultingObject = merge({
6055 sockets: event.socket ? [event.socket] : []
6058 resultingObject.stop = function() {
6061 self.sockets.forEach(function(socket) {
6062 if (self.type == 'local') {
6064 streamid: self.streamid,
6069 if (self.type == 'remote') {
6071 promptStreamStop: true,
6072 streamid: self.streamid
6077 if (self.type == 'remote') return;
6079 var stream = self.stream;
6080 if (stream) self.rtcMultiConnection.stopMediaStream(stream);
6083 resultingObject.mute = function(session) {
6085 this._private(session, true);
6088 resultingObject.unmute = function(session) {
6090 this._private(session, false);
6093 function muteOrUnmuteLocally(session, isPause, mediaElement) {
6094 if (!mediaElement) return;
6095 var lastPauseState = mediaElement.onpause;
6096 var lastPlayState = mediaElement.onplay;
6097 mediaElement.onpause = mediaElement.onplay = function() {};
6099 if (isPause) mediaElement.pause();
6100 else mediaElement.play();
6102 mediaElement.onpause = lastPauseState;
6103 mediaElement.onplay = lastPlayState;
6106 resultingObject._private = function(session, enabled) {
6107 if (session && !isNull(session.sync) && session.sync == false) {
6108 muteOrUnmuteLocally(session, enabled, this.mediaElement);
6120 resultingObject.startRecording = function(session) {
6130 if (isString(session)) {
6132 audio: session == 'audio',
6133 video: session == 'video'
6137 if (!window.RecordRTC) {
6138 return loadScript(self.rtcMultiConnection.resources.RecordRTC, function() {
6139 self.startRecording(session);
6143 log('started recording session', session);
6145 self.videoRecorder = self.audioRecorder = null;
6148 // firefox supports both audio/video recording in single webm file
6149 if (session.video) {
6150 self.videoRecorder = RecordRTC(self.stream, {
6153 } else if (session.audio) {
6154 self.audioRecorder = RecordRTC(self.stream, {
6158 } else if (isChrome) {
6159 // chrome supports recording in two separate files: WAV and WebM
6160 if (session.video) {
6161 self.videoRecorder = RecordRTC(self.stream, {
6166 if (session.audio) {
6167 self.audioRecorder = RecordRTC(self.stream, {
6173 if (self.audioRecorder) {
6174 self.audioRecorder.startRecording();
6177 if (self.videoRecorder) self.videoRecorder.startRecording();
6180 resultingObject.stopRecording = function(callback, session) {
6188 if (isString(session)) {
6190 audio: session == 'audio',
6191 video: session == 'video'
6195 log('stopped recording session', session);
6199 if (session.audio && self.audioRecorder) {
6200 self.audioRecorder.stopRecording(function() {
6201 if (session.video && self.videoRecorder) {
6202 self.videoRecorder.stopRecording(function() {
6204 audio: self.audioRecorder.getBlob(),
6205 video: self.videoRecorder.getBlob()
6209 audio: self.audioRecorder.getBlob()
6212 } else if (session.video && self.videoRecorder) {
6213 self.videoRecorder.stopRecording(function() {
6215 video: self.videoRecorder.getBlob()
6221 resultingObject.takeSnapshot = function(callback) {
6223 mediaElement: this.mediaElement,
6224 userid: this.userid,
6225 connection: connection,
6230 // redundant: kept only for backward compatibility
6231 resultingObject.streamObject = resultingObject;
6233 return resultingObject;
6236 // new RTCMultiConnection().set({properties}).connect()
6237 connection.set = function(properties) {
6238 for (var property in properties) {
6239 this[property] = properties[property];
6244 // www.RTCMultiConnection.org/docs/onMediaError/
6245 connection.onMediaError = function(event) {
6246 error('name', event.name);
6247 error('constraintName', toStr(event.constraintName));
6248 error('message', event.message);
6249 error('original session', event.session);
6252 // www.RTCMultiConnection.org/docs/takeSnapshot/
6253 connection.takeSnapshot = function(userid, callback) {
6256 connection: connection,
6261 connection.saveToDisk = function(blob, fileName) {
6262 if (blob.size && blob.type) FileSaver.SaveToDisk(URL.createObjectURL(blob), fileName || blob.name || blob.type.replace('/', '-') + blob.type.split('/')[1]);
6263 else FileSaver.SaveToDisk(blob, fileName);
6266 // www.RTCMultiConnection.org/docs/selectDevices/
6267 connection.selectDevices = function(device1, device2) {
6268 if (device1) select(this.devices[device1]);
6269 if (device2) select(this.devices[device2]);
6271 function select(device) {
6272 if (!device) return;
6273 connection._mediaSources[device.kind] = device.id;
6277 // www.RTCMultiConnection.org/docs/getDevices/
6278 connection.getDevices = function(callback) {
6279 // if, not yet fetched.
6280 if (!DetectRTC.MediaDevices.length) {
6281 return setTimeout(function() {
6282 connection.getDevices(callback);
6286 // loop over all audio/video input/output devices
6287 DetectRTC.MediaDevices.forEach(function(device) {
6288 connection.devices[device.deviceId] = device;
6291 if (callback) callback(connection.devices);
6294 connection.getMediaDevices = connection.enumerateDevices = function(callback) {
6295 if (!callback) throw 'callback is mandatory.';
6296 connection.getDevices(function() {
6297 callback(connection.DetectRTC.MediaDevices);
6301 // www.RTCMultiConnection.org/docs/onCustomMessage/
6302 connection.onCustomMessage = function(message) {
6303 log('Custom message', message);
6306 // www.RTCMultiConnection.org/docs/ondrop/
6307 connection.ondrop = function(droppedBy) {
6308 log('Media connection is dropped by ' + droppedBy);
6311 // www.RTCMultiConnection.org/docs/drop/
6312 connection.drop = function(config) {
6313 config = config || {};
6314 connection.attachStreams = [];
6316 // "drop" should detach all local streams
6317 for (var stream in connection.streams) {
6318 if (connection._skip.indexOf(stream) == -1) {
6319 stream = connection.streams[stream];
6320 if (stream.type == 'local') {
6321 connection.detachStreams.push(stream.streamid);
6322 onStreamEndedHandler(stream, connection);
6323 } else onStreamEndedHandler(stream, connection);
6327 // www.RTCMultiConnection.org/docs/sendCustomMessage/
6328 connection.sendCustomMessage({
6330 dontRenegotiate: isNull(config.renegotiate) ? true : config.renegotiate
6334 // www.RTCMultiConnection.org/docs/Translator/
6335 connection.Translator = {
6336 TranslateText: function(text, callback) {
6337 // if(location.protocol === 'https:') return callback(text);
6339 var newScript = document.createElement('script');
6340 newScript.type = 'text/javascript';
6342 var sourceText = encodeURIComponent(text); // escape
6344 var randomNumber = 'method' + connection.token();
6345 window[randomNumber] = function(response) {
6346 if (response.data && response.data.translations[0] && callback) {
6347 callback(response.data.translations[0].translatedText);
6350 if (response.error && response.error.message == 'Daily Limit Exceeded') {
6351 warn('Text translation failed. Error message: "Daily Limit Exceeded."');
6353 // returning original text
6358 var source = 'https://www.googleapis.com/language/translate/v2?key=' + connection.googKey + '&target=' + (connection.language || 'en-US') + '&callback=window.' + randomNumber + '&q=' + sourceText;
6359 newScript.src = source;
6360 document.getElementsByTagName('head')[0].appendChild(newScript);
6364 // you can easily override it by setting it NULL!
6365 connection.setDefaultEventsForMediaElement = function(mediaElement, streamid) {
6366 mediaElement.onpause = function() {
6367 if (connection.streams[streamid] && !connection.streams[streamid].muted) {
6368 connection.streams[streamid].mute();
6372 // todo: need to make sure that "onplay" EVENT doesn't play self-voice!
6373 mediaElement.onplay = function() {
6374 if (connection.streams[streamid] && connection.streams[streamid].muted) {
6375 connection.streams[streamid].unmute();
6379 var volumeChangeEventFired = false;
6380 mediaElement.onvolumechange = function() {
6381 if (!volumeChangeEventFired) {
6382 volumeChangeEventFired = true;
6383 connection.streams[streamid] && setTimeout(function() {
6384 var root = connection.streams[streamid];
6385 connection.streams[streamid].sockets.forEach(function(socket) {
6387 streamid: root.streamid,
6388 isVolumeChanged: true,
6389 volume: mediaElement.volume
6392 volumeChangeEventFired = false;
6398 // www.RTCMultiConnection.org/docs/onMediaFile/
6399 connection.onMediaFile = function(e) {
6400 log('onMediaFile', e);
6401 connection.body.appendChild(e.mediaElement);
6404 // www.RTCMultiConnection.org/docs/shareMediaFile/
6405 // this method handles pre-recorded media streaming
6406 connection.shareMediaFile = function(file, video, streamerid) {
6407 streamerid = streamerid || connection.token();
6409 if (!PreRecordedMediaStreamer) {
6410 loadScript(connection.resources.PreRecordedMediaStreamer, function() {
6411 connection.shareMediaFile(file, video, streamerid);
6416 return PreRecordedMediaStreamer.shareMediaFile({
6419 streamerid: streamerid,
6420 connection: connection
6424 // www.RTCMultiConnection.org/docs/onpartofscreen/
6425 connection.onpartofscreen = function(e) {
6426 var image = document.createElement('img');
6427 image.src = e.screenshot;
6428 connection.body.appendChild(image);
6431 connection.skipLogs = function() {
6432 log = error = warn = function() {};
6435 // www.RTCMultiConnection.org/docs/hold/
6436 connection.hold = function(mLine) {
6437 for (var peer in connection.peers) {
6438 connection.peers[peer].hold(mLine);
6442 // www.RTCMultiConnection.org/docs/onhold/
6443 connection.onhold = function(track) {
6444 log('onhold', track);
6446 if (track.kind != 'audio') {
6447 track.mediaElement.pause();
6448 track.mediaElement.setAttribute('poster', track.screenshot || connection.resources.muted);
6450 if (track.kind == 'audio') {
6451 track.mediaElement.muted = true;
6455 // www.RTCMultiConnection.org/docs/unhold/
6456 connection.unhold = function(mLine) {
6457 for (var peer in connection.peers) {
6458 connection.peers[peer].unhold(mLine);
6462 // www.RTCMultiConnection.org/docs/onunhold/
6463 connection.onunhold = function(track) {
6464 log('onunhold', track);
6466 if (track.kind != 'audio') {
6467 track.mediaElement.play();
6468 track.mediaElement.removeAttribute('poster');
6470 if (track.kind != 'audio') {
6471 track.mediaElement.muted = false;
6475 connection.sharePartOfScreen = function(args) {
6476 var lastScreenshot = '';
6478 function partOfScreenCapturer() {
6480 if (connection.partOfScreen && !connection.partOfScreen.sharing) {
6484 capturePartOfScreen({
6485 element: args.element,
6486 connection: connection,
6487 callback: function(screenshot) {
6488 // don't share repeated content
6489 if (screenshot != lastScreenshot) {
6490 lastScreenshot = screenshot;
6492 for (var channel in connection.channels) {
6493 connection.channels[channel].send({
6494 screenshot: screenshot,
6495 isPartOfScreen: true
6500 // "once" can be used to share single screenshot
6501 !args.once && setTimeout(partOfScreenCapturer, args.interval || 200);
6506 partOfScreenCapturer();
6508 connection.partOfScreen = merge({
6513 connection.pausePartOfScreenSharing = function() {
6514 for (var peer in connection.peers) {
6515 connection.peers[peer].pausePartOfScreenSharing = true;
6518 if (connection.partOfScreen) {
6519 connection.partOfScreen.sharing = false;
6523 connection.resumePartOfScreenSharing = function() {
6524 for (var peer in connection.peers) {
6525 connection.peers[peer].pausePartOfScreenSharing = false;
6528 if (connection.partOfScreen) {
6529 connection.partOfScreen.sharing = true;
6533 connection.stopPartOfScreenSharing = function() {
6534 for (var peer in connection.peers) {
6535 connection.peers[peer].stopPartOfScreenSharing = true;
6538 if (connection.partOfScreen) {
6539 connection.partOfScreen.sharing = false;
6543 connection.takeScreenshot = function(element, callback) {
6544 if (!element || !callback) throw 'Invalid number of arguments.';
6546 if (!window.html2canvas) {
6547 return loadScript(connection.resources.html2canvas, function() {
6548 connection.takeScreenshot(element);
6552 if (isString(element)) {
6553 element = document.querySelector(element);
6554 if (!element) element = document.getElementById(element);
6556 if (!element) throw 'HTML Element is inaccessible!';
6558 // html2canvas.js is used to take screenshots
6559 html2canvas(element, {
6560 onrendered: function(canvas) {
6561 callback(canvas.toDataURL());
6566 // this event is fired when RTCMultiConnection detects that chrome extension
6567 // for screen capturing is installed and available
6568 connection.onScreenCapturingExtensionAvailable = function() {
6569 log('It seems that screen capturing extension is installed and available on your system!');
6572 if (!isPluginRTC && DetectRTC.screen.onScreenCapturingExtensionAvailable) {
6573 DetectRTC.screen.onScreenCapturingExtensionAvailable = function() {
6574 connection.onScreenCapturingExtensionAvailable();
6578 connection.changeBandwidth = function(bandwidth) {
6579 for (var peer in connection.peers) {
6580 connection.peers[peer].changeBandwidth(bandwidth);
6584 connection.convertToAudioStream = function(mediaStream) {
6585 convertToAudioStream(mediaStream);
6588 connection.onstatechange = function(state) {
6589 log('on:state:change (' + state.userid + '):', state.name + ':', state.reason || '');
6592 connection.onfailed = function(event) {
6593 if (!event.peer.numOfRetries) event.peer.numOfRetries = 0;
6594 event.peer.numOfRetries++;
6596 error('ICE connectivity check is failed. Renegotiating peer connection.');
6597 event.peer.numOfRetries < 2 && event.peer.renegotiate();
6599 if (event.peer.numOfRetries >= 2) event.peer.numOfRetries = 0;
6602 connection.onconnected = function(event) {
6603 // event.peer.addStream || event.peer.getConnectionStats
6604 log('Peer connection has been established between you and', event.userid);
6607 connection.ondisconnected = function(event) {
6608 error('Peer connection seems has been disconnected between you and', event.userid);
6610 if (isEmpty(connection.channels)) return;
6611 if (!connection.channels[event.userid]) return;
6613 // use WebRTC data channels to detect user's presence
6614 connection.channels[event.userid].send({
6615 checkingPresence: true
6618 // wait 5 seconds, if target peer didn't response, simply disconnect
6619 setTimeout(function() {
6620 // iceConnectionState == 'disconnected' occurred out of low-bandwidth
6621 // or internet connectivity issues
6622 if (connection.peers[event.userid].connected) {
6623 delete connection.peers[event.userid].connected;
6627 // to make sure this user's all remote streams are removed.
6628 connection.streams.remove({
6630 userid: event.userid
6633 connection.remove(event.userid);
6637 connection.onstreamid = function(event) {
6638 // event.isScreen || event.isVideo || event.isAudio
6639 log('got remote streamid', event.streamid, 'from', event.userid);
6642 connection.stopMediaStream = function(mediaStream) {
6643 if (!mediaStream) throw 'MediaStream argument is mandatory.';
6645 if (connection.keepStreamsOpened) {
6646 if (mediaStream.onended) mediaStream.onended();
6650 // remove stream from "localStreams" object
6651 // when native-stop method invoked.
6652 if (connection.localStreams[mediaStream.streamid]) {
6653 delete connection.localStreams[mediaStream.streamid];
6657 // Firefox don't yet support onended for any stream (remote/local)
6658 if (mediaStream.onended) mediaStream.onended();
6661 // Latest firefox does support mediaStream.getAudioTrack but doesn't support stop on MediaStreamTrack
6662 var checkForMediaStreamTrackStop = Boolean(
6663 (mediaStream.getAudioTracks || mediaStream.getVideoTracks) && (
6664 (mediaStream.getAudioTracks()[0] && !mediaStream.getAudioTracks()[0].stop) ||
6665 (mediaStream.getVideoTracks()[0] && !mediaStream.getVideoTracks()[0].stop)
6669 if (!mediaStream.getAudioTracks || checkForMediaStreamTrackStop) {
6670 if (mediaStream.stop) {
6676 if (mediaStream.getAudioTracks().length && mediaStream.getAudioTracks()[0].stop) {
6677 mediaStream.getAudioTracks().forEach(function(track) {
6682 if (mediaStream.getVideoTracks().length && mediaStream.getVideoTracks()[0].stop) {
6683 mediaStream.getVideoTracks().forEach(function(track) {
6689 connection.changeBandwidth = function(bandwidth) {
6690 if (!bandwidth || isString(bandwidth) || isEmpty(bandwidth)) {
6691 throw 'Invalid "bandwidth" arguments.';
6694 forEach(connection.peers, function(peer) {
6695 peer.peer.bandwidth = bandwidth;
6698 connection.renegotiate();
6701 // www.RTCMultiConnection.org/docs/openSignalingChannel/
6702 // http://goo.gl/uvoIcZ
6705 var href = window.location.href;
6706 var hostPatt = new RegExp(window.location.host +"/[^/]*");
6707 var res = hostPatt.exec(href);
6708 var protocol = window.location.protocol.replace("http","ws");
6710 var signalingServerPath = protocol + "//" + res + "/webrtc";
6711 var SIGNALING_SERVER = signalingServerPath; //"ws://localhost:80/quantum/webrtc"; //"wss://localhost:80/quantum/webrtc"; --> ws for http and wss for https
6713 connection.openSignalingChannel = function(config) {
6716 config.channel = config.channel || this.channel;
6717 var websocket = new WebSocket(SIGNALING_SERVER);
6718 websocket.channel = config.channel;
6719 websocket.onopen = function() {
6720 websocket.push(JSON.stringify({
6722 channel: config.channel
6724 if (config.callback)
6725 config.callback(websocket);
6727 websocket.onmessage = function(event) {
6728 config.onmessage(JSON.parse(event.data));
6730 websocket.push = websocket.send;
6731 websocket.send = function(data) {
6732 websocket.push(JSON.stringify({
6734 channel: config.channel
6739 // make sure firebase.js is loaded
6740 if (!window.Firebase) {
6741 return loadScript(connection.resources.firebase, function() {
6742 connection.openSignalingChannel(config);
6746 var channel = config.channel || connection.channel;
6748 if (connection.firebase) {
6749 // for custom firebase instances
6750 connection.resources.firebaseio = connection.resources.firebaseio.replace('//chat.', '//' + connection.firebase + '.');
6753 var firebase = new Firebase(connection.resources.firebaseio + channel);
6754 firebase.channel = channel;
6755 firebase.on('child_added', function(data) {
6756 config.onmessage(data.val());
6759 firebase.send = function(data) {
6760 // a quick dirty workaround to make sure firebase
6761 // shouldn't fail for NULL values.
6762 for (var prop in data) {
6763 if (isNull(data[prop]) || typeof data[prop] == 'function') {
6771 if (!connection.socket)
6772 connection.socket = firebase;
6774 firebase.onDisconnect().remove();
6776 setTimeout(function() {
6777 config.callback(firebase);
6785 connection.Plugin = Plugin;