UNPKG

open-easyrtc

Version:

Open-EasyRTC enables quick development of WebRTC

1,291 lines (1,161 loc) 232 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; } /** peerConns is a map from caller names to the below object structure { pc: RTCPeerConnection - the WebRTC peer connection object isInitiator: boolean - true if this peer initiated the connection // States accepted: boolean - true if the connection has been accepted cancelled: boolean - temporarily true if a connection was cancelled by the peer asking to initiate it failing: boolean - true if the connection is in a failed state pendingAwnser: // RTC candidatesToSend: Array - SDP candidates temporarily queued supportHalfTrickleIce: boolean - true if peer supports half trickle ICE // Data dataChannelS: RTCDataChannel - RTC DataChannel for outgoing messages if present dataChannelR: RTCDataChannel - RTC DataChannel for incoming messages if present dataChannelReady: boolean - true if the data channel can be used for sending yet sharingData: boolean - true if data channel is being shared // Streams connectTime: number - timestamp when the connection was started startedAV: boolean - true if we have traded audio/video streams sharingAudio: boolean - true if audio is being shared sharingVideo: boolean - true if video is being shared liveRemoteStreams: Object - map of active remote media streams remoteStreamIdToName: Object - map of remote stream IDs to names streamsAddedAcks: Array - ack callbacks waiting for stream received messages mediaStream: MediaStream - local media stream object // Callbacks callSuccessCB: function(string) - see the easyrtc.call documentation callFailureCB: function(errorCode, string) - see the easyrtc.call documentation wasAcceptedCB: function(boolean, string) - see the easyrtc.call documentation } @private */ 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; /** * Socket.IO connection configuration options. * @private * @type {Object} * @property {string[]} transports - List of transports to try in order. WebSocket first if available * because some government agencies don't like WSS. * @property {boolean} withCredentials - Whether cross-site requests should use credentials * such as cookies and authorization headers. * @property {boolean} reconnection - Whether to reconnect automatically on connection loss. * @property {boolean} forceNew - Whether to reuse an existing connection or force a new one. * @property {number} pingTimeout - Connection timeout in milliseconds before connect_error * and connect_timeout events are emitted. How long to wait for pong before disconnect. * @property {number} pingInterval - How often to send ping packets in milliseconds. */ var connectionOptions = { // a list of transports to try (in order). // use WebSocket first, if available because some gov agency dont like wss "transports": ["polling", "websocket"], // whether or not cross-site requests should made using credentials such as cookies, authorization headers. "withCredentials": false, // whether to reconnect automatically "reconnection": false, // whether to reuse an existing connection 'forceNew': true, // connection timeout before a connect_error and connect_timeout events are emitted // how long to wait for pong before disconnect 'pingTimeout': 10000, // how often to send ping 'pingInterval': 5000 }; /** * 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 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; } if (peerConnObj.sendingOffer) { logDebug('renegotiate already in progress', peerConnObj.sendingOffer); return; } var pc = peerConnObj.pc; var callFailureCB = peerConnObj.callFailureCB || self.showError; if (typeof iceRestart === 'undefined') { iceRestart = pc.iceConnectionState !== 'connected'; } logDebug('renegotiate iceRestart:' + iceRestart); peerConnObj.sendingOffer = true; pc.createOffer({ iceRestart: iceRestart }) .then(function setLocalAndSendRenegotiate(sessionDescription) { if (peerConnObj.cancelled) { logDebug('renegotiate ignored because call cancelled', peerConnObj.cancelled, peerConnObj.sendingOffer); return; } if (sdpLocalFilter) { sessionDescription.sdp = sdpLocalFilter(sessionDescription.sdp); } pc.setLocalDescription(sessionDescription).then(function() { peerConnObj.sendingOffer = false; sendSignalling(otherUser, "offer", sessionDescription, null, callFailureCB); }, function(errorText) { peerConnObj.sendingOffer = false; callFailureCB(self.errCodes.CALL_ERR, errorText); }); }, 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 * @param {String} key */ this.getConstantString = function(key) { // // 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 // 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.webSocket = null; /** @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 userConfig = {}; /** @private */ var offersPending = {}; /** @private */ var credential = null; /** @private */ this.audioEnabled = true; /** @private */ this.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 = {}; /** * @private * Room data state from signaling server. * @example * var roomData = { * [roomName]{ * field: {}, * roomStatus: "join"|"leave", * clientList: {}, * clientListDelta: { * removeClient: [string] * updateClient: { * [string]: { * ["apiField" | "presence"]: any * } * } * } * } * processRoomData(roomData) */ this.roomData = {}; /** 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 */ this.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} pcIceConfig 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 */ var haveAudioVideo = { audio: false, video: false }; /** @private */ var dataEnabled = false; // this was null, but that was generating an error. /** @private */ var serverPath = null; /** @private */ var roomOccupantListener = null; /** @private */ var onDataChannelOpen = null; /** @private */ var onDataChannelClose = null; /** @private */ var lastLoggedInList = {}; /** @private */ var receivePeer = {msgTypes: {}}; /** @private */ var receiveServerCB = null; // // 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. // /** @private */ 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) { }; /** @private * @param {string} easyrtcid */ this.hasPendingOffer = function (easyrtcid) { return !!offersPending[easyrtcid]; }; /** @private * @param {string} easyrtcid */ this.hasAcceptancePending = function (easyrtcid) { return !!acceptancePending[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(easyrtcid) { if (peerConns && peerConns[easyrtcid]) { return peerConns[easyrtcid].pc; } return null; }; /** * This function gets the statistics for a particular peer connection. * @param {String} easyrtcid * @param {Function} callback gets the easyrtcid for the peer and a stats object with basics stats and raw webrtc stats as report property. * If there is no peer connection to easyrtcid, then the stats will have a value of {connected:false}. */ this.getPeerStatistics = function(easyrtcid, callback) { if (!peerConns[easyrtcid]) { callback(easyrtcid, {"connected": false}); } else if (peerConns[easyrtcid].pc.getStats) { var stats = { "audioLevel": -1, "framesSent": -1, "framesDecoded": -1, "framesDropped": -1, "framesReceived": -1, "download": -1, "upload": -1, "report": null }; peerConns[easyrtcid].pc.getStats().then(function(res) { // Handle falsy RTCStatsResponse if (!res) { return; } // Handle RTCStatsResponse if (typeof res.result === "function") { res = res.result(); } var lastResult = peerConns[easyrtcid].stats; // Reset cumulative stats stats.download = 0; stats.upload = 0; stats.framesSent = 0; stats.framesDropped = 0; stats.framesDecoded = 0; stats.framesReceived = 0; stats.report = res; res.forEach(function(report) { var bytes, bitrate; var type = report.type; var now = report.timestamp; // debug; //console.log(report.type, report.id, report.kind, report); if (report.bytesSent && ( type === 'outboundrtp' || type === 'outbound-rtp' ) ) { if (lastResult && lastResult.get(report.id)) { bytes = report.bytesSent; // calculate bitrate bitrate = 8 * (bytes - lastResult.get(report.id).bytesSent) / (now - lastResult.get(report.id).timestamp); if (bitrate > 0) { stats.upload += bitrate; } } if (report.kind === 'video') { stats.framesSent = +report.framesSent; } } else if (report.bytesReceived && ( type === 'inboundrtp' || type === 'inbound-rtp' || type === 'ssrc' ) ) { if (lastResult && lastResult.get(report.id)) { bytes = report.bytesReceived; // calculate bitrate bitrate = 8 * (bytes - lastResult.get(report.id).bytesReceived) / (now - lastResult.get(report.id).timestamp); if (bitrate > 0) { stats.download += bitrate; } } if (report.kind === 'video') { stats.framesDecoded = +report.framesDecoded; stats.framesDropped = +report.framesDropped; stats.framesReceived = +report.framesReceived; } } if (type === 'media-source' && !report.ended && !report.detached && report.hasOwnProperty('audioLevel') && !report.frameHeight) { stats.audioLevel = +report.audioLevel; // TODO safari //stats.audioLevel = stats.audioLevel * 10; } }); peerConns[easyrtcid].stats = res; callback(easyrtcid, stats); }); } else { callback(easyrtcid, {"error": self.getConstantString("statsNotSupported")}); } }; /** * @private * @param roomName * @param fields */ function sendRoomApiFields(roomName, fields) { var dataToShip = { msgType: "setRoomApiField", msgData: { setRoomApiField: { roomName: roomName, field: fields } } }; if (self.webSocket) { self.webSocket.emit("easyrtcCmd", dataToShip, function(ackMsg) { if (ackMsg.msgType === "error") { self.showError(ackMsg.msgData.errorCode, ackMsg.msgData.errorText); } } ); } else { logDebug("websocket failed because no connection to server"); throw "Attempt to send message without a valid connection to the server."; } } /** @private */ var roomApiFieldTimer =