Remove base.debug.assert.
[chromium-blink-merge.git] / remoting / webapp / base / js / client_session.js
bloba791697a90513f4de6eefd1d545f964bad26d7ad
1 // Copyright (c) 2012 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
5 /**
6  * @fileoverview
7  * Class handling creation and teardown of a remoting client session.
8  *
9  * The ClientSession class controls lifetime of the client plugin
10  * object and provides the plugin with the functionality it needs to
11  * establish connection, e.g. delivers incoming/outgoing signaling
12  * messages.
13  *
14  * This class should not access the plugin directly, instead it should
15  * do it through ClientPlugin class which abstracts plugin version
16  * differences.
17  */
19 'use strict';
21 /** @suppress {duplicate} */
22 var remoting = remoting || {};
24 /**
25  * @param {remoting.ClientPlugin} plugin
26  * @param {remoting.SignalStrategy} signalStrategy Signal strategy.
27  * @param {remoting.ClientSession.EventHandler} listener
28  *
29  * @constructor
30  * @extends {base.EventSourceImpl}
31  * @implements {base.Disposable}
32  * @implements {remoting.ClientPlugin.ConnectionEventHandler}
33  */
34 remoting.ClientSession = function(plugin, signalStrategy, listener) {
35   base.inherits(this, base.EventSourceImpl);
37   /** @private */
38   this.state_ = remoting.ClientSession.State.INITIALIZING;
40   /** @private {!remoting.Error} */
41   this.error_ = remoting.Error.none();
43   /** @private {remoting.Host} */
44   this.host_ = null;
46   /** @private {remoting.CredentialsProvider} */
47   this.credentialsProvider_ = null;
49   /** @private */
50   this.sessionId_ = '';
52   /** @private */
53   this.listener_ = listener;
55   /** @private */
56   this.hasReceivedFrame_ = false;
58   /** @private {remoting.Logger} */
59   this.logger_ = new remoting.LogToServer(signalStrategy);
61   /** @private */
62   this.signalStrategy_ = signalStrategy;
64   var state = this.signalStrategy_.getState();
65   console.assert(state == remoting.SignalStrategy.State.CONNECTED,
66                  'ClientSession ctor called in state ' + state + '.');
67   this.signalStrategy_.setIncomingStanzaCallback(
68       this.onIncomingMessage_.bind(this));
70   /** @private {remoting.FormatIq} */
71   this.iqFormatter_ = null;
73   /**
74    * Allow host-offline error reporting to be suppressed in situations where it
75    * would not be useful, for example, when using a cached host JID.
76    *
77    * @type {boolean} @private
78    */
79   this.logHostOfflineErrors_ = true;
81   /** @private {remoting.ClientPlugin} */
82   this.plugin_ = plugin;
83   plugin.setConnectionEventHandler(this);
85   /** @private  */
86   this.connectedDisposables_ = new base.Disposables();
88   this.defineEvents(Object.keys(remoting.ClientSession.Events));
91 /** @enum {string} */
92 remoting.ClientSession.Events = {
93   videoChannelStateChanged: 'videoChannelStateChanged'
96 /**
97  * @interface
98  * [START]-------> [onConnected] ------> [onDisconnected]
99  *    |
100  *    |-----> [OnConnectionFailed]
102  */
103 remoting.ClientSession.EventHandler = function() {};
106  * Called when the connection failed before it is connected.
108  * @param {!remoting.Error} error
109  */
110 remoting.ClientSession.EventHandler.prototype.onConnectionFailed =
111     function(error) {};
114  * Called when a new session has been connected.  The |connectionInfo| will be
115  * valid until onDisconnected() is called.
117  * @param {!remoting.ConnectionInfo} connectionInfo
118  */
119 remoting.ClientSession.EventHandler.prototype.onConnected =
120     function(connectionInfo) {};
123  * Called when the current session has been disconnected.
125  * @param {!remoting.Error} reason Reason that the session is disconnected.
126  *     Set to remoting.Error.none() if there is no error.
127  */
128 remoting.ClientSession.EventHandler.prototype.onDisconnected =
129     function(reason) {};
131 // Note that the positive values in both of these enums are copied directly
132 // from connection_to_host.h and must be kept in sync. Code in
133 // chromoting_instance.cc converts the C++ enums into strings that must match
134 // the names given here.
135 // The negative values represent state transitions that occur within the
136 // web-app that have no corresponding plugin state transition.
137 /** @enum {number} */
138 remoting.ClientSession.State = {
139   CONNECTION_CANCELED: -3,  // Connection closed (gracefully) before connecting.
140   CONNECTION_DROPPED: -2,  // Succeeded, but subsequently closed with an error.
141   CREATED: -1,
142   UNKNOWN: 0,
143   INITIALIZING: 1,
144   CONNECTING: 2,
145   AUTHENTICATED: 3,
146   CONNECTED: 4,
147   CLOSED: 5,
148   FAILED: 6
152  * @param {string} state The state name.
153  * @return {remoting.ClientSession.State} The session state enum value.
154  */
155 remoting.ClientSession.State.fromString = function(state) {
156   if (!remoting.ClientSession.State.hasOwnProperty(state)) {
157     throw "Invalid ClientSession.State: " + state;
158   }
159   return remoting.ClientSession.State[state];
162 /** @enum {number} */
163 remoting.ClientSession.ConnectionError = {
164   UNKNOWN: -1,
165   NONE: 0,
166   HOST_IS_OFFLINE: 1,
167   SESSION_REJECTED: 2,
168   INCOMPATIBLE_PROTOCOL: 3,
169   NETWORK_FAILURE: 4,
170   HOST_OVERLOAD: 5
174  * @param {string} error The connection error name.
175  * @return {remoting.ClientSession.ConnectionError} The connection error enum.
176  */
177 remoting.ClientSession.ConnectionError.fromString = function(error) {
178   if (!remoting.ClientSession.ConnectionError.hasOwnProperty(error)) {
179     console.error('Unexpected ClientSession.ConnectionError string: ', error);
180     return remoting.ClientSession.ConnectionError.UNKNOWN;
181   }
182   return remoting.ClientSession.ConnectionError[error];
186  * Type used for performance statistics collected by the plugin.
187  * @constructor
188  */
189 remoting.ClientSession.PerfStats = function() {};
190 /** @type {number} */
191 remoting.ClientSession.PerfStats.prototype.videoBandwidth;
192 /** @type {number} */
193 remoting.ClientSession.PerfStats.prototype.videoFrameRate;
194 /** @type {number} */
195 remoting.ClientSession.PerfStats.prototype.captureLatency;
196 /** @type {number} */
197 remoting.ClientSession.PerfStats.prototype.encodeLatency;
198 /** @type {number} */
199 remoting.ClientSession.PerfStats.prototype.decodeLatency;
200 /** @type {number} */
201 remoting.ClientSession.PerfStats.prototype.renderLatency;
202 /** @type {number} */
203 remoting.ClientSession.PerfStats.prototype.roundtripLatency;
205 // Keys for connection statistics.
206 remoting.ClientSession.STATS_KEY_VIDEO_BANDWIDTH = 'videoBandwidth';
207 remoting.ClientSession.STATS_KEY_VIDEO_FRAME_RATE = 'videoFrameRate';
208 remoting.ClientSession.STATS_KEY_CAPTURE_LATENCY = 'captureLatency';
209 remoting.ClientSession.STATS_KEY_ENCODE_LATENCY = 'encodeLatency';
210 remoting.ClientSession.STATS_KEY_DECODE_LATENCY = 'decodeLatency';
211 remoting.ClientSession.STATS_KEY_RENDER_LATENCY = 'renderLatency';
212 remoting.ClientSession.STATS_KEY_ROUNDTRIP_LATENCY = 'roundtripLatency';
215  * Set of capabilities for which hasCapability() can be used to test.
217  * @enum {string}
218  */
219 remoting.ClientSession.Capability = {
220   // When enabled this capability causes the client to send its screen
221   // resolution to the host once connection has been established. See
222   // this.plugin_.notifyClientResolution().
223   SEND_INITIAL_RESOLUTION: 'sendInitialResolution',
225   // Let the host know that we're interested in knowing whether or not it
226   // rate limits desktop-resize requests.
227   // TODO(kelvinp): This has been supported since M-29.  Currently we only have
228   // <1000 users on M-29 or below. Remove this and the capability on the host.
229   RATE_LIMIT_RESIZE_REQUESTS: 'rateLimitResizeRequests',
231   // Indicates native touch input support. If the host does not support
232   // touch then the client will let Chrome synthesize mouse events from touch
233   // input, for compatibility with non-touch-aware systems.
234   TOUCH_EVENTS: 'touchEvents',
236   // Indicates that host/client supports Google Drive integration, and that the
237   // client should send to the host the OAuth tokens to be used by Google Drive
238   // on the host.
239   GOOGLE_DRIVE: 'googleDrive',
241   // Indicates that the client supports the video frame-recording extension.
242   VIDEO_RECORDER: 'videoRecorder',
244   // Indicates that the client supports 'cast'ing the video stream to a
245   // cast-enabled device.
246   CAST: 'casting',
250  * Connects to |host| using |credentialsProvider| as the credentails.
252  * @param {remoting.Host} host
253  * @param {remoting.CredentialsProvider} credentialsProvider
254  */
255 remoting.ClientSession.prototype.connect = function(host, credentialsProvider) {
256   this.host_ = host;
257   this.credentialsProvider_ = credentialsProvider;
258   this.iqFormatter_ =
259       new remoting.FormatIq(this.signalStrategy_.getJid(), host.jabberId);
260   this.plugin_.connect(this.host_, this.signalStrategy_.getJid(),
261                        credentialsProvider);
265  * Disconnect the current session with a particular |error|.  The session will
266  * raise a |stateChanged| event in response to it.  The caller should then call
267  * dispose() to remove and destroy the <embed> element.
269  * @param {!remoting.Error} error The reason for the disconnection.  Use
270  *    remoting.Error.none() if there is no error.
271  * @return {void} Nothing.
272  */
273 remoting.ClientSession.prototype.disconnect = function(error) {
274   if (this.isFinished()) {
275     // Do not send the session-terminate Iq if disconnect() is already called or
276     // if it is initiated by the host.
277     return;
278   }
280   this.sendIq_(
281       '<cli:iq ' +
282           'to="' + this.host_.jabberId + '" ' +
283           'type="set" ' +
284           'id="session-terminate" ' +
285           'xmlns:cli="jabber:client">' +
286         '<jingle ' +
287             'xmlns="urn:xmpp:jingle:1" ' +
288             'action="session-terminate" ' +
289             'sid="' + this.sessionId_ + '">' +
290           '<reason><success/></reason>' +
291         '</jingle>' +
292       '</cli:iq>');
294   var state = error.isNone() ?
295                   remoting.ClientSession.State.CLOSED :
296                   remoting.ClientSession.State.FAILED;
297   this.error_ = error;
298   this.setState_(state);
302  * Deletes the <embed> element from the container and disconnects.
304  * @return {void} Nothing.
305  */
306 remoting.ClientSession.prototype.dispose = function() {
307   base.dispose(this.connectedDisposables_);
308   this.connectedDisposables_ = null;
309   base.dispose(this.plugin_);
310   this.plugin_ = null;
314  * @return {remoting.ClientSession.State} The current state.
315  */
316 remoting.ClientSession.prototype.getState = function() {
317   return this.state_;
321  * @return {remoting.Logger}.
322  */
323 remoting.ClientSession.prototype.getLogger = function() {
324   return this.logger_;
328  * @return {!remoting.Error} The current error code.
329  */
330 remoting.ClientSession.prototype.getError = function() {
331   return this.error_;
335  * Drop the session when the computer is suspended for more than
336  * |suspendDurationInMS|.
338  * @param {number} suspendDurationInMS maximum duration of suspension allowed
339  *     before the session will be dropped.
340  */
341 remoting.ClientSession.prototype.dropSessionOnSuspend = function(
342     suspendDurationInMS) {
343   if (this.state_ !== remoting.ClientSession.State.CONNECTED) {
344     console.error('The session is not connected.');
345     return;
346   }
348   var suspendDetector = new remoting.SuspendDetector(suspendDurationInMS);
349   this.connectedDisposables_.add(
350       suspendDetector,
351       new base.EventHook(
352           suspendDetector, remoting.SuspendDetector.Events.resume,
353           this.disconnect.bind(
354               this, new remoting.Error(remoting.Error.Tag.CLIENT_SUSPENDED))));
358  * Called when the client receives its first frame.
360  * @return {void} Nothing.
361  */
362 remoting.ClientSession.prototype.onFirstFrameReceived = function() {
363   this.hasReceivedFrame_ = true;
367  * @return {boolean} Whether the client has received a video buffer.
368  */
369 remoting.ClientSession.prototype.hasReceivedFrame = function() {
370   return this.hasReceivedFrame_;
374  * Sends a signaling message.
376  * @param {string} message XML string of IQ stanza to send to server.
377  * @return {void} Nothing.
378  * @private
379  */
380 remoting.ClientSession.prototype.sendIq_ = function(message) {
381   // Extract the session id, so we can close the session later.
382   var parser = new DOMParser();
383   var iqNode = parser.parseFromString(message, 'text/xml').firstChild;
384   var jingleNode = iqNode.firstChild;
385   if (jingleNode) {
386     var action = jingleNode.getAttribute('action');
387     if (jingleNode.nodeName == 'jingle' && action == 'session-initiate') {
388       this.sessionId_ = jingleNode.getAttribute('sid');
389     }
390   }
392   console.log(base.timestamp() + this.iqFormatter_.prettifySendIq(message));
393   if (this.signalStrategy_.getState() !=
394       remoting.SignalStrategy.State.CONNECTED) {
395     console.log("Message above is dropped because signaling is not connected.");
396     return;
397   }
399   this.signalStrategy_.sendMessage(message);
403  * @param {string} message XML string of IQ stanza to send to server.
404  */
405 remoting.ClientSession.prototype.onOutgoingIq = function(message) {
406   this.sendIq_(message);
410  * @param {string} msg
411  */
412 remoting.ClientSession.prototype.onDebugMessage = function(msg) {
413   console.log('plugin: ' + msg.trimRight());
417  * @param {Element} message
418  * @private
419  */
420 remoting.ClientSession.prototype.onIncomingMessage_ = function(message) {
421   if (!this.plugin_) {
422     return;
423   }
424   var formatted = new XMLSerializer().serializeToString(message);
425   console.log(base.timestamp() +
426               this.iqFormatter_.prettifyReceiveIq(formatted));
427   this.plugin_.onIncomingIq(formatted);
431  * Callback that the plugin invokes to indicate that the connection
432  * status has changed.
434  * @param {remoting.ClientSession.State} status The plugin's status.
435  * @param {remoting.ClientSession.ConnectionError} error The plugin's error
436  *        state, if any.
437  */
438 remoting.ClientSession.prototype.onConnectionStatusUpdate =
439     function(status, error) {
440   if (status == remoting.ClientSession.State.FAILED) {
441     switch (error) {
442       case remoting.ClientSession.ConnectionError.HOST_IS_OFFLINE:
443         this.error_ = new remoting.Error(
444             remoting.Error.Tag.HOST_IS_OFFLINE);
445         break;
446       case remoting.ClientSession.ConnectionError.SESSION_REJECTED:
447         this.error_ = new remoting.Error(
448             remoting.Error.Tag.INVALID_ACCESS_CODE);
449         break;
450       case remoting.ClientSession.ConnectionError.INCOMPATIBLE_PROTOCOL:
451         this.error_ = new remoting.Error(
452             remoting.Error.Tag.INCOMPATIBLE_PROTOCOL);
453         break;
454       case remoting.ClientSession.ConnectionError.NETWORK_FAILURE:
455         this.error_ = new remoting.Error(
456             remoting.Error.Tag.P2P_FAILURE);
457         break;
458       case remoting.ClientSession.ConnectionError.HOST_OVERLOAD:
459         this.error_ = new remoting.Error(
460             remoting.Error.Tag.HOST_OVERLOAD);
461         break;
462       default:
463         this.error_ = remoting.Error.unexpected();
464     }
465   }
466   this.setState_(status);
470  * Callback that the plugin invokes to indicate that the connection type for
471  * a channel has changed.
473  * @param {string} channel The channel name.
474  * @param {string} connectionType The new connection type.
475  * @private
476  */
477 remoting.ClientSession.prototype.onRouteChanged =
478     function(channel, connectionType) {
479   console.log('plugin: Channel ' + channel + ' using ' +
480               connectionType + ' connection.');
481   this.logger_.setConnectionType(connectionType);
485  * Callback that the plugin invokes to indicate when the connection is
486  * ready.
488  * @param {boolean} ready True if the connection is ready.
489  */
490 remoting.ClientSession.prototype.onConnectionReady = function(ready) {
491   // TODO(jamiewalch): Currently, the logic for determining whether or not the
492   // connection is available is based solely on whether or not any video frames
493   // have been received recently. which leads to poor UX on slow connections.
494   // Re-enable this once crbug.com/435315 has been fixed.
495   var ignoreVideoChannelState = true;
496   if (ignoreVideoChannelState) {
497     console.log('Video channel ' + (ready ? '' : 'not ') + 'ready.');
498     return;
499   }
501   this.raiseEvent(remoting.ClientSession.Events.videoChannelStateChanged,
502                   ready);
505 /** @return {boolean} */
506 remoting.ClientSession.prototype.isFinished = function() {
507   var finishedStates = [
508     remoting.ClientSession.State.CLOSED,
509     remoting.ClientSession.State.FAILED,
510     remoting.ClientSession.State.CONNECTION_CANCELED,
511     remoting.ClientSession.State.CONNECTION_DROPPED
512   ];
513   return finishedStates.indexOf(this.getState()) !== -1;
516  * @param {remoting.ClientSession.State} newState The new state for the session.
517  * @return {void} Nothing.
518  * @private
519  */
520 remoting.ClientSession.prototype.setState_ = function(newState) {
521   // If we are at a finished state, ignore further state changes.
522   if (this.isFinished()) {
523     return;
524   }
526   var oldState = this.state_;
527   this.state_ = this.translateState_(oldState, newState);
529   if (newState == remoting.ClientSession.State.CONNECTED) {
530     this.connectedDisposables_.add(
531         new base.RepeatingTimer(this.reportStatistics.bind(this), 1000));
532     if (this.plugin_.hasCapability(
533           remoting.ClientSession.Capability.TOUCH_EVENTS)) {
534       this.plugin_.enableTouchEvents(true);
535     }
536   } else if (this.isFinished()) {
537     base.dispose(this.connectedDisposables_);
538     this.connectedDisposables_ = null;
539   }
541   this.notifyStateChanges_(oldState, this.state_);
542   this.logger_.logClientSessionStateChange(this.state_, this.error_);
546  * @param {remoting.ClientSession.State} oldState The new state for the session.
547  * @param {remoting.ClientSession.State} newState The new state for the session.
548  * @private
549  */
550 remoting.ClientSession.prototype.notifyStateChanges_ =
551     function(oldState, newState) {
552   /** @type {remoting.Error} */
553   var error;
554   switch (this.state_) {
555     case remoting.ClientSession.State.CONNECTED:
556       console.log('Connection established.');
557       var connectionInfo = new remoting.ConnectionInfo(
558           this.host_, this.credentialsProvider_, this, this.plugin_);
559       this.listener_.onConnected(connectionInfo);
560       break;
562     case remoting.ClientSession.State.CONNECTING:
563       remoting.identity.getEmail().then(function(/** string */ email) {
564         console.log('Connecting as ' + email);
565       });
566       break;
568     case remoting.ClientSession.State.AUTHENTICATED:
569       console.log('Connection authenticated.');
570       break;
572     case remoting.ClientSession.State.INITIALIZING:
573       console.log('Connection initializing .');
574       break;
576     case remoting.ClientSession.State.CLOSED:
577       console.log('Connection closed.');
578       this.listener_.onDisconnected(remoting.Error.none());
579       break;
581     case remoting.ClientSession.State.CONNECTION_CANCELED:
582     case remoting.ClientSession.State.FAILED:
583       error = this.getError();
584       if (!error.isNone()) {
585         console.error('Connection failed: ' + error.toString());
586       }
587       this.listener_.onConnectionFailed(error);
588       break;
590     case remoting.ClientSession.State.CONNECTION_DROPPED:
591       error = this.getError();
592       console.error('Connection dropped: ' + error.toString());
593       this.listener_.onDisconnected(error);
594       break;
596     default:
597       console.error('Unexpected client plugin state: ' + newState);
598   }
602  * @param {remoting.ClientSession.State} previous
603  * @param {remoting.ClientSession.State} current
604  * @return {remoting.ClientSession.State}
605  * @private
606  */
607 remoting.ClientSession.prototype.translateState_ = function(previous, current) {
608   var State = remoting.ClientSession.State;
609   if (previous == State.CONNECTING || previous == State.AUTHENTICATED) {
610     if (current == State.CLOSED) {
611       return remoting.ClientSession.State.CONNECTION_CANCELED;
612     } else if (current == State.FAILED &&
613         this.error_.hasTag(remoting.Error.Tag.HOST_IS_OFFLINE) &&
614         !this.logHostOfflineErrors_) {
615       // The application requested host-offline errors to be suppressed, for
616       // example, because this connection attempt is using a cached host JID.
617       console.log('Suppressing host-offline error.');
618       return State.CONNECTION_CANCELED;
619     }
620   } else if (previous == State.CONNECTED && current == State.FAILED) {
621     return State.CONNECTION_DROPPED;
622   }
623   return current;
626 /** @private */
627 remoting.ClientSession.prototype.reportStatistics = function() {
628   this.logger_.logStatistics(this.plugin_.getPerfStats());
632  * Enable or disable logging of connection errors due to a host being offline.
633  * For example, if attempting a connection using a cached JID, host-offline
634  * errors should not be logged because the JID will be refreshed and the
635  * connection retried.
637  * @param {boolean} enable True to log host-offline errors; false to suppress.
638  */
639 remoting.ClientSession.prototype.logHostOfflineErrors = function(enable) {
640   this.logHostOfflineErrors_ = enable;