/* Copyright 2015, 2016 OpenMarket 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; const DEBUG = true; // set true to enable console logging. // events: hangup, error(err), replaced(call), state(state, oldState) /** * Fires when the MatrixCall encounters an error when sending a Matrix event. *

* This is required to allow errors, which occur during sending of events, to bubble up. * (This is because call.js does a hangup when it encounters a normal `error`, which in * turn could lead to an UnknownDeviceError.) *

* To deal with an UnknownDeviceError when trying to send events, the application should let * users know that there are new devices in the encrypted room (into which the event was * sent) and give the user the options to resend unsent events or cancel them. Resending * is done using {@link module:client~MatrixClient#resendEvent} and cancelling can be done by using * {@link module:client~MatrixClient#cancelPendingEvent}. *

* MatrixCall will not do anything in response to an error that causes `send_event_error` * to be emitted with the exception of sending `m.call.candidates`, which is retried upon * failure when ICE candidates are being sent. This happens during call setup. * * @event module:webrtc/call~MatrixCall#"send_event_error" * @param {Error} err The error caught from calling client.sendEvent in call.js. * @example * matrixCall.on("send_event_error", function(err){ * console.error(err); * }); */ /** * 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 {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.URL = opts.URL; // Array of Objects with urls, username, credential keys this.turnServers = opts.turnServers || []; if (this.turnServers.length === 0) { this.turnServers.push({ urls: [MatrixCall.FALLBACK_STUN_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; } /** The length of time a call can be ringing for. */ MatrixCall.CALL_TIMEOUT_MS = 60000; /** The fallback server to use for STUN. */ MatrixCall.FALLBACK_STUN_SERVER = 'stun:stun.l.google.com:19302'; /** 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"; 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. * @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 = _getChromeScreenSharingConstraints(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) { console.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