UNPKG

open-easyrtc

Version:

Open-EasyRTC enables quick development of WebRTC

1,299 lines (1,175 loc) 229 kB
/* global define, module, require, console, MediaStreamTrack, createIceServer, RTCIceCandidate, RTCPeerConnection, RTCSessionDescription */ /*! Script: easyrtc.js Provides client side support for the EasyRTC framework. See the easyrtc_client_api.md and easyrtc_client_tutorial.md for more details. About: License Copyright (c) 2016, Priologic Software Inc. All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ (function (root, factory) { if (typeof define === 'function' && define.amd) { //RequireJS (AMD) build system define(['easyrtc_lang', 'socket.io', 'webrtc-adapter'], factory); } else if (typeof module === 'object' && module.exports) { //CommonJS build system module.exports = factory(require('easyrtc_lang'), require('socket.io'), require('webrtc-adapter')); } else { //Vanilla JS, ensure dependencies are loaded correctly if (typeof window.io === 'undefined' || !window.io) { throw new Error("easyrtc requires socket.io"); } root.easyrtc = factory(window.easyrtc_lang, window.io, window.adapter); } }(this, function (easyrtc_lang, io, adapter, undefined) { "use strict"; /** * @class Easyrtc. * * @returns {Easyrtc} the new easyrtc instance. * * @constructs Easyrtc */ var Easyrtc = function() { var self = this; function logDebug (message, obj) { if (self.debugPrinter) { self.debugPrinter(message, obj); } } function isEmptyObj(obj) { if (obj === null || obj === undefined) { return true; } var key; for (key in obj) { if (obj.hasOwnProperty(key)) { return false; } } return true; } /** @private */ // // // peerConns is a map from caller names to the below object structure // { startedAV: boolean, -- true if we have traded audio/video streams // dataChannelS: RTPDataChannel for outgoing messages if present // dataChannelR: RTPDataChannel for incoming messages if present // dataChannelReady: true if the data channel can be used for sending yet // connectTime: timestamp when the connection was started // sharingAudio: true if audio is being shared // sharingVideo: true if video is being shared // cancelled: temporarily true if a connection was cancelled by the peer asking to initiate it // candidatesToSend: SDP candidates temporarily queued // streamsAddedAcks: ack callbacks waiting for stream received messages // pc: RTCPeerConnection // mediaStream: mediaStream // function callSuccessCB(string) - see the easyrtc.call documentation. // function callFailureCB(errorCode, string) - see the easyrtc.call documentation. // function wasAcceptedCB(boolean,string) - see the easyrtc.call documentation. // } // var peerConns = {}; /** @private */ var autoInitUserMedia = true; /** @private */ var sdpLocalFilter = null; /** @private */ var sdpRemoteFilter = null; /** @private */ var iceCandidateFilter = null; /** @private */ var iceConnectionStateChangeListener = null; var signalingStateChangeListener = null; /** @private */ var connectionOptions = { // whether or not cross-site requests should made using credentials such as cookies, authorization headers. "withCredentials": false, // whether to reconnect automatically "reconnection": false, // connection timeout before a connect_error and connect_timeout events are emitted 'timeout': 25000, // whether to reuse an existing connection 'forceNew': true }; /** @private */ // // this function replaces the deprecated MediaStream.stop method // function stopStream(stream) { var i; var tracks; tracks = stream.getAudioTracks(); for( i = 0; i < tracks.length; i++ ) { try { tracks[i].stop(); } catch(err){} } tracks = stream.getVideoTracks(); for( i = 0; i < tracks.length; i++ ) { try { tracks[i].stop(); } catch(err){} } if (typeof stream.stop === 'function') { try { stream.stop(); } catch(err){} } } /** * Sets functions which filter sdp records before calling setLocalDescription or setRemoteDescription. * This is advanced functionality which can break things, easily. See the easyrtc_rates.js file for a * filter builder. * @param {Function} localFilter a function that takes an sdp string and returns an sdp string. * @param {Function} remoteFilter a function that takes an sdp string and returns an sdp string. */ this.setSdpFilters = function(localFilter, remoteFilter) { sdpLocalFilter = localFilter; sdpRemoteFilter = remoteFilter; }; /** * Sets a function to warn about the peer connection closing. * @param {Function} handler: a function that gets an easyrtcid as an argument. */ this.setPeerClosedListener = function( handler ) { this.onPeerClosed = handler; }; /** * Sets a function to warn about the peer connection open. * @param {Function} handler: a function that gets an easyrtcid as an argument. */ this.setPeerOpenListener = function( handler ) { this.onPeerOpen = handler; }; /** * Sets a function to receive warnings about the peer connection * failing. The peer connection may recover by itself. * @param {Function} failingHandler: a function that gets an easyrtcid as an argument. * @param {Function} recoveredHandler: a function that gets an easyrtcid as an argument. */ this.setPeerFailingListener = function( failingHandler, recoveredHandler ) { this.onPeerFailing = failingHandler; this.onPeerRecovered = recoveredHandler; }; /** * Sets a function which filters IceCandidate records being sent or received. * * Candidate records can be received while they are being generated locally (before being * sent to a peer), and after they are received by the peer. The filter receives two arguments, the candidate record and a boolean * flag that is true for a candidate being received from another peer, * and false for a candidate that was generated locally. The candidate record has the form: * {type: 'candidate', label: sdpMLineIndex, id: sdpMid, candidate: candidateString} * The function should return one of the following: the input candidate record, a modified candidate record, or null (indicating that the * candidate should be discarded). * @param {Function} filter */ this.setIceCandidateFilter = function(filter) { iceCandidateFilter = filter; }; /** * Sets a function that listens on IceConnectionStateChange events. * * During ICE negotiation the peer connection fires the iceconnectionstatechange event. * It is sometimes useful for the application to learn about these changes, especially if the ICE connection fails. * The function should accept three parameters: the easyrtc id of the peer, the iceconnectionstatechange event target and the iceconnectionstate. * @param {Function} listener */ this.setIceConnectionStateChangeListener = function(listener) { iceConnectionStateChangeListener = listener; }; /** * Sets a function that listens on SignalingStateChange events. * * During ICE negotiation the peer connection fires the signalingstatechange event. * The function should accept three parameters: the easyrtc id of the peer, the signalingstatechange event target and the signalingstate. * @param {Function} listener */ this.setSignalingStateChangeListener = function(listener) { signalingStateChangeListener = listener; }; /** * Controls whether a default local media stream should be acquired automatically during calls and accepts * if a list of streamNames is not supplied. The default is true, which mimics the behaviour of earlier releases * that didn't support multiple streams. This function should be called before easyrtc.call or before entering an * accept callback. * @param {Boolean} flag true to allocate a default local media stream. */ this.setAutoInitUserMedia = function(flag) { autoInitUserMedia = !!flag; }; /** * This function performs a printf like formatting. It actually takes an unlimited * number of arguments, the declared arguments arg1, arg2, arg3 are present just for * documentation purposes. * @param {String} format A string like "abcd{1}efg{2}hij{1}." * @param {String} arg1 The value that replaces {1} * @param {String} arg2 The value that replaces {2} * @param {String} arg3 The value that replaces {3} * @returns {String} the formatted string. */ this.format = function(format, arg1, arg2, arg3) { var formatted = arguments[0]; for (var i = 1; i < arguments.length; i++) { var regexp = new RegExp('\\{' + (i - 1) + '\\}', 'gi'); formatted = formatted.replace(regexp, arguments[i]); } return formatted; }; // // this private method handles adding a stream to a peer connection. // chrome only supports adding streams, safari only supports adding // tracks. // function addStreamToPeerConnection(stream, peerConnection) { if (peerConnection.addStream) { var existingStreams = peerConnection.getLocalStreams(); if (existingStreams.indexOf(stream) === -1) { peerConnection.addStream(stream); } } else { var existingTracks = peerConnection.getSenders().map(function (sender) { return sender.track; }); var i; var tracks = stream.getAudioTracks(); for (i = 0; i < tracks.length; i++ ) { if (existingTracks.indexOf(tracks[i]) === -1) { peerConnection.addTrack(tracks[i], stream); } } tracks = stream.getVideoTracks(); for (i = 0; i < tracks.length; i++ ) { if (existingTracks.indexOf(tracks[i]) === -1) { peerConnection.addTrack(tracks[i], stream); } } } } /** This method is used to trigger renegotiation, which is how you * you update change the properties of an existing connection (such as the * the bandwidth used. Before calling it, you modify your sdp filters * to reflect the desired changes. * @param otherUser the easyrtcid of the peer corresponding to the * connection being updated. */ this.renegotiate = function(otherUser, iceRestart) { var peerConnObj = peerConns[otherUser]; if(!peerConnObj) { logDebug("Attempt to renegotiate ice on nonexistant connection"); return; } var callFailureCB = peerConnObj.callFailureCB || self.showError; var pc = peerConnObj.pc; var setLocalAndSendMessage0 = function(sessionDescription) { if (peerConnObj.cancelled) { logDebug('renegotiate.setLocalAndSendMessage0.ignored', peerConnObj.cancelled, peerConnObj.sendingOffer); return; } var sendOffer = function() { peerConnObj.sendingOffer = false; sendSignalling(otherUser, "offer", sessionDescription, null, callFailureCB); }; if (sdpLocalFilter) { sessionDescription.sdp = sdpLocalFilter(sessionDescription.sdp); } pc.setLocalDescription(sessionDescription).then(sendOffer, function(errorText) { peerConnObj.sendingOffer = false; callFailureCB(self.errCodes.CALL_ERR, errorText); }); }; if (peerConnObj.sendingOffer) { logDebug('initiateSendOffer.setLocalAndSendMessage0.ignored', peerConnObj.sendingOffer); return; } if (typeof iceRestart === 'undefined') { iceRestart = pc.iceConnectionState !== 'connected'; } logDebug('iceRestart:' + iceRestart); peerConnObj.sendingOffer = true; pc.createOffer({iceRestart: iceRestart }).then(setLocalAndSendMessage0) .catch(function(reason) { peerConnObj.sendingOffer = false; callFailureCB(self.errCodes.CALL_ERR, JSON.stringify(reason)); }); }; /** * This function checks if a socket is actually connected. * @private * @param {Object} socket a socket.io socket. * @return true if the socket exists and is connected, false otherwise. */ function isSocketConnected(socket) { return socket && ( (socket.socket && socket.socket.connected) || socket.connected ); } /** @private */ // // Maps a key to a language specific string using the easyrtc_lang map. // Defaults to the key if the key can not be found, but outputs a warning in that case. // This function is only used internally by easyrtc.js // var haveAudioVideo = { audio: false, video: false }; /** * @private * @param {String} key */ this.getConstantString = function(key) { if (easyrtc_lang[key]) { return easyrtc_lang[key]; } else { self.showError(self.errCodes.DEVELOPER_ERR, "Could not find key='" + key + "' in easyrtc_lang"); return key; } }; /** @private */ // // this is a list of the events supported by the generalized event listener. // var allowedEvents = { roomOccupant: true, // this receives the list of everybody in any room you belong to roomOccupants: true // this receives a {roomName:..., occupants:...} value for a specific room }; /** @private */ // // A map of eventListeners. The key is the event type. // var eventListeners = {}; /** * This function checks if an attempt was made to add an event listener or * or emit an unlisted event, since such is typically a typo. * @private * @param {String} eventName * @param {String} callingFunction the name of the calling function. */ function event(eventName, callingFunction) { if (typeof eventName !== 'string') { self.showError(self.errCodes.DEVELOPER_ERR, callingFunction + " called without a string as the first argument"); throw "developer error"; } if (!allowedEvents[eventName]) { self.showError(self.errCodes.DEVELOPER_ERR, callingFunction + " called with a bad event name = " + eventName); throw "developer error"; } } /** * Adds an event listener for a particular type of event. * Currently the only eventName supported is "roomOccupant". * @param {String} eventName the type of the event * @param {Function} eventListener the function that expects the event. * The eventListener gets called with the eventName as it's first argument, and the event * data as it's second argument. * @returns {void} */ this.addEventListener = function(eventName, eventListener) { event(eventName, "addEventListener"); if (typeof eventListener !== 'function') { self.showError(self.errCodes.DEVELOPER_ERR, "addEventListener called with a non-function for second argument"); throw "developer error"; } // // remove the event listener if it's already present so we don't end up with two copies // self.removeEventListener(eventName, eventListener); if (!eventListeners[eventName]) { eventListeners[eventName] = []; } eventListeners[eventName][eventListeners[eventName].length] = eventListener; }; /** * Removes an event listener. * @param {String} eventName * @param {Function} eventListener */ this.removeEventListener = function(eventName, eventListener) { event(eventName, "removeEventListener"); var listeners = eventListeners[eventName]; var i = 0; if (listeners) { for (i = 0; i < listeners.length; i++) { if (listeners[i] === eventListener) { if (i < listeners.length - 1) { listeners[i] = listeners[listeners.length - 1]; } listeners.length = listeners.length - 1; } } } }; /** * Emits an event, or in other words, calls all the eventListeners for a * particular event. * @param {String} eventName * @param {Object} eventData */ this.emitEvent = function(eventName, eventData) { event(eventName, "emitEvent"); var listeners = eventListeners[eventName]; var i = 0; if (listeners) { for (i = 0; i < listeners.length; i++) { listeners[i](eventName, eventData); } } }; /** * Error codes that the EasyRTC will use in the errorCode field of error object passed * to error handler set by easyrtc.setOnError. The error codes are short printable strings. * @type Object */ this.errCodes = { BAD_NAME: "BAD_NAME", // a user name wasn't of the desired form CALL_ERR: "CALL_ERR", // something went wrong creating the peer connection DEVELOPER_ERR: "DEVELOPER_ERR", // the developer using the EasyRTC library made a mistake SYSTEM_ERR: "SYSTEM_ERR", // probably an error related to the network CONNECT_ERR: "CONNECT_ERR", // error occurred when trying to create a connection MEDIA_ERR: "MEDIA_ERR", // unable to get the local media MEDIA_WARNING: "MEDIA_WARNING", // didn't get the desired resolution INTERNAL_ERR: "INTERNAL_ERR", PEER_GONE: "PEER_GONE", // peer doesn't exist ALREADY_CONNECTED: "ALREADY_CONNECTED", BAD_CREDENTIAL: "BAD_CREDENTIAL", ICECANDIDATE_ERR: "ICECANDIDATE_ERR", NOVIABLEICE: "NOVIABLEICE", SIGNAL_ERR: "SIGNAL_ERR" }; this.apiVersion = "1.1.1-beta"; /** Most basic message acknowledgment object */ this.ackMessage = {msgType: "ack"}; /** Regular expression pattern for user ids. This will need modification to support non US character sets */ this.usernameRegExp = /^(.){1,64}$/; /** Default cookieId name */ this.cookieId = "easyrtcsid"; /** @private */ this.username = null; /** Flag to indicate that user is currently logging out */ this.loggingOut = false; /** @private */ this.disconnecting = false; /** @private */ // // A map of ids to local media streams. // var namedLocalMediaStreams = {}; /** @private */ var sessionFields = []; /** @private */ var receivedMediaConstraints = {}; /** * Control whether the client requests audio from a peer during a call. * Must be called before the call to have an effect. * @param value - true to receive audio, false otherwise. The default is true. */ this.enableAudioReceive = function(value) { if ( adapter && adapter.browserDetails && (adapter.browserDetails.browser === "chrome" && adapter.browserDetails.version < 65) ) { receivedMediaConstraints.mandatory = receivedMediaConstraints.mandatory || {}; receivedMediaConstraints.mandatory.OfferToReceiveAudio = value; } else { receivedMediaConstraints.offerToReceiveAudio = value; } }; /** * Control whether the client requests video from a peer during a call. * Must be called before the call to have an effect. * @param value - true to receive video, false otherwise. The default is true. */ this.enableVideoReceive = function(value) { if ( adapter && adapter.browserDetails && (adapter.browserDetails.browser === "chrome" && adapter.browserDetails.version < 65) ) { receivedMediaConstraints.mandatory = receivedMediaConstraints.mandatory || {}; receivedMediaConstraints.mandatory.OfferToReceiveVideo = value; } else { receivedMediaConstraints.offerToReceiveVideo = value; } }; // True by default // TODO should not be true by default only for legacy this.enableAudioReceive(true); this.enableVideoReceive(true); function getSourceList(callback, sourceType) { navigator.mediaDevices.enumerateDevices().then( function(values) { var results = []; for (var i = 0; i < values.length; i++) { var source = values[i]; if (source.kind === sourceType) { //backwards compatibility if (!source.id) { source.id = source.deviceId; } results.push(source); } } callback(results); } ).catch( function(reason) { logDebug("Unable to enumerate devices (" + reason + ")"); } ); } /** * Sets the audio output device of a Video object. * That is to say, this controls what speakers get the sound. * In theory, this works on Chrome but probably doesn't work anywhere else yet. * This code was cribbed from https://webrtc.github.io/samples/src/content/devices/multi/. * @param {Object} element an HTML5 video element * @param {String} sinkId a deviceid from getAudioSinkList */ this.setAudioOutput = function(element, sinkId) { if (typeof element.sinkId !== 'undefined') { element.setSinkId(sinkId) .then(function() { logDebug('Success, audio output device attached: ' + sinkId + ' to ' + 'element with ' + element.title + ' as source.'); }) .catch(function(error) { var errorMessage = error; if (error.name === 'SecurityError') { errorMessage = 'You need to use HTTPS for selecting audio output ' + 'device: ' + error; } logDebug(errorMessage); }); } else { logDebug('Browser does not support output device selection.'); } }; /** * Gets a list of the available audio sinks (ie, speakers) * @param {Function} callback receives list of {deviceId:String, groupId:String, label:String, kind:"audio"} * @example easyrtc.getAudioSinkList( function(list) { * var i; * for( i = 0; i < list.length; i++ ) { * console.log("label=" + list[i].label + ", id= " + list[i].deviceId); * } * }); */ this.getAudioSinkList = function(callback){ getSourceList(callback, "audiooutput"); }; /** * Gets a list of the available audio sources (ie, microphones) * @param {Function} callback receives list of {deviceId:String, groupId:String, label:String, kind:"audio"} * @example easyrtc.getAudioSourceList( function(list) { * var i; * for( i = 0; i < list.length; i++ ) { * console.log("label=" + list[i].label + ", id= " + list[i].deviceId); * } * }); */ this.getAudioSourceList = function(callback){ getSourceList(callback, "audioinput"); }; /** * Gets a list of the available video sources (ie, cameras) * @param {Function} callback receives list of {deviceId:String, groupId:String, label:String, kind:"video"} * @example easyrtc.getVideoSourceList( function(list) { * var i; * for( i = 0; i < list.length; i++ ) { * console.log("label=" + list[i].label + ", id= " + list[i].deviceId); * } * }); */ this.getVideoSourceList = function(callback) { getSourceList(callback, "videoinput"); }; /** @private */ var dataChannelName = "dc"; /** @private */ var oldConfig = {}; /** @private */ var offersPending = {}; /** @private */ var credential = null; /** @private */ self.audioEnabled = true; /** @private */ self.videoEnabled = true; /** @private */ this.debugPrinter = null; /** Your easyrtcid */ this.myEasyrtcid = ""; /** The height of the local media stream video in pixels. This field is set an indeterminate period * of time after easyrtc.initMediaSource succeeds. Note: in actuality, the dimensions of a video stream * change dynamically in response to external factors, you should check the videoWidth and videoHeight attributes * of your video objects before you use them for pixel specific operations. */ this.nativeVideoHeight = 0; /** This constant determines how long (in bytes) a message can be before being split in chunks of that size. * This is because there is a limitation of the length of the message you can send on the * data channel between browsers. */ this.maxP2PMessageLength = 1000; /** The width of the local media stream video in pixels. This field is set an indeterminate period * of time after easyrtc.initMediaSource succeeds. Note: in actuality, the dimensions of a video stream * change dynamically in response to external factors, you should check the videoWidth and videoHeight attributes * of your video objects before you use them for pixel specific operations. */ this.nativeVideoWidth = 0; /** The rooms the user is in. This only applies to room oriented applications and is set at the same * time a token is received. */ this.roomJoin = {}; /** Checks if the supplied string is a valid user name (standard identifier rules) * @param {String} name * @return {Boolean} true for a valid user name * @example * var name = document.getElementById('nameField').value; * if( !easyrtc.isNameValid(name)){ * console.error("Bad user name"); * } */ this.isNameValid = function(name) { return self.usernameRegExp.test(name); }; /** * This function sets the name of the cookie that client side library will look for * and transmit back to the server as it's easyrtcsid in the first message. * @param {String} cookieId */ this.setCookieId = function(cookieId) { self.cookieId = cookieId; }; /** @private */ this._desiredVideoProperties = {}; // default camera /** * Specify particular video source. Call this before you call easyrtc.initMediaSource(). * @param {String} videoSrcId is a id value from one of the entries fetched by getVideoSourceList. null for default. * @example easyrtc.setVideoSource( videoSrcId); */ this.setVideoSource = function(videoSrcId) { self._desiredVideoProperties.videoSrcId = videoSrcId; }; /** This function requests that screen capturing be used to provide the local media source * rather than a webcam. If you have multiple screens, they are composited side by side. * Note: this functionality is not supported by Firefox, has to be called before calling initMediaSource (or easyApp), we don't currently supply a way to * turn it off (once it's on), only works if the website is hosted SSL (https), and the image quality is rather * poor going across a network because it tries to transmit so much data. In short, screen sharing * through WebRTC isn't worth using at this point, but it is provided here so people can try it out. * @param {Boolean} enableScreenCapture * @param {String} mediaSourceId (optional) * @example * easyrtc.setScreenCapture(true); */ this.setScreenCapture = function(enableScreenCapture, mediaSourceId) { if (enableScreenCapture) { // Set video self._presetMediaConstraints = { video:{ mozMediaSource: "screen", chromeMediaSource: 'screen', mediaSource: "screen", mediaSourceId: 'screen:0', maxWidth: screen.width, maxHeight: screen.height, minWidth: screen.width, minHeight: screen.height, minFrameRate: 1, maxFrameRate: 15 }, // In Chrome, the chrome.desktopCapture extension API can be used to capture the screen, // which includes system audio (but only on Windows and Chrome OS and without plans for OS X or Linux). // - http://stackoverflow.com/questions/34235077/capture-system-sound-from-browser?answertab=votes#tab-top audio: false /* { optional: { chromeMediaSource: 'system', chromeMediaSourceId: that.chromeMediaSourceId } } */ }; if (mediaSourceId) { if ( adapter && adapter.browserDetails && (adapter.browserDetails.browser === "chrome") ) { var mandatory = self._presetMediaConstraints.video; mandatory.chromeMediaSource = 'desktop'; mandatory.chromeMediaSourceId = mediaSourceId; self._presetMediaConstraints.video = { mandatory: mandatory, // http://www.acis.ufl.edu/~ptony82/jingle-html/classwebrtc_1_1MediaConstraintsInterface.html optional: [{ googTemporalLayeredScreencast: true }] }; } else { self._presetMediaConstraints.video.mediaSourceId = mediaSourceId; } } // Clear } else { delete self._presetMediaConstraints; } }; /** @private */ this._desiredAudioProperties = {}; // default camera /** * Specify particular video source. Call this before you call easyrtc.initMediaSource(). * @param {String} audioSrcId is a id value from one of the entries fetched by getAudioSourceList. null for default. * @example easyrtc.setAudioSource( audioSrcId); */ this.setAudioSource = function(audioSrcId) { self._desiredAudioProperties.audioSrcId = audioSrcId; }; /** This function is used to set the dimensions of the local camera, usually to get HD. * If called, it must be called before calling easyrtc.initMediaSource (explicitly or implicitly). * assuming it is supported. If you don't pass any parameters, it will use default camera dimensions. * @param {Number} width in pixels * @param {Number} height in pixels * @param {number} frameRate is optional * @example * easyrtc.setVideoDims(1280,720); * @example * easyrtc.setVideoDims(); */ this.setVideoDims = function(width, height, frameRate) { self._desiredVideoProperties.width = width; self._desiredVideoProperties.height = height; if (frameRate !== undefined) { self._desiredVideoProperties.frameRate = frameRate; } }; /** * Builds the constraint object passed to getUserMedia. * @returns {Object} mediaConstraints */ self.getUserMediaConstraints = function() { var constraints = {}; // // _presetMediaConstraints allow you to provide your own constraints to be used // with initMediaSource. // if (self._presetMediaConstraints) { constraints = self._presetMediaConstraints; delete self._presetMediaConstraints; return constraints; } else if (!self.videoEnabled) { constraints.video = false; } else { // Tested Firefox 49 and MS Edge require minFrameRate and maxFrameRate // instead max,min,ideal that cause GetUserMedia failure. // Until confirmed both browser support idea,max and min we need this. if ( adapter && adapter.browserDetails && (adapter.browserDetails.browser === "firefox" || adapter.browserDetails.browser === "edge") ) { constraints.video = {}; if (self._desiredVideoProperties.width) { constraints.video.width = self._desiredVideoProperties.width; } if (self._desiredVideoProperties.height) { constraints.video.height = self._desiredVideoProperties.height; } if (self._desiredVideoProperties.frameRate) { constraints.video.frameRate = { minFrameRate: self._desiredVideoProperties.frameRate, maxFrameRate: self._desiredVideoProperties.frameRate }; } if (self._desiredVideoProperties.videoSrcId) { constraints.video.deviceId = self._desiredVideoProperties.videoSrcId; } // chrome and opera } else { constraints.video = {}; if (self._desiredVideoProperties.width) { constraints.video.width = { max: self._desiredVideoProperties.width, min : self._desiredVideoProperties.width, ideal : self._desiredVideoProperties.width }; } if (self._desiredVideoProperties.height) { constraints.video.height = { max: self._desiredVideoProperties.height, min: self._desiredVideoProperties.height, ideal: self._desiredVideoProperties.height }; } if (self._desiredVideoProperties.frameRate) { constraints.video.frameRate = { max: self._desiredVideoProperties.frameRate, ideal: self._desiredVideoProperties.frameRate }; } if (self._desiredVideoProperties.videoSrcId) { constraints.video.deviceId = self._desiredVideoProperties.videoSrcId; } // hack for opera if (Object.keys(constraints.video).length === 0 ) { constraints.video = true; } } } if (!self.audioEnabled) { constraints.audio = false; } else { constraints.audio = {}; if (self._desiredAudioProperties.audioSrcId) { constraints.audio.deviceId = self._desiredAudioProperties.audioSrcId; } } return constraints; }; /** Set the application name. Applications can only communicate with other applications * that share the same API Key and application name. There is no predefined set of application * names. Maximum length is * @param {String} name * @example * easyrtc.setApplicationName('simpleAudioVideo'); */ this.setApplicationName = function(name) { self.applicationName = name; }; /** Enable or disable logging to the console. * Note: if you want to control the printing of debug messages, override the * easyrtc.debugPrinter variable with a function that takes a message string as it's argument. * This is exactly what easyrtc.enableDebug does when it's enable argument is true. * @param {Boolean} enable - true to turn on debugging, false to turn off debugging. Default is false. * @example * easyrtc.enableDebug(true); */ this.enableDebug = function(enable) { if (enable) { self.debugPrinter = function(message, obj) { var now = new Date().toISOString(); var stackString = new Error().stack; var srcLine = "location unknown"; if (stackString) { var stackFrameStrings = stackString.split('\n'); srcLine = ""; if (stackFrameStrings.length >= 5) { srcLine = stackFrameStrings[4]; } } console.log("debug " + now + " : " + message + " [" + srcLine + "]"); if (typeof obj !== 'undefined') { console.log("debug " + now + " : ", obj); } }; } else { self.debugPrinter = null; } }; /** * Determines if the local browser supports WebRTC GetUserMedia (access to camera and microphone). * @returns {Boolean} True getUserMedia is supported. */ this.supportsGetUserMedia = function() { return navigator.mediaDevices && typeof navigator.mediaDevices.getUserMedia !== 'undefined'; }; /** * Determines if the local browser supports WebRTC Peer connections to the extent of being able to do video chats. * @returns {Boolean} True if Peer connections are supported. */ this.supportsPeerConnections = function() { return typeof RTCPeerConnection !== 'undefined'; }; /** Determines whether the current browser supports the new data channels. * EasyRTC will not open up connections with the old data channels. * @returns {Boolean} */ this.supportsDataChannels = function() { var hasCreateDataChannel = false; if (self.supportsPeerConnections()) { try { var peer = new RTCPeerConnection({iceServers: []}, {}); hasCreateDataChannel = typeof peer.createDataChannel !== 'undefined'; peer.close(); } catch (err) { // Ignore possible RTCPeerConnection.close error // hasCreateDataChannel should reflect the feature state still. } } return hasCreateDataChannel; }; /** @private */ // // Experimental function to determine if statistics gathering is supported. // this.supportsStatistics = function() { var hasGetStats = false; if (self.supportsPeerConnections()) { try { var peer = new RTCPeerConnection({iceServers: []}, {}); hasGetStats = typeof peer.getStats !== 'undefined'; peer.close(); } catch (err) { // Ignore possible RTCPeerConnection.close error // hasCreateDataChannel should reflect the feature state still. } } return hasGetStats; }; /** @private * @param {Array} pc_ice_config ice configuration array * @param {Object} optionalStuff peer constraints. */ this.createRTCPeerConnection = function(pc_config, optionalStuff) { if (self.supportsPeerConnections()) { return new RTCPeerConnection(pc_config, optionalStuff); } else { throw "Your browser doesn't support webRTC (RTCPeerConnection)"; } }; // // this should really be part of adapter.js // Versions of chrome < 31 don't support reliable data channels transport. // Firefox does. // this.getDatachannelConstraints = function() { return { reliable: adapter && adapter.browserDetails && adapter.browserDetails.browser !== "chrome" && adapter.browserDetails.version < 31 }; }; /** @private */ haveAudioVideo = { audio: false, video: false }; /** @private */ var dataEnabled = false; /** @private */ var serverPath = null; // this was null, but that was generating an error. /** @private */ var roomOccupantListener = null; /** @private */ var onDataChannelOpen = null; /** @private */ var onDataChannelClose = null; /** @private */ var lastLoggedInList = {}; /** @private */ var receivePeer = {msgTypes: {}}; /** @private */ var receiveServerCB = null; /** @private */ // // a map keeping track of whom we've requested a call with so we don't try to // call them a second time before they've responded. // var acceptancePending = {}; /** @private * @param {string} caller * @param {Function} helper */ this.acceptCheck = function(caller, helper) { helper(true); }; /** @private * @param {string} easyrtcid * @param {HTMLMediaStream} stream */ this.streamAcceptor = function(easyrtcid, stream) { }; /** @private * @param {string} easyrtcid */ this.onStreamClosed = function(easyrtcid) { }; /** @private * @param {string} easyrtcid */ this.callCancelled = function(easyrtcid) { }; /** * This function gets the raw RTCPeerConnection for a given easyrtcid * @param {String} easyrtcid * @param {RTCPeerConnection} for that easyrtcid, or null if no connection exists * Submitted by Fabian Bernhard. */ this.getPeerConnectionByUserId = function(userId) { if (peerConns && peerConns[userId]) { return peerConns[userId].pc; } return null; }; var chromeStatsFilter = [ { "googTransmitBitrate": "transmitBitRate", "googActualEncBitrate": "encodeRate", "googAvailableSendBandwidth": "availableSendRate" }, { "googCodecName": "audioCodec", "googTypingNoiseState": "typingNoise", "packetsSent": "audioPacketsSent", "bytesSent": "audioBytesSent" }, { "googCodecName": "videoCodec", "googFrameRateSent": "outFrameRate", "packetsSent": "videoPacketsSent", "bytesSent": "videoBytesSent" }, { "packetsLost": "videoPacketsLost", "packetsReceived": "videoPacketsReceived", "bytesReceived": "videoBytesReceived", "googFrameRateOutput": "frameRateOut" }, { "packetsLost": "audioPacketsLost", "packetsReceived": "audioPacketsReceived", "bytesReceived": "audioBytesReceived", "audioOutputLevel": "audioOutputLevel" }, { "googRemoteAddress": "remoteAddress", "googActiveConnection": "activeConnection" }, { "audioInputLevel": "audioInputLevel" } ]; var firefoxStatsFilter = { "outboundrtp_audio.bytesSent": "audioBytesSent", "outboundrtp_video.bytesSent": "videoBytesSent", "inboundrtp_video.bytesReceived": "videoBytesReceived", "inboundrtp_audio.bytesReceived": "audioBytesReceived", "outboundrtp_audio.packetsSent": "audioPacketsSent", "outboundrtp_video.packetsSent": "videoPacketsSent", "inboundrtp_video.packetsReceived": "videoPacketsReceived", "inboundrtp_audio.packetsReceived": "audioPacketsReceived", "inboundrtp_video.packetsLost": "videoPacketsLost", "inboundrtp_audio.packetsLost": "audioPacketsLost", "firefoxRemoteAddress": "remoteAddress" }; /** * This is a basic statistics filter that keesp just the generally * useful information. */ this.standardStatsFilter = adapter && adapter.browserDetails && adapter.browserDetails.browser === "firefox" ? firefoxStatsFilter : chromeStatsFilter; function getFirefoxPeerStatistics(peerId, callback, filter) { if (!peerConns[peerId]) { callback(peerId, {"connected": false}); } else if (peerConns[peerId].pc.getStats) { // TODO Safari // [Error] Unhandled Promise Rejection: TypeError: Argument 1 ('selector') to RTCPeerConnection.getStats must be an instance of MediaStreamTrack peerConns[peerId].pc.getStats(null).then(function(stats) { var items = {}; var candidates = {}; var activeId = null; var srcKey; // // the stats objects has a group of entries. Each entry is either an rtcp, rtp entry // or a candidate entry. // if (stats) { stats.forEach(function(entry) { var majorKey; var subKey; if (entry.type.match(/boundrtp/)) { if (entry.id.match(/audio/)) { majorKey = entry.type + "_audio"; } else if (entry.id.match(/video/)) { majorKey = entry.type + "_video"; } else { return; } for (subKey in entry) { if (entry.hasOwnProperty(subKey)) { items[majorKey + "." + subKey] = entry[subKey]; } } } else { if( entry.hasOwnProperty("ipAddress") && entry.id) { candidates[entry.id] = entry.ipAddress + ":" + entry.portNumber; } else if( entry.hasOwnProperty("selected") && entry.hasOwnProperty("remoteCandidateId") && entry.selected ) { activeId = entry.remoteCandidateId; } } }); } if (activeId) { items.firefoxRemoteAddress = candidates[activeId]; } if (!filter) { callback(peerId, items); } else { var filteredItems = {}; for (srcKey in filter) { if (filter.hasOwnProperty(srcKey) && items.hasOwnProperty(srcKey)) { filteredItems[ filter[srcKey]] = items[srcKey]; } } callback(peerId, filteredItems); } }, function(error) { logDebug("unable to get statistics"); }); } else { callback(peerId, {"statistics": self.getConstantString("statsNotSupported")}); } } function getChromePeerStatistics(peerId, callback, filter) { if (!peerConns[peerId]) { callback(peerId, {"connected": false}); } else if (peerConns[peerId].pc.getStats) { peerConns[peerId].pc.getStats().then(function(stats) { var localStats = {}; var part, parts = stats.result(); var i, j; var itemKeys; var itemKey; var names; var userKey; var partNames = []; var partList; var bestBytes = 0; var bestI; var turnAd