Roll src/third_party/WebKit 3529d49:06e8485 (svn 202554:202555)
[chromium-blink-merge.git] / remoting / webapp / base / js / client_session.js
blobac74e948708b8fafb0dede7b2edd3b0a97cb7ce9
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.Logger} logger
28  * @param {remoting.ClientSession.EventHandler} listener
29  *
30  * @constructor
31  * @extends {base.EventSourceImpl}
32  * @implements {base.Disposable}
33  * @implements {remoting.ClientPlugin.ConnectionEventHandler}
34  */
35 remoting.ClientSession = function(
36     plugin, signalStrategy, logger, listener) {
37   base.inherits(this, base.EventSourceImpl);
39   /** @private */
40   this.state_ = remoting.ClientSession.State.INITIALIZING;
42   /** @private {!remoting.Error} */
43   this.error_ = remoting.Error.none();
45   /** @private {remoting.Host} */
46   this.host_ = null;
48   /** @private {remoting.CredentialsProvider} */
49   this.credentialsProvider_ = null;
51   /** @private */
52   this.sessionId_ = '';
54   /** @private */
55   this.listener_ = listener;
57   /** @private */
58   this.hasReceivedFrame_ = false;
60   /** @private {remoting.Logger} */
61   this.logger_ = logger;
63   /** @private */
64   this.signalStrategy_ = signalStrategy;
66   var state = this.signalStrategy_.getState();
67   console.assert(state == remoting.SignalStrategy.State.CONNECTED,
68                  'ClientSession ctor called in state ' + state + '.');
69   this.signalStrategy_.setIncomingStanzaCallback(
70       this.onIncomingMessage_.bind(this));
72   /** @private {remoting.FormatIq} */
73   this.iqFormatter_ = null;
75   /** @private {remoting.XmppErrorCache} */
76   this.xmppErrorCache_ = new remoting.XmppErrorCache();
79   /** @private {remoting.ClientPlugin} */
80   this.plugin_ = plugin;
81   plugin.setConnectionEventHandler(this);
83   /** @private  */
84   this.connectedDisposables_ = new base.Disposables();
86   this.defineEvents(Object.keys(remoting.ClientSession.Events));
89 /** @enum {string} */
90 remoting.ClientSession.Events = {
91   videoChannelStateChanged: 'videoChannelStateChanged'
94 /**
95  * @interface
96  * [START]-------> [onConnected] ------> [onDisconnected]
97  *    |
98  *    |-----> [OnConnectionFailed]
99  *
100  */
101 remoting.ClientSession.EventHandler = function() {};
104  * Called when the connection failed before it is connected.
106  * @param {!remoting.Error} error
107  */
108 remoting.ClientSession.EventHandler.prototype.onConnectionFailed =
109     function(error) {};
112  * Called when a new session has been connected.  The |connectionInfo| will be
113  * valid until onDisconnected() is called.
115  * @param {!remoting.ConnectionInfo} connectionInfo
116  */
117 remoting.ClientSession.EventHandler.prototype.onConnected =
118     function(connectionInfo) {};
121  * Called when the current session has been disconnected.
123  * @param {!remoting.Error} reason Reason that the session is disconnected.
124  *     Set to remoting.Error.none() if there is no error.
125  */
126 remoting.ClientSession.EventHandler.prototype.onDisconnected =
127     function(reason) {};
129 // Note that the positive values in both of these enums are copied directly
130 // from connection_to_host.h and must be kept in sync. Code in
131 // chromoting_instance.cc converts the C++ enums into strings that must match
132 // the names given here.
133 // The negative values represent state transitions that occur within the
134 // web-app that have no corresponding plugin state transition.
136 // TODO(kelvinp): Merge this enum with remoting.ChromotingEvent.SessionState
137 // once we have migrated away from XMPP-based logging (crbug.com/523423).
138 // NOTE: The enums here correspond to the Chromoting.Connections enumerated
139 // histogram defined in src/tools/metrics/histograms/histograms.xml. UMA
140 // histograms don't work well with negative values, so only non-negative values
141 // have been used for Chromoting.Connections.
142 // The maximum values for the UMA enumerated histogram is included here for use
143 // when uploading values to UMA.
144 // The 2 lists should be kept in sync, and any new enums should be append-only.
145 /** @enum {number} */
146 remoting.ClientSession.State = {
147   MIN_STATE_ENUM: -3,
148   CONNECTION_CANCELED: -3,  // Connection closed (gracefully) before connecting.
149   CONNECTION_DROPPED: -2,  // Succeeded, but subsequently closed with an error.
150   CREATED: -1,
151   UNKNOWN: 0,
152   INITIALIZING: 1,
153   CONNECTING: 2,
154   AUTHENTICATED: 3,
155   CONNECTED: 4,
156   CLOSED: 5,
157   FAILED: 6,
158   MAX_STATE_ENUM: 6,
162  * @param {string} state The state name.
163  * @return {remoting.ClientSession.State} The session state enum value.
164  */
165 remoting.ClientSession.State.fromString = function(state) {
166   if (!remoting.ClientSession.State.hasOwnProperty(state)) {
167     throw "Invalid ClientSession.State: " + state;
168   }
169   return remoting.ClientSession.State[state];
172 /** @enum {number} */
173 remoting.ClientSession.ConnectionError = {
174   UNKNOWN: -1,
175   NONE: 0,
176   HOST_IS_OFFLINE: 1,
177   SESSION_REJECTED: 2,
178   INCOMPATIBLE_PROTOCOL: 3,
179   NETWORK_FAILURE: 4,
180   HOST_OVERLOAD: 5
184  * @param {string} error The connection error name.
185  * @return {remoting.ClientSession.ConnectionError} The connection error enum.
186  */
187 remoting.ClientSession.ConnectionError.fromString = function(error) {
188   if (!remoting.ClientSession.ConnectionError.hasOwnProperty(error)) {
189     console.error('Unexpected ClientSession.ConnectionError string: ', error);
190     return remoting.ClientSession.ConnectionError.UNKNOWN;
191   }
192   return remoting.ClientSession.ConnectionError[error];
196  * Type used for performance statistics collected by the plugin.
197  * @constructor
198  */
199 remoting.ClientSession.PerfStats = function() {};
200 /** @type {number} */
201 remoting.ClientSession.PerfStats.prototype.videoBandwidth;
202 /** @type {number} */
203 remoting.ClientSession.PerfStats.prototype.videoFrameRate;
204 /** @type {number} */
205 remoting.ClientSession.PerfStats.prototype.captureLatency;
206 /** @type {number} */
207 remoting.ClientSession.PerfStats.prototype.encodeLatency;
208 /** @type {number} */
209 remoting.ClientSession.PerfStats.prototype.decodeLatency;
210 /** @type {number} */
211 remoting.ClientSession.PerfStats.prototype.renderLatency;
212 /** @type {number} */
213 remoting.ClientSession.PerfStats.prototype.roundtripLatency;
215 // Keys for connection statistics.
216 remoting.ClientSession.STATS_KEY_VIDEO_BANDWIDTH = 'videoBandwidth';
217 remoting.ClientSession.STATS_KEY_VIDEO_FRAME_RATE = 'videoFrameRate';
218 remoting.ClientSession.STATS_KEY_CAPTURE_LATENCY = 'captureLatency';
219 remoting.ClientSession.STATS_KEY_ENCODE_LATENCY = 'encodeLatency';
220 remoting.ClientSession.STATS_KEY_DECODE_LATENCY = 'decodeLatency';
221 remoting.ClientSession.STATS_KEY_RENDER_LATENCY = 'renderLatency';
222 remoting.ClientSession.STATS_KEY_ROUNDTRIP_LATENCY = 'roundtripLatency';
225  * Set of capabilities for which hasCapability() can be used to test.
227  * @enum {string}
228  */
229 remoting.ClientSession.Capability = {
230   // When enabled this capability causes the client to send its screen
231   // resolution to the host once connection has been established. See
232   // this.plugin_.notifyClientResolution().
233   SEND_INITIAL_RESOLUTION: 'sendInitialResolution',
235   // Let the host know that we're interested in knowing whether or not it
236   // rate limits desktop-resize requests.
237   // TODO(kelvinp): This has been supported since M-29.  Currently we only have
238   // <1000 users on M-29 or below. Remove this and the capability on the host.
239   RATE_LIMIT_RESIZE_REQUESTS: 'rateLimitResizeRequests',
241   // Indicates native touch input support. If the host does not support
242   // touch then the client will let Chrome synthesize mouse events from touch
243   // input, for compatibility with non-touch-aware systems.
244   TOUCH_EVENTS: 'touchEvents',
246   // Indicates that host/client supports Google Drive integration, and that the
247   // client should send to the host the OAuth tokens to be used by Google Drive
248   // on the host.
249   GOOGLE_DRIVE: 'googleDrive',
251   // Indicates that the client supports the video frame-recording extension.
252   VIDEO_RECORDER: 'videoRecorder',
254   // Indicates that the client supports 'cast'ing the video stream to a
255   // cast-enabled device.
256   CAST: 'casting',
258   // Indicates desktop shape support.
259   DESKTOP_SHAPE: 'desktopShape',
263  * Connects to |host| using |credentialsProvider| as the credentails.
265  * @param {remoting.Host} host
266  * @param {remoting.CredentialsProvider} credentialsProvider
267  */
268 remoting.ClientSession.prototype.connect = function(host, credentialsProvider) {
269   this.host_ = host;
270   this.credentialsProvider_ = credentialsProvider;
271   this.iqFormatter_ =
272       new remoting.FormatIq(this.signalStrategy_.getJid(), host.jabberId);
273   this.plugin_.connect(this.host_, this.signalStrategy_.getJid(),
274                        credentialsProvider);
278  * Disconnect the current session with a particular |error|.  The session will
279  * raise a |stateChanged| event in response to it.  The caller should then call
280  * dispose() to remove and destroy the <embed> element.
282  * @param {!remoting.Error} error The reason for the disconnection.  Use
283  *    remoting.Error.none() if there is no error.
284  * @return {void} Nothing.
285  */
286 remoting.ClientSession.prototype.disconnect = function(error) {
287   if (this.isFinished()) {
288     // Do not send the session-terminate Iq if disconnect() is already called or
289     // if it is initiated by the host.
290     return;
291   }
293   this.sendIq_(
294       '<cli:iq ' +
295           'to="' + this.host_.jabberId + '" ' +
296           'type="set" ' +
297           'id="session-terminate" ' +
298           'xmlns:cli="jabber:client">' +
299         '<jingle ' +
300             'xmlns="urn:xmpp:jingle:1" ' +
301             'action="session-terminate" ' +
302             'sid="' + this.sessionId_ + '">' +
303           '<reason><success/></reason>' +
304         '</jingle>' +
305       '</cli:iq>');
307   var state = error.isNone() ?
308                   remoting.ClientSession.State.CLOSED :
309                   remoting.ClientSession.State.FAILED;
310   this.error_ = error;
311   this.setState_(state);
315  * Deletes the <embed> element from the container and disconnects.
317  * @return {void} Nothing.
318  */
319 remoting.ClientSession.prototype.dispose = function() {
320   base.dispose(this.connectedDisposables_);
321   this.connectedDisposables_ = null;
322   base.dispose(this.plugin_);
323   this.plugin_ = null;
327  * @return {remoting.ClientSession.State} The current state.
328  */
329 remoting.ClientSession.prototype.getState = function() {
330   return this.state_;
334  * @return {remoting.Logger}.
335  */
336 remoting.ClientSession.prototype.getLogger = function() {
337   return this.logger_;
341  * @return {!remoting.Error} The current error code.
342  */
343 remoting.ClientSession.prototype.getError = function() {
344   return this.error_;
348  * Drop the session when the computer is suspended for more than
349  * |suspendDurationInMS|.
351  * @param {number} suspendDurationInMS maximum duration of suspension allowed
352  *     before the session will be dropped.
353  */
354 remoting.ClientSession.prototype.dropSessionOnSuspend = function(
355     suspendDurationInMS) {
356   if (this.state_ !== remoting.ClientSession.State.CONNECTED) {
357     console.error('The session is not connected.');
358     return;
359   }
361   var suspendDetector = new remoting.SuspendDetector(suspendDurationInMS);
362   this.connectedDisposables_.add(
363       suspendDetector,
364       new base.EventHook(
365           suspendDetector, remoting.SuspendDetector.Events.resume,
366           this.disconnect.bind(
367               this, new remoting.Error(remoting.Error.Tag.CLIENT_SUSPENDED))));
371  * Called when the client receives its first frame.
373  * @return {void} Nothing.
374  */
375 remoting.ClientSession.prototype.onFirstFrameReceived = function() {
376   this.hasReceivedFrame_ = true;
380  * @return {boolean} Whether the client has received a video buffer.
381  */
382 remoting.ClientSession.prototype.hasReceivedFrame = function() {
383   return this.hasReceivedFrame_;
387  * Sends a signaling message.
389  * @param {string} message XML string of IQ stanza to send to server.
390  * @return {void} Nothing.
391  * @private
392  */
393 remoting.ClientSession.prototype.sendIq_ = function(message) {
394   // Extract the session id, so we can close the session later.
395   var parser = new DOMParser();
396   var iqNode = parser.parseFromString(message, 'text/xml').firstChild;
397   var jingleNode = iqNode.firstChild;
398   if (jingleNode) {
399     var action = jingleNode.getAttribute('action');
400     if (jingleNode.nodeName == 'jingle' && action == 'session-initiate') {
401       this.sessionId_ = jingleNode.getAttribute('sid');
402     }
403   }
405   console.log(base.timestamp() + this.iqFormatter_.prettifySendIq(message));
406   if (this.signalStrategy_.getState() !=
407       remoting.SignalStrategy.State.CONNECTED) {
408     console.log("Message above is dropped because signaling is not connected.");
409     return;
410   }
412   this.signalStrategy_.sendMessage(message);
416  * @param {string} message XML string of IQ stanza to send to server.
417  */
418 remoting.ClientSession.prototype.onOutgoingIq = function(message) {
419   this.sendIq_(message);
423  * @param {string} msg
424  */
425 remoting.ClientSession.prototype.onDebugMessage = function(msg) {
426   console.log('plugin: ' + msg.trimRight());
430  * @param {Element} message
431  * @private
432  */
433 remoting.ClientSession.prototype.onIncomingMessage_ = function(message) {
434   if (!this.plugin_) {
435     return;
436   }
437   var formatted = new XMLSerializer().serializeToString(message);
438   console.log(base.timestamp() +
439               this.iqFormatter_.prettifyReceiveIq(formatted));
440   this.xmppErrorCache_.processStanza(message);
441   this.plugin_.onIncomingIq(formatted);
445  * Callback that the plugin invokes to indicate that the connection
446  * status has changed.
448  * @param {remoting.ClientSession.State} status The plugin's status.
449  * @param {remoting.ClientSession.ConnectionError} error The plugin's error
450  *        state, if any.
451  */
452 remoting.ClientSession.prototype.onConnectionStatusUpdate =
453     function(status, error) {
454   if (status == remoting.ClientSession.State.FAILED) {
455     switch (error) {
456       case remoting.ClientSession.ConnectionError.HOST_IS_OFFLINE:
457         this.error_ = new remoting.Error(
458             remoting.Error.Tag.HOST_IS_OFFLINE);
459         break;
460       case remoting.ClientSession.ConnectionError.SESSION_REJECTED:
461         this.error_ = new remoting.Error(
462             remoting.Error.Tag.INVALID_ACCESS_CODE);
463         break;
464       case remoting.ClientSession.ConnectionError.INCOMPATIBLE_PROTOCOL:
465         this.error_ = new remoting.Error(
466             remoting.Error.Tag.INCOMPATIBLE_PROTOCOL);
467         break;
468       case remoting.ClientSession.ConnectionError.NETWORK_FAILURE:
469         this.error_ = new remoting.Error(
470             remoting.Error.Tag.P2P_FAILURE);
471         break;
472       case remoting.ClientSession.ConnectionError.HOST_OVERLOAD:
473         this.error_ = new remoting.Error(
474             remoting.Error.Tag.HOST_OVERLOAD);
475         break;
476       default:
477         this.error_ = remoting.Error.unexpected();
478     }
479   }
480   this.setState_(status);
484  * Callback that the plugin invokes to indicate that the connection type for
485  * a channel has changed.
487  * @param {string} channel The channel name.
488  * @param {string} connectionType The new connection type.
489  * @private
490  */
491 remoting.ClientSession.prototype.onRouteChanged = function(channel,
492                                                            connectionType) {
493   this.logger_.setConnectionType(connectionType);
497  * Callback that the plugin invokes to indicate when the connection is
498  * ready.
500  * @param {boolean} ready True if the connection is ready.
501  */
502 remoting.ClientSession.prototype.onConnectionReady = function(ready) {
503   // TODO(jamiewalch): Currently, the logic for determining whether or not the
504   // connection is available is based solely on whether or not any video frames
505   // have been received recently. which leads to poor UX on slow connections.
506   // Re-enable this once crbug.com/435315 has been fixed.
507   var ignoreVideoChannelState = true;
508   if (ignoreVideoChannelState) {
509     console.log('Video channel ' + (ready ? '' : 'not ') + 'ready.');
510     return;
511   }
513   this.raiseEvent(remoting.ClientSession.Events.videoChannelStateChanged,
514                   ready);
517 /** @return {boolean} */
518 remoting.ClientSession.prototype.isFinished = function() {
519   var finishedStates = [
520     remoting.ClientSession.State.CLOSED,
521     remoting.ClientSession.State.FAILED,
522     remoting.ClientSession.State.CONNECTION_CANCELED,
523     remoting.ClientSession.State.CONNECTION_DROPPED
524   ];
525   return finishedStates.indexOf(this.getState()) !== -1;
528  * @param {remoting.ClientSession.State} newState The new state for the session.
529  * @return {void} Nothing.
530  * @private
531  */
532 remoting.ClientSession.prototype.setState_ = function(newState) {
533   // If we are at a finished state, ignore further state changes.
534   if (this.isFinished()) {
535     return;
536   }
538   var oldState = this.state_;
539   this.state_ = this.translateState_(oldState, newState);
541   if (newState == remoting.ClientSession.State.CONNECTED) {
542     this.connectedDisposables_.add(
543         new base.RepeatingTimer(this.reportStatistics.bind(this), 1000));
544     if (this.plugin_.hasCapability(
545           remoting.ClientSession.Capability.TOUCH_EVENTS)) {
546       this.plugin_.enableTouchEvents(true);
547     }
548   } else if (this.isFinished()) {
549     base.dispose(this.connectedDisposables_);
550     this.connectedDisposables_ = null;
551   }
553   this.notifyStateChanges_(oldState, this.state_);
554   // Record state count in an UMA enumerated histogram.
555   recordState(this.state_);
556   this.logger_.logClientSessionStateChange(
557       this.state_, this.error_, this.xmppErrorCache_.getFirstError());
561  * Records a Chromoting Connection State, stored in an UMA enumerated histogram.
562  * @param {remoting.ClientSession.State} state State identifier.
563  */
564 function recordState(state) {
565   // According to src/base/metrics/histogram.h, for a UMA enumerated histogram,
566   // the upper limit should be 1 above the max-enum.
567   var histogram_max = remoting.ClientSession.State.MAX_STATE_ENUM -
568       remoting.ClientSession.State.MIN_STATE_ENUM + 1;
570   var metricDescription = {
571     metricName: 'Chromoting.Connections',
572     type: 'histogram-linear',
573     // According to histogram.h, minimum should be 1. Values less than minimum
574     // end up in the 0th bucket.
575     min: 1,
576     max: histogram_max,
577     // The # of buckets should include 1 for underflow.
578     buckets: histogram_max + 1
579   };
581   chrome.metricsPrivate.recordValue(metricDescription, state -
582       remoting.ClientSession.State.MIN_STATE_ENUM);
586  * @param {remoting.ClientSession.State} oldState The new state for the session.
587  * @param {remoting.ClientSession.State} newState The new state for the session.
588  * @private
589  */
590 remoting.ClientSession.prototype.notifyStateChanges_ =
591     function(oldState, newState) {
592   /** @type {remoting.Error} */
593   var error;
594   switch (this.state_) {
595     case remoting.ClientSession.State.CONNECTED:
596       console.log('Connection established.');
597       var connectionInfo = new remoting.ConnectionInfo(
598           this.host_, this.credentialsProvider_, this, this.plugin_);
599       this.listener_.onConnected(connectionInfo);
600       break;
602     case remoting.ClientSession.State.CONNECTING:
603       remoting.identity.getEmail().then(function(/** string */ email) {
604         console.log('Connecting as ' + email);
605       });
606       break;
608     case remoting.ClientSession.State.AUTHENTICATED:
609       console.log('Connection authenticated.');
610       break;
612     case remoting.ClientSession.State.INITIALIZING:
613       console.log('Connection initializing .');
614       break;
616     case remoting.ClientSession.State.CLOSED:
617       console.log('Connection closed.');
618       this.listener_.onDisconnected(remoting.Error.none());
619       break;
621     case remoting.ClientSession.State.CONNECTION_CANCELED:
622     case remoting.ClientSession.State.FAILED:
623       error = this.getError();
624       if (!error.isNone()) {
625         console.error('Connection failed: ' + error.toString());
626       }
627       this.listener_.onConnectionFailed(error);
628       break;
630     case remoting.ClientSession.State.CONNECTION_DROPPED:
631       error = this.getError();
632       console.error('Connection dropped: ' + error.toString());
633       this.listener_.onDisconnected(error);
634       break;
636     default:
637       console.error('Unexpected client plugin state: ' + newState);
638   }
642  * @param {remoting.ClientSession.State} previous
643  * @param {remoting.ClientSession.State} current
644  * @return {remoting.ClientSession.State}
645  * @private
646  */
647 remoting.ClientSession.prototype.translateState_ = function(previous, current) {
648   var State = remoting.ClientSession.State;
649   if (previous == State.CONNECTING || previous == State.AUTHENTICATED) {
650     if (current == State.CLOSED) {
651       return remoting.ClientSession.State.CONNECTION_CANCELED;
652     }
653   } else if (previous == State.CONNECTED && current == State.FAILED) {
654     return State.CONNECTION_DROPPED;
655   }
656   return current;
659 /** @private */
660 remoting.ClientSession.prototype.reportStatistics = function() {
661   this.logger_.logStatistics(this.plugin_.getPerfStats());