/* Copyright 2015, 2016 OpenMarket Ltd Copyright 2017 New Vector Ltd Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ "use strict"; /** * This is an internal module. See {@link createNewMatrixCall} for the public API. * @module webrtc/call */ const utils = require("../utils"); const EventEmitter = require("events").EventEmitter; import logger from '../../src/logger'; const DEBUG = true; // set true to enable console logging. // events: hangup, error(err), replaced(call), state(state, oldState) /** * Fires whenever an error occurs when call.js encounters an issue with setting up the call. *

* The error given will have a code equal to either `MatrixCall.ERR_LOCAL_OFFER_FAILED` or * `MatrixCall.ERR_NO_USER_MEDIA`. `ERR_LOCAL_OFFER_FAILED` is emitted when the local client * fails to create an offer. `ERR_NO_USER_MEDIA` is emitted when the user has denied access * to their audio/video hardware. * * @event module:webrtc/call~MatrixCall#"error" * @param {Error} err The error raised by MatrixCall. * @example * matrixCall.on("error", function(err){ * console.error(err.code, err); * }); */ /** * Construct a new Matrix Call. * @constructor * @param {Object} opts Config options. * @param {string} opts.roomId The room ID for this call. * @param {Object} opts.webRtc The WebRTC globals from the browser. * @param {boolean} opts.forceTURN whether relay through TURN should be forced. * @param {Object} opts.URL The URL global. * @param {Array} opts.turnServers Optional. A list of TURN servers. * @param {MatrixClient} opts.client The Matrix Client instance to send events to. */ function MatrixCall(opts) { this.roomId = opts.roomId; this.client = opts.client; this.webRtc = opts.webRtc; this.forceTURN = opts.forceTURN; this.URL = opts.URL; // Array of Objects with urls, username, credential keys this.turnServers = opts.turnServers || []; if (this.turnServers.length === 0 && this.client.isFallbackICEServerAllowed()) { this.turnServers.push({ urls: [MatrixCall.FALLBACK_ICE_SERVER], }); } utils.forEach(this.turnServers, function(server) { utils.checkObjectHasKeys(server, ["urls"]); }); this.callId = "c" + new Date().getTime() + Math.random(); this.state = 'fledgling'; this.didConnect = false; // A queue for candidates waiting to go out. // We try to amalgamate candidates into a single candidate message where // possible this.candidateSendQueue = []; this.candidateSendTries = 0; // Lookup from opaque queue ID to a promise for media element operations that // need to be serialised into a given queue. Store this per-MatrixCall on the // assumption that multiple matrix calls will never compete for control of the // same DOM elements. this.mediaPromises = Object.create(null); this.screenSharingStream = null; this._answerContent = null; } /** The length of time a call can be ringing for. */ MatrixCall.CALL_TIMEOUT_MS = 60000; /** The fallback ICE server to use for STUN or TURN protocols. */ MatrixCall.FALLBACK_ICE_SERVER = 'stun:turn.matrix.org'; /** An error code when the local client failed to create an offer. */ MatrixCall.ERR_LOCAL_OFFER_FAILED = "local_offer_failed"; /** * An error code when there is no local mic/camera to use. This may be because * the hardware isn't plugged in, or the user has explicitly denied access. */ MatrixCall.ERR_NO_USER_MEDIA = "no_user_media"; /* * Error code used when a call event failed to send * because unknown devices were present in the room */ MatrixCall.ERR_UNKNOWN_DEVICES = "unknown_devices"; /* * Error code usewd when we fail to send the invite * for some reason other than there being unknown devices */ MatrixCall.ERR_SEND_INVITE = "send_invite"; /* * Error code usewd when we fail to send the answer * for some reason other than there being unknown devices */ MatrixCall.ERR_SEND_ANSWER = "send_answer"; utils.inherits(MatrixCall, EventEmitter); /** * Place a voice call to this room. * @throws If you have not specified a listener for 'error' events. */ MatrixCall.prototype.placeVoiceCall = function() { debuglog("placeVoiceCall"); checkForErrorListener(this); _placeCallWithConstraints(this, _getUserMediaVideoContraints('voice')); this.type = 'voice'; }; /** * Place a video call to this room. * @param {Element} remoteVideoElement a <video> DOM element * to render video to. * @param {Element} localVideoElement a <video> DOM element * to render the local camera preview. * @throws If you have not specified a listener for 'error' events. */ MatrixCall.prototype.placeVideoCall = function(remoteVideoElement, localVideoElement) { debuglog("placeVideoCall"); checkForErrorListener(this); this.localVideoElement = localVideoElement; this.remoteVideoElement = remoteVideoElement; _placeCallWithConstraints(this, _getUserMediaVideoContraints('video')); this.type = 'video'; _tryPlayRemoteStream(this); }; /** * Place a screen-sharing call to this room. This includes audio. * This method is EXPERIMENTAL and subject to change without warning. It * only works in Google Chrome and Firefox >= 44. * @param {Element} remoteVideoElement a <video> DOM element * to render video to. * @param {Element} localVideoElement a <video> DOM element * to render the local camera preview. * @throws If you have not specified a listener for 'error' events. */ MatrixCall.prototype.placeScreenSharingCall = function(remoteVideoElement, localVideoElement) { debuglog("placeScreenSharingCall"); checkForErrorListener(this); const screenConstraints = _getScreenSharingConstraints(this); if (!screenConstraints) { return; } this.localVideoElement = localVideoElement; this.remoteVideoElement = remoteVideoElement; const self = this; this.webRtc.getUserMedia(screenConstraints, function(stream) { self.screenSharingStream = stream; debuglog("Got screen stream, requesting audio stream..."); const audioConstraints = _getUserMediaVideoContraints('voice'); _placeCallWithConstraints(self, audioConstraints); }, function(err) { self.emit("error", callError( MatrixCall.ERR_NO_USER_MEDIA, "Failed to get screen-sharing stream: " + err, ), ); }); this.type = 'video'; _tryPlayRemoteStream(this); }; /** * Play the given HTMLMediaElement, serialising the operation into a chain * of promises to avoid racing access to the element * @param {Element} element HTMLMediaElement element to play * @param {string} queueId Arbitrary ID to track the chain of promises to be used */ MatrixCall.prototype.playElement = function(element, queueId) { logger.log("queuing play on " + queueId + " and element " + element); // XXX: FIXME: Does this leak elements, given the old promises // may hang around and retain a reference to them? if (this.mediaPromises[queueId]) { // XXX: these promises can fail (e.g. by